代码演示环境:
在2D游戏中,地图是整体结构,或者我们叫做游戏地图(game map),通常是几个屏幕的宽度表示。有些游戏地图是屏幕的20倍;甚至是100位以上,主要特点是跨屏幕之后,让地图滚动显示,这种类型的游戏又叫做2D平台游戏(2D platform game)。
所以平台游戏是指玩家从一个平台跑到另外一平台,在其中需要跑、跳等动作,除此之外,还要避开敌人,以及采血、加体力等动作。本章我们介绍怎样创建基本的地图、地图文件、碰撞侦测、加体力、简单的敌人,以及生成背景的视差滚动效果等。
如果在游戏中如果巨幅图片,这种策略不是最好的解决方案,因为这会产生大量的内存消耗,可能会导致不装载图片。
另外,巨幅图片不能说明玩家哪个地图可以使用,哪些地图不可以使用。解决这个问题的一般策略是使用基于title的图片。tile-base地图是把地图分解决成表格,每个单元格包含一小块的图片,或者没有图片。如下图示:
基于tile的地力点有点像使用预制块来创建游戏,不是同的就是这些块的颜色不,并且可以无限制使用颜色。
Tile地力的包含的引用属于表格的每个单元格(cell)所有,这样,我们只需要一些小图片就可以实现整个tile的画面显示,并且我们可以根据游戏的需求无限制创建背景画面,而不担心内存的约束问题。大多数游戏都使用16或者32位的图片来表示单元格,我们这里使用是64位的图片,如下图所示:
以上就是基于tile的地图,它有九个块效果可以很容易决定哪些是“solid”部分,哪些是”empty”的地图,这样,你可以知道哪部分地图玩家可以跳,哪部分玩家可以穿墙。下面我们首先实现这个Tile地图。
package com.funfree.arklis.engine.tilegame;
import java.awt.Image;
import java.util.LinkedList;
import java.util.Iterator;
import com.funfree.arklis.graphic.*;
/**
功能:书写一个类用来表示Tile地图
作者:技术大黍
备注:该类包含的数据用来实现地图、包括小怪。每个tile引用一个图片。当然这些图片会被
使用多次。
*/
public class TileMap{
private Image[][] tiles; //表示地图
private LinkedList sprites; //表示游戏中的小怪
private Sprite player; //表示玩家
/**
初始化成员变量时指定地图的宽和高
*/
public TileMap(int width, int height){
tiles = new Image[width][height];
sprites = new LinkedList();
}
/**
返回地图的宽度
*/
public int getWidth(){
return tiles.length;
}
/**
获取地图的高度
*/
public int getHeight(){
return tiles[0].length;
}
/**
根据指定的坐标来获取相应的tile。如果返回null值,那么表示地图越界了。
*/
public Image getTile(int x, int y){
if(x < 0 || x >= getWidth() ||
y < 0 || y >= getHeight()){
//那么回返null值
return null;
}else{
//否则返回tile
return tiles[x][y];
}
}
/**
根据指定的坐标和指定的图片来更换tile
*/
public void setTile(int x, int y, Image tile){
tiles[x][y] = tile;
}
/**
返回玩家角色
*/
public Sprite getPlayer(){
return player;
}
/**
指定玩家角色
*/
public void setPlayer(Sprite player){
this.player = player;
}
/**
添加小怪地图中
*/
public void addSprite(Sprite sprite){
sprites.add(sprite);
}
/**
删除该地图中的小怪
*/
public void removeSprite(Sprite sprite){
sprites.remove(sprite);
}
/**
迭代出该地图中所有的小怪,除了玩家本身之外。
*/
public Iterator getSprites(){
return sprites.iterator();
}
}
除了地图,TileMap还包含在游戏中的小怪,小怪可以地图中任何位置,并且没有边界的限制。如下图:
下面我们需要有一个地方来保存该地图,然后在恰当的时候实际创建该地图。Tile地图游戏总是有多个级别的地图,该示例也不例外。如果我们可以很轻松的方式来创建多个地图,那么玩家可以在完成一个地图之后,然后开始下一个地图的游戏情节。
我们创建地图时呼叫TileMap的addTile()方法和addSprite()方法,该方法的灵活性非常好,但是,这样编辑地图的级别比较困难,并且代码本身也不是很优雅。所以,大多数的tile游戏有自己的地图编辑器来创建地图。这个地图编辑器是可视化添加tile和小怪到游戏中,这样做的方式是非常简捷的方式。
一般把地图保存到中介地图文件中,而这个文件是可以让游戏解析的。这样,我们可定义一个基于文本地图文件,这样我们可以编辑地图,因为tile是被定义在一个表格中的,所以文本文件中的每个字符可以表示一个tile或者是一个小怪/玩家,如下图:
其中”#”表示注释,而其它的表示tile的row。该地图是固定的,所以可以我们可让地图变量,并且可能添加更多的line或者让line更长。那么解析地图的步骤有三步:
完成这个工作是ResourceManager类。需要注意的是:添加sprite到TileMap中去时,开始,我们需要创建不同的Sprite对象,这样,我们可根据这些“主”怪来克隆小怪;
第二,每个sprite不需要尺寸与tile的尺寸一样,所以,我们需要保证每个sprite在tile中的中央,这些事件都在addSprite()方法完成。本章以前的Sprite的位置相同的屏幕,但是在本章示例中,sprite的位置是相同到tile地图。我们使用TileMapRender的静态方法titlesToPixels()来转换tile位置到地图的位置。该函数乘以tile的数值,
int pixelSize = numTiles * TITLE_SIZE;
以上公式可以让sprite移动到地图上的任意一个位置,并且不需要调整tile的边界。也就是说,我们有一个灵活的方式来创建地图和解析它们,以及创建一个TileMap对象。
在示例中,所有的地图都在map文件夹中(map1.txt和map2.txt)等等。如果我们需要下一个地图,只需要让代码去寻找下一个地图文件即可;如果没有找到,代码回装载第一个地图。也就是说,我们不需要新地图,只需要在这个目录中删除地图文件即可,也不需要告诉游戏有多少个地图存在。
package com.funfree.arklis.engine.tilegame;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.io.*;
import java.util.ArrayList;
import javax.swing.ImageIcon;
import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;
/**
功能:书写一个ResourceManager类用来装载和管理tile图片和“主”怪。游戏中的
小怪是从“主”怪克隆而来。
作者:技术大黍
备注:该类有第四章GameCore所有功能。
*/
public class ResourceManager {
private ArrayList tiles; //保存文字地图的集合
private int currentMap; //当前地图
private GraphicsConfiguration gc; //显示卡
// 用来被克隆的主怪
private Sprite playerSprite;
private Sprite musicSprite;
private Sprite coinSprite;
private Sprite goalSprite;
private Sprite grubSprite;
private Sprite flySprite;
/**
使用指定的显卡来创建资源管理器对象
*/
public ResourceManager(GraphicsConfiguration gc) {
this.gc = gc;
loadTileImages();
loadCreatureSprites();
loadPowerUpSprites();
}
/**
从images目录获取图片
*/
public Image loadImage(String name) {
String filename = "images/" + name;
return new ImageIcon(filename).getImage();
}
//获取Mirror图片
public Image getMirrorImage(Image image) {
return getScaledImage(image, -1, 1);
}
//获取反转后的图片
public Image getFlippedImage(Image image) {
return getScaledImage(image, 1, -1);
}
/**
完成图片的转换功能
*/
private Image getScaledImage(Image image, float x, float y) {
// 设置一个图片转换对象
AffineTransform transform = new AffineTransform();
transform.scale(x, y);
transform.translate(
(x-1) * image.getWidth(null) / 2,
(y-1) * image.getHeight(null) / 2);
// 创建透明的图片(不同半透明)
Image newImage = gc.createCompatibleImage(
image.getWidth(null),
image.getHeight(null),
Transparency.BITMASK);
// 绘制透明图片
Graphics2D g = (Graphics2D)newImage.getGraphics();
g.drawImage(image, transform, null);
g.dispose();
return newImage;
}
/**
从maps目录中装载下一下地图
*/
public TileMap loadNextMap() {
TileMap map = null;
while (map == null) {
currentMap++;
try {
map = loadMap(
"maps/map" + currentMap + ".txt");
}
catch (IOException ex) {
if (currentMap == 1) {
// 无装载的地图,返回null值!
return null;
}
currentMap = 0;
map = null;
}
}
return map;
}
/**
重新装载maps目录下的地图文本
*/
public TileMap reloadMap() {
try {
return loadMap(
"maps/map" + currentMap + ".txt");
}
catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
完成装载地图的核心方法
*/
private TileMap loadMap(String filename)throws IOException{
ArrayList lines = new ArrayList();
int width = 0;
int height = 0;
// 读取文本文件中的每一行内容到集合中保存
BufferedReader reader = new BufferedReader(
new FileReader(filename));
for(;;) {
String line = reader.readLine();
// 没有内容可读取了
if (line == null) {
reader.close();
break;
}
// 添加每一行记录,除了注释
if (!line.startsWith("#")) {
lines.add(line);
width = Math.max(width, line.length());
}
}
// 解析每一行,以便创建TileEngine对象
height = lines.size();
TileMap newMap = new TileMap(width, height);
for (int y = 0; y < height; y++) {
//获取集合中的字符串对象
String line = (String)lines.get(y);
//把每个字符中的字符取出来
for (int x = 0; x < line.length(); x++) {
char ch = line.charAt(x);
// 检查字符是否为A,B,C等字符
int tile = ch - 'A';
//如果是字符A
if (tile >= 0 && tile < tiles.size()) {
//那么根据tile值来创建地图元素--这里地图实现的核心方法
newMap.setTile(x, y, (Image)tiles.get(tile));
}
// 如果字符是表示小怪的,比如0, !或者*,那么分别添加主怪到集合中
else if (ch == 'o') {
addSprite(newMap, coinSprite, x, y);
}else if (ch == '!') {
addSprite(newMap, musicSprite, x, y);
}else if (ch == '*') {
addSprite(newMap, goalSprite, x, y);
}else if (ch == '1') {
addSprite(newMap, grubSprite, x, y);
}else if (ch == '2') {
addSprite(newMap, flySprite, x, y);
}
}
}
// 添加玩家到地图中去
Sprite player = (Sprite)playerSprite.clone();
player.setX(TileMapRenderer.tilesToPixels(3));
player.setY(0);
newMap.setPlayer(player);
//返回新的tile地图对象
return newMap;
}
/**
添加小怪到地图中去,并且是指定是的位置。
*/
private void addSprite(TileMap map,Sprite hostSprite, int tileX, int tileY){
if (hostSprite != null) {
// 从“主”怪克隆小怪
Sprite sprite = (Sprite)hostSprite.clone();
// 把小怪置中
sprite.setX(
TileMapRenderer.tilesToPixels(tileX) +
(TileMapRenderer.tilesToPixels(1) -
sprite.getWidth()) / 2);
// 在底部调试该小怪
sprite.setY(
TileMapRenderer.tilesToPixels(tileY + 1) -
sprite.getHeight());
// 添加该小怪到地图中去
map.addSprite(sprite);
}
}
// -----------------------------------------------------------
// 实现装载小怪和图片的代码
// -----------------------------------------------------------
public void loadTileImages() {
//保存查找A,B,C等字符,这样可以非常方便的删除images目录下的tiles
tiles = new ArrayList();
char ch = 'A';
while (true) {
String name = "tile_" + ch + ".png";
File file = new File("images/" + name);
if (!file.exists()) {
break;
}
tiles.add(loadImage(name));
ch++;
}
}
public void loadCreatureSprites() {
//声明一个图片至少保存4个图片的数组
Image[][] images = new Image[4][];
// 装载左边朝向的图片
images[0] = new Image[] {
//装载玩家图片
loadImage("player1.png"),
loadImage("player2.png"),
loadImage("player3.png"),
//装载苍蝇图片
loadImage("fly1.png"),
loadImage("fly2.png"),
loadImage("fly3.png"),
//装载蠕虫图片
loadImage("grub1.png"),
loadImage("grub2.png"),
};
images[1] = new Image[images[0].length];
images[2] = new Image[images[0].length];
images[3] = new Image[images[0].length];
for (int i = 0; i < images[0].length; i++) {
// 装载右朝向的图片
images[1][i] = getMirrorImage(images[0][i]);
// 装载左朝向“死亡”图片
images[2][i] = getFlippedImage(images[0][i]);
// 装载右朝向“死亡”图片
images[3][i] = getFlippedImage(images[1][i]);
}
// 创建creature动画对象
Animation[] playerAnim = new Animation[4];
Animation[] flyAnim = new Animation[4];
Animation[] grubAnim = new Animation[4];
for (int i = 0; i < 4; i++) {
playerAnim[i] = createPlayerAnim(
images[i][0], images[i][1], images[i][2]);
flyAnim[i] = createFlyAnim(
images[i][3], images[i][4], images[i][5]);
grubAnim[i] = createGrubAnim(
images[i][6], images[i][7]);
}
// 创建creature小怪(包括玩家)
playerSprite = new Player(playerAnim[0], playerAnim[1],
playerAnim[2], playerAnim[3]);
flySprite = new Fly(flyAnim[0], flyAnim[1],
flyAnim[2], flyAnim[3]);
grubSprite = new Grub(grubAnim[0], grubAnim[1],
grubAnim[2], grubAnim[3]);
}
//根据指定的图片来创建玩家动画对象
private Animation createPlayerAnim(Image player1,Image player2, Image player3){
Animation anim = new Animation();
anim.addFrame(player1, 250);
anim.addFrame(player2, 150);
anim.addFrame(player1, 150);
anim.addFrame(player2, 150);
anim.addFrame(player3, 200);
anim.addFrame(player2, 150);
return anim;
}
//根据指定的图片来创建苍蝇动画对象
private Animation createFlyAnim(Image img1, Image img2,Image img3){
Animation anim = new Animation();
anim.addFrame(img1, 50);
anim.addFrame(img2, 50);
anim.addFrame(img3, 50);
anim.addFrame(img2, 50);
return anim;
}
//根据指定的图片来创建蠕虫动画对象
private Animation createGrubAnim(Image img1, Image img2) {
Animation anim = new Animation();
anim.addFrame(img1, 250);
anim.addFrame(img2, 250);
return anim;
}
private void loadPowerUpSprites() {
// 创建“心”怪用来加体力
Animation anim = new Animation();
anim.addFrame(loadImage("heart1.png"), 150);
anim.addFrame(loadImage("heart2.png"), 150);
anim.addFrame(loadImage("heart3.png"), 150);
anim.addFrame(loadImage("heart2.png"), 150);
goalSprite = new PowerUp.Goal(anim);
// 创建 "星"怪,用来加分
anim = new Animation();
anim.addFrame(loadImage("star1.png"), 100);
anim.addFrame(loadImage("star2.png"), 100);
anim.addFrame(loadImage("star3.png"), 100);
anim.addFrame(loadImage("star4.png"), 100);
coinSprite = new PowerUp.Star(anim);
// 创建“音乐”怪用来加速玩家
anim = new Animation();
anim.addFrame(loadImage("music1.png"), 150);
anim.addFrame(loadImage("music2.png"), 150);
anim.addFrame(loadImage("music3.png"), 150);
anim.addFrame(loadImage("music2.png"), 150);
musicSprite = new PowerUp.Music(anim);
}
}
我们知道tile地图大于屏幕,所以只有一部分地图同一时间在屏幕上显示。玩家的移动的原理是:地图滚动来保持玩家在屏幕的中央位置如下图所示:
它的计算公式如下:
int offsetX = screenWidth / 2 – Math.round(player.getX()) – TITLE_SIZE;
该公式把屏幕的水平位置赋值给offsetX变量,这个公式不复杂,所以我们需要确保玩家在离开左边边缘到地图右边边缘时,地图滚动必须停止,这样地图的边缘不会被显示在屏幕上。于是,我们需要这样限制:
int mapWidth = titlesToPixels(map.getWidth());
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);
int offsetY = screenHeight – titlesToPixels(map.getHeight());
完整的计算公式如下图所示:
完整的代码参见TileMapRenderer类。
package com.funfree.arklis.engine.tilegame;
import java.awt.*;
import java.util.Iterator;
import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;
/**
功能:该类用来绘制TileMap到屏幕中,是一个核心工具类。它绘制所有tile、小怪(包括玩家),以及可选的
背景图片到玩家的位置。如果背景图片宽度小于tile地图的宽度,那么背景图片会慢慢移动出现,从而产生
视差效果给玩家。有三个静态方法用来转换像素到tile的位置,返回既然。注意:该类使用tile的尺寸是64位
作者:技术大黍
备注:该类是核心类,它完成tile的呈现功能。
*/
public class TileMapRenderer {
//指定tile的固定大小
private static final int TILE_SIZE = 64;
//使用bit为单位表示tile的尺寸 Math.pow(2,TILE_SIZE_BITS) == TILE_SIZE
private static final int TILE_SIZE_BITS = 6;
private Image background;
/**
转换像素的位置为tile的位置
*/
public static int pixelsToTiles(float pixels) {
return pixelsToTiles(Math.round(pixels));
}
/**
转换像素的位置为tile的位置
*/
public static int pixelsToTiles(int pixels) {
//把正确的值转换成负值像素
return pixels >> TILE_SIZE_BITS;
//或者使用floor方法,而不是power函数计算tile的尺寸
//return (int)Math.floor((float)pixels / TILE_SIZE);
}
/**
转换tile的位置为像素的位置
*/
public static int tilesToPixels(int numTiles) {
//该方法移位的目标是加速作用,但是实际上对于现代的处理器效果不大
return numTiles << TILE_SIZE_BITS;
//或者使用乘法运算,而不使用power方法计算tile的尺寸
//return numTiles * TILE_SIZE;
}
/**
设置背景图片
*/
public void setBackground(Image background) {
this.background = background;
}
/**
绘制指定的TileMap
*/
public void draw(Graphics2D g, TileMap map,int screenWidth, int screenHeight){
//取得玩家角色
Sprite player = map.getPlayer();
//获取的宽度
int mapWidth = tilesToPixels(map.getWidth());
//根据玩家当前的位置获取地图滚动的位置(屏幕宽度一半减去当前玩家X坐标减去TILE_SIZE)
int offsetX = screenWidth / 2 -
Math.round(player.getX()) - TILE_SIZE;
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);
// 取得y的偏移量,然后绘制所有sprites和tiles
int offsetY = screenHeight -
tilesToPixels(map.getHeight());
// 绘制黑色背景
if (background == null ||
screenHeight > background.getHeight(null)){
g.setColor(Color.black);
g.fillRect(0, 0, screenWidth, screenHeight);
}
// 绘制视觉差效果的背景图片
if (background != null) {
int x = offsetX *
(screenWidth - background.getWidth(null)) /
(screenWidth - mapWidth);
int y = screenHeight - background.getHeight(null);
g.drawImage(background, x, y, null);
}
// 绘制可视化的tiles
int firstTileX = pixelsToTiles(-offsetX);
int lastTileX = firstTileX + pixelsToTiles(screenWidth) + 1;
for (int y = 0; y < map.getHeight(); y++) {
for (int x = firstTileX; x <= lastTileX; x++) {
Image image = map.getTile(x, y);
if (image != null) {
g.drawImage(image,
tilesToPixels(x) + offsetX,
tilesToPixels(y) + offsetY,
null);
}
}
}
// 绘制玩家
g.drawImage(player.getImage(),
Math.round(player.getX()) + offsetX,
Math.round(player.getY()) + offsetY,
null);
// 绘制小怪
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite sprite = (Sprite)i.next();
int x = Math.round(sprite.getX()) + offsetX;
int y = Math.round(sprite.getY()) + offsetY;
g.drawImage(sprite.getImage(), x, y, null);
// 当sprite对象在屏幕上时需要唤醒creature对象
if (sprite instanceof Creature &&
x >= 0 && x < screenWidth){
((Creature)sprite).wakeUp();
}
}
}
}
在绘制tile之后,我们需要绘制sprite图形。这里我们分开来绘制sprite对象,它的思路如下:
前两个思路不用置疑的,因为在地图有许多sprite对象,但是,不是每张地图都有很多sprite对象,所以你可brute-force方法来检查每个sprite是否可视。也就是遍历列表,绘制出每个sprite对象:
Iterator i = map.getSprites();
while(i.hasNext()){
Sprite sprite = (Sprite)i.next();
int x = Math.round(sprite.getX()) + offsetX;
int y = Math.round(sprite.getY()) + offsetY;
g.drawImage(sprite.getImage(),x,y,null);
}
现在我们已经绘制了tile和sprite对象,接下面我们绘制背景。当我们绘制背景时,我们需要怎样把背景合成为地图,实现的策略如下:
第三种方式我们叫”parallax scrolling”(视觉差滚动),视差出现的原理是:从不同的视点让对象在不同的位置出现。比如,我们讲过不会使用巨幅图片表示背景,如果我们要创建背景地图是屏幕的两倍,背景从屏幕的第一个屏幕到第二个屏幕
package com.funfree.arklis.engine.tilegame;
import java.awt.*;
import java.awt.event.*;
import java.util.Iterator;
import static java.lang.System.*;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.sampled.AudioFormat;
import com.funfree.arklis.sounds.*;
import javax.swing.border.*;
import javax.swing.*;
import com.funfree.arklis.engine.tilegame.*;
import com.funfree.arklis.engine.*;
import com.funfree.arklis.input.*;
import com.funfree.arklis.graphic.*;
import com.funfree.arklis.engine.tilegame.sprites.*;
/**
功能:该类管理游戏中所有方面,包括碰撞侦测等等。
作者:技术大黍
*/
public class GameManager extends GameCore {
// 示压缩4400Hz、16位、单声道little-endian顺序
private static final AudioFormat PLAYBACK_FORMAT =
new AudioFormat(44100, 16, 1, true, false);
private static final int DRUM_TRACK = 1;
public static final float GRAVITY = 0.002f;
private Point pointCache = new Point();
private TileMap map;
private MidiPlayer midiPlayer;
private SoundManager soundManager;
private ResourceManager resourceManager;
private Sound prizeSound;
private Sound boopSound;
private InputManager inputManager;
private TileMapRenderer renderer;
//玩家的四个游戏行为
private GameAction moveLeft;
private GameAction moveRight;
private GameAction jump;
private GameAction exit;
public void init() {
super.init();
// 设置输入管理器
initInput();
// 启动资源管理器
resourceManager = new ResourceManager(
screen.getFullScreenWindow().getGraphicsConfiguration());
// 装载资源
renderer = new TileMapRenderer();
renderer.setBackground(
resourceManager.loadImage("background.jpg"));
// 载入第一个地图
map = resourceManager.loadNextMap();
// 载入声音
soundManager = new SoundManager(PLAYBACK_FORMAT);
prizeSound = soundManager.getSound("sounds/prize.wav");
boopSound = soundManager.getSound("sounds/boop2.wav");
// 播放声音
midiPlayer = new MidiPlayer();
Sequence sequence =
midiPlayer.getSequence("sounds/music.midi");
midiPlayer.play(sequence, true);
toggleDrumPlayback();
}
/**
放一个接口方法,以便关联类使用,比如InputComponent类使用。
*/
public InputManager getInputManager(){
return inputManager;
}
/**
添加有游戏行为的名称,以便让InputComponent使用这些文字来修改被影射的键
*/
private void addActionConfig(JPanel configPanel, GameAction action){
JLabel label = new JLabel(action.getName(), JLabel.RIGHT);
com.funfree.arklis.engine.InputComponent input =
new com.funfree.arklis.engine.InputComponent(action,this);
configPanel.add(label);
configPanel.add(input);
getList().add(input);//放到集合中保存起来
}
/**
关闭所有的被使用的资源
*/
public void stop() {
super.stop();
midiPlayer.close();
soundManager.close();
}
private void initInput() {
moveLeft = new GameAction("左移");
moveRight = new GameAction("右移");
jump = new GameAction("跳",
GameAction.DETECT_INITIAL_PRESS_ONLY);
exit = new GameAction("退出",
GameAction.DETECT_INITIAL_PRESS_ONLY);
inputManager = new InputManager(
screen.getFullScreenWindow());
inputManager.setCursor(InputManager.INVISIBLE_CURSOR);
inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT);
inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT);
inputManager.mapToKey(jump, KeyEvent.VK_SPACE);
inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);
}
/**
检查输入
*/
private void checkInput(long elapsedTime) {
if (exit.isPressed()) {
stop();
}
Player player = (Player)map.getPlayer();
if (player.isAlive()) {
float velocityX = 0;
if (moveLeft.isPressed()) {
velocityX -= player.getMaxSpeed();
}
if (moveRight.isPressed()) {
velocityX += player.getMaxSpeed();
}
if (jump.isPressed()) {
player.jump(false);
}
player.setVelocityX(velocityX);
}
}
//这里最关键的部分是使用呈现器TileMapRenderer类来封装所有复杂的呈现过程。
public void draw(Graphics2D g) {
renderer.draw(g, map,
screen.getWidth(), screen.getHeight());
}
/**
获取当前的地图
*/
public TileMap getMap() {
return map;
}
/**
打开/关闭背景midi音乐
*/
public void toggleDrumPlayback() {
Sequencer sequencer = midiPlayer.getSequencer();
if (sequencer != null) {
sequencer.setTrackMute(DRUM_TRACK,
!sequencer.getTrackMute(DRUM_TRACK));
}
}
/**
获取Sprite碰撞的tile对象。只需要Sprite的x和y值需要被修改,但是不是同时修改。
如果返回null表示没有侦测到Sprite的碰撞。该方法是实现游戏的核心方法!
*/
public Point getTileCollision(Sprite sprite,float newX, float newY){
float fromX = Math.min(sprite.getX(), newX);
float fromY = Math.min(sprite.getY(), newY);
float toX = Math.max(sprite.getX(), newX);
float toY = Math.max(sprite.getY(), newY);
// 获取tile的位置
int fromTileX = TileMapRenderer.pixelsToTiles(fromX);
int fromTileY = TileMapRenderer.pixelsToTiles(fromY);
int toTileX = TileMapRenderer.pixelsToTiles(
toX + sprite.getWidth() - 1);
int toTileY = TileMapRenderer.pixelsToTiles(
toY + sprite.getHeight() - 1);
// 检查是否有碰撞的title
for (int x = fromTileX; x <= toTileX; x++) {
for (int y = fromTileY; y <= toTileY; y++) {
if (x < 0 || x >= map.getWidth() ||
map.getTile(x, y) != null){
// 碰撞发现了,返回碰撞的tile
pointCache.setLocation(x, y);
return pointCache;
}
}
}
// 没有碰撞
return null;
}
/**
检查两个Sprite是否发生了碰撞。如果两个对象同一种类,那么返回false值。如果
一个Sprites是Creatue类并且是死的,那么也返回false值。
*/
public boolean isCollision(Sprite s1, Sprite s2) {
// 如果两个Sprite是同一对象,那么返回false值
if (s1 == s2) {
return false;
}
// 如果有一个Sprite是死的,那么返回false值
if (s1 instanceof Creature && !((Creature)s1).isAlive()) {
return false;
}
if (s2 instanceof Creature && !((Creature)s2).isAlive()) {
return false;
}
// 否则获取Sprite的像素位置
int s1x = Math.round(s1.getX());
int s1y = Math.round(s1.getY());
int s2x = Math.round(s2.getX());
int s2y = Math.round(s2.getY());
// 然后检查两个sprite的边界是否交叉
return (s1x < s2x + s2.getWidth() &&
s2x < s1x + s1.getWidth() &&
s1y < s2y + s2.getHeight() &&
s2y < s1y + s1.getHeight());
}
/**
获取与指定Sprite碰撞的Sprite对象,如果返回null值,那么表示Sprite没有与指定的Sprite碰撞。
*/
public Sprite getSpriteCollision(Sprite sprite) {
// 遍历Sprite列表
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite otherSprite = (Sprite)i.next();
if (isCollision(sprite, otherSprite)) {
// 如果发现碰撞,那么返回该Sprite对象
return otherSprite;
}
}
// 否则没有碰撞发生
return null;
}
/**
更新当前地图中的所有Sprite的Animation、position和速率。
*/
public void update(long elapsedTime) {
Creature player = (Creature)map.getPlayer();
// 如果玩家死亡!那么重新启动地图
if (player.getState() == Creature.STATE_DEAD) {
map = resourceManager.reloadMap();
return;
}
// 获取键盘/鼠标的输入
checkInput(elapsedTime);
// 更新玩家
updateCreature(player, elapsedTime);
player.update(elapsedTime);
// 更新其它的sprite对象
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite sprite = (Sprite)i.next();
if (sprite instanceof Creature) {
Creature creature = (Creature)sprite;
if (creature.getState() == Creature.STATE_DEAD) {
i.remove();
}else {
updateCreature(creature, elapsedTime);
}
}
// 普通更新
sprite.update(elapsedTime);
}
}
/**
更新create对象,让所有creaute没有飞行的下降,然后检查它们是否有碰撞
*/
private void updateCreature(Creature creature,long elapsedTime){
// 应用加速度
if (!creature.isFlying()) {
creature.setVelocityY(creature.getVelocityY() +
GRAVITY * elapsedTime);
}
// 修改x值
float dx = creature.getVelocityX();
float oldX = creature.getX();
float newX = oldX + dx * elapsedTime;
Point tile = getTileCollision(creature, newX, creature.getY());
if (tile == null) {
creature.setX(newX);
}else {
// 画出tile边界
if (dx > 0) {
creature.setX(
TileMapRenderer.tilesToPixels(tile.x) -
creature.getWidth());
}else if (dx < 0) {
creature.setX(
TileMapRenderer.tilesToPixels(tile.x + 1));
}
creature.collideHorizontal();
}
if (creature instanceof Player) {
checkPlayerCollision((Player)creature, false);
}
// 修改y值
float dy = creature.getVelocityY();
float oldY = creature.getY();
float newY = oldY + dy * elapsedTime;
tile = getTileCollision(creature, creature.getX(), newY);
if (tile == null) {
creature.setY(newY);
}else {
// 画出tile的边界
if (dy > 0) {
creature.setY(
TileMapRenderer.tilesToPixels(tile.y) -
creature.getHeight());
}else if (dy < 0) {
creature.setY(
TileMapRenderer.tilesToPixels(tile.y + 1));
}
creature.collideVertical();
}
if (creature instanceof Player) {
boolean canKill = (oldY < creature.getY());
checkPlayerCollision((Player)creature, canKill);
}
}
/**
检查玩家是否与其它的Sprite发生碰撞。如果可以玩家Sprite对象,那么返回true值。
*/
public void checkPlayerCollision(Player player,boolean canKill){
//如果玩家死亡
if (!player.isAlive()) {
return;//那么不做为
}
// 检查玩家是否与其它的sprite发生碰撞
Sprite collisionSprite = getSpriteCollision(player);
if (collisionSprite instanceof PowerUp) {
acquirePowerUp((PowerUp)collisionSprite);
}
else if (collisionSprite instanceof Creature) {
Creature badguy = (Creature)collisionSprite;
if (canKill) {
// 杀死小怪,然后让玩家弹起
soundManager.play(boopSound);
badguy.setState(Creature.STATE_DYING);
player.setY(badguy.getY() - player.getHeight());
player.jump(true);
}
else {
// 否则玩家死亡!
player.setState(Creature.STATE_DYING);
}
}
}
/**
给玩家加技能、分、体力,然后从地图上移除这些元素。
*/
public void acquirePowerUp(PowerUp powerUp) {
// 从地图移除poswerUp
map.removeSprite(powerUp);
if (powerUp instanceof PowerUp.Star) {
// 给玩家加点
soundManager.play(prizeSound);
}else if (powerUp instanceof PowerUp.Music) {
// 修改音乐
soundManager.play(prizeSound);
toggleDrumPlayback();
}else if (powerUp instanceof PowerUp.Goal) {
// 进入到下一张地图
soundManager.play(prizeSound,
new EchoFilter(2000, .7f), false);
map = resourceManager.loadNextMap();
}
}
/**
功能:该方法是一个非常重要的辅助方法--用来创建游戏的菜单
*/
private JButton createButton(String name, String toolTip){
//装载图片
String imagePath = "images/menu/" + name + ".png";
ImageIcon iconRollover = new ImageIcon(imagePath);
int w = iconRollover.getIconWidth();
int h = iconRollover.getIconHeight();
//给当前按钮设置光标的样式
Cursor cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
//让默认的图片透明
Image image = screen.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
Graphics2D g = (Graphics2D)image.getGraphics();
Composite alpha = AlphaComposite.getInstance(AlphaComposite.DST_OVER, .5f);
g.setComposite(alpha);//设置透明度
g.drawImage(iconRollover.getImage(),0,0,null);
g.dispose();
ImageIcon iconDefault = new ImageIcon(image);
//显示被按下的图片
image = screen.createCompatibleImage(w,h,Transparency.TRANSLUCENT);
g = (Graphics2D)image.getGraphics();
g.drawImage(iconRollover.getImage(),2,2,null);
g.dispose();
ImageIcon iconPressed = new ImageIcon(image);
//创建按钮对象
JButton button = new JButton();
button.addActionListener(this);
button.setIgnoreRepaint(true);
button.setFocusable(false);
button.setToolTipText(toolTip);
button.setBorder(null);
button.setContentAreaFilled(false);
button.setCursor(cursor);
button.setIcon(iconDefault);
button.setRolloverIcon(iconRollover);
button.setPressedIcon(iconPressed);
return button;
}
}
在我们计算了offsetX—它表示地图在屏幕中的位置,所以我们需要一个公式把offsetX转换成backgroundX的值。offsetX的范围是从0(地图的左半部分开始)到screenWidth-mapWidth(地图的右半部分),这样匹配backgroundX的范围(从0到screenWidth – backgound.getWidth(null))的值。这样我们可插入点:
int backgroundX = offsetX *
(screenWidth – background.getWidth(null)) /
(screenWidth - mapWidth);
g.drawImage(background, backgroundX,0,null);
使用以上公式的条件是:
最后件事件就是:我们可以不使用图片使用背景,我们可以使用另外一个TileMap来自由绘制背景,它甚至可以与地图一样大,所以我们不必设置滚动的背景,而只需要把两个滚动速率调不一致即可,并且让前景地图是透明的,那么背景就可以看见了。如果我们需要使用背景作为tile图片,那么需要确保右边界匹配左边界,这样才会出无缝滚动效果。
有一个事件我们必须做,那么就是使用close方法实现游戏中所有的sprite对象,PowerUp类个通用的clone()方法,用来反射克隆的对象,包含它的子类。在该方法中,它选择第一个构造方法来创建新的对象的实例。注意:PowerUp的子类基本不做实际的事件,它们只是一个占位符而忆,这些子类可以实现的功能如下:
在示例游戏中有两种坏蛋,一个苍蝇和蠕虫。它们一直运行,直到碰到墙为止。在实现坏蛋之前,我们来看一下原图玩家与坏蛋都是面朝左的。所以,当我们需要让玩家向右移动时,那么玩家必须面向右,这就需要我们动态创建玩家面朝向右。
参见前面的ResourceManager类。
在游戏中每个坏蛋都四种不同的游戏行为:
这样,我们通过动画类Animation来修改sprite的不同方向和死亡的样子。玩家有三种状态:STATE_NORMAL、STATE_DYING和STATE_DEAD状态。从死亡到已死亡只有一秒钟的时间。该类向Sprite添加了如下功能:
package com.funfree.arklis.engine.tilegame.sprites;
import java.lang.reflect.Constructor;
import com.funfree.arklis.graphic.*;
/**
功能:该类是一个Sprite类,用来表示下降或者死亡的Sprite对象。它有游戏行为四个:
左移、右移、左移死亡和右移死亡
作者:技术大黍
*/
public abstract class Creature extends Sprite {
/**
从Dying到DEAD的时间:
*/
private static final int DIE_TIME = 1000;
public static final int STATE_NORMAL = 0;
public static final int STATE_DYING = 1;
public static final int STATE_DEAD = 2;
//指定游戏行为
private Animation left;
private Animation right;
private Animation deadLeft;
private Animation deadRight;
private int state;
private long stateTime;
/**
使用指定的游戏行为创建Creature对象
*/
public Creature(Animation left, Animation right,
Animation deadLeft, Animation deadRight){
super(right);
this.left = left;
this.right = right;
this.deadLeft = deadLeft;
this.deadRight = deadRight;
state = STATE_NORMAL;
}
//针对Player和InputManagerTest类修订构造方法--可能无意义:为让编译通过!(版本ver 1.0.1)
public Creature(Animation action){
super(action);
}
/**
从主怪克隆一个副本
*/
public Object clone() {
// 使用反射技术来创建一个正确的子类对象(呼叫上面的构造方法)
Constructor constructor = getClass().getConstructors()[0];
try {
return constructor.newInstance(new Object[] {
(Animation)left.clone(),
(Animation)right.clone(),
(Animation)deadLeft.clone(),
(Animation)deadRight.clone()
});
}catch (Exception ex) {
// 应该不会出现,如果出现返回null值
ex.printStackTrace();
return null;
}
}
/**
获取该Creature的最大速度
*/
public float getMaxSpeed() {
return 0;
}
/**
当Creature第一次出现在屏幕上时唤醒该creature对象,一般creature开始向左移动
*/
public void wakeUp() {
if (getState() == STATE_NORMAL && getVelocityX() == 0) {
setVelocityX(-getMaxSpeed());
}
}
/**
得到该Creature的状态:正常STATE_NORMAL、正在死亡STATE_DYING,或者已死亡STATE_DEAD。
*/
public int getState() {
return state;
}
/**
设置该Creature的状态:STATE_NORMAL,STATE_DYING或者STATE_DYING
*/
public void setState(int state) {
if (this.state != state) {
this.state = state;
stateTime = 0;
if (state == STATE_DYING) {
setVelocityX(0);
setVelocityY(0);
}
}
}
/**
检查该creature是否还活着
*/
public boolean isAlive() {
return (state == STATE_NORMAL);
}
/**
检查该creature是否正在飞行
*/
public boolean isFlying() {
return false;
}
/**
在update()方法之前呼叫,如果该creature与水平的tile发生碰撞情况的话。
*/
public void collideHorizontal() {
setVelocityX(-getVelocityX());
}
/**
在update()方法之前呼叫,如果出现垂直碰撞的话
*/
public void collideVertical() {
setVelocityY(0);
}
/**
更新该creature对象
*/
public void update(long elapsedTime) {
// 选择正确的游戏行为
Animation newAnim = anim;
if (getVelocityX() < 0) {
newAnim = left;
}else if (getVelocityX() > 0) {
newAnim = right;
}if (state == STATE_DYING && newAnim == left) {
newAnim = deadLeft;
}else if (state == STATE_DYING && newAnim == right) {
newAnim = deadRight;
}
// 更新游戏行为
if (anim != newAnim) {
anim = newAnim;
anim.start();
}else {
anim.update(elapsedTime);
}
// 变更死亡状态
stateTime += elapsedTime;
if (state == STATE_DYING && stateTime >= DIE_TIME) {
setState(STATE_DEAD);
}
}
}
该类向Creature类添加了跳(jump)的功能。大多数情况下,我们希望玩家跳,如果玩家不在地上,所以重写setY()和方法collideVertical()方法可以追踪该玩家是否在地上。如果玩家在地上,那么该玩家可以跳,另外我们可强制玩家跳(jump(true))。
package com.funfree.arklis.engine.tilegame.sprites;
import com.funfree.arklis.graphic.*;
/**
功能:这是一个玩家
作者:技术大黍
备注:根据第四章的Player来修订的类。
*/
public class Player extends Creature {
private static final float JUMP_SPEED = -.95f; //设置跳的速度
//版本ver 1.0.1添加开始
public static final int STATE_NORMAL = 0;
public static final int STATE_JUMPING = 1;
public static final float SPEED = .3F;
public static final float GRAVITY = .002F;
private boolean onGround; //标识是否在地上
private int floorY;
private int state;
//版本ver 1.0.1添加结束
public Player(Animation left, Animation right,
Animation deadLeft, Animation deadRight){
super(left, right, deadLeft, deadRight);
}
//版本ver 1.0.1添加开始
public Player(Animation animation){
super(animation);
state = STATE_NORMAL;
}
public int getState(){
return state;
}
public void setState(int state){
this.state = state;
}
/**
设置floor的位置,不管玩家是否开始跳,或者已经着陆
*/
public void setFloorY(int floorY){
this.floorY = floorY;
setY(floorY);
}
/**
让玩家产生的跳的动作
*/
public void jump(){
setVelocityY(-1);
state = STATE_JUMPING;
}
//版本ver 1.0.1添加结束
public void collideHorizontal() {
setVelocityX(0);
}
public void collideVertical() {
// 检查是否碰撞到地上
if (getVelocityY() > 0) {
onGround = true;
}
setVelocityY(0);
}
public void setY(float y) {
// 检查是否落下
if (Math.round(y) > Math.round(getY())) {
onGround = false;
}
super.setY(y);
}
public void wakeUp() {
// do nothing
}
/**
如果玩家在地下,或者强制中是true时,那么让玩家跳
*/
public void jump(boolean forceJump) {
if (onGround || forceJump) {
onGround = false;
setVelocityY(JUMP_SPEED);
}
}
//获取最大的移动速度
public float getMaxSpeed() {
return 0.5f;
}
/**
更新玩家的位置和动作,也可以设置玩家的状态为NORMAL状态,如果玩家已经着陆了
在这里可能无意义,因为需要让编译通过。版本ver 1.0.1添加方法
*/
public void update(long elapsedTime){
//设置水平位置的速率(下降效果)
if(getState() == STATE_JUMPING){
setVelocityY(getVelocityY() + GRAVITY * elapsedTime);
}
//移动玩家
super.update(elapsedTime);
//检查玩家是否着陆
if(getState() == STATE_JUMPING && getY() >= floorY){
setVelocityY(0);
setY(floorY);
setState(STATE_NORMAL);
}
}
}
游戏中我们必须确保玩家和坏蛋不能穿墙而过,但可以平台上跳。每次我们移动玩家或者坏蛋时,我们需要检查该creature是否与其它的tile发生了碰撞;如果是,那么我们必须调整相应的位置。
因为我们使用基于tile的地图,所以碰撞侦测技术比较容易实现。理论上说,一个sprite可以一次跨多个tile,并且可一次可以定位在四个不同tile上。所以, 需要不断检查当前tile是否有sprite占用,并且每个sprite将要占用的下一下tile对象。
在GameManager类的getTileCollision()方法就中完成该任务的。它检查一个sprite从原来的位置到新的位置是否跨跳了任何solid tile对象,如果是这样的情况,那么返回与sprite碰撞的tile的位置,否则返回null值。另外,该方法可以处理地图左边界或者右边界是否与create发生了碰撞,以保证creature对象在地图。
注意:该方法在处理一个sprite在多个帧之间跨跳多个tile时不是很完美,需要使用第十一章的sprite-to-environment碰撞侦测来完美,但是该代码可以处理大多数的碰撞情况了。
如果sprite往下移,然后再右移时,我们会侦测到sprite会被碰撞tile的情况。同时,上面的sprite右移也会碰撞到tile,直观看很容易解决这些问题:让两个sprite左移一点就可以了。但是怎样计算左移、不上、不下和右移偏移量?
为解决这个问题,首先我们把sprite的移支分解成两个部分:水平移动和垂直移动。所以,我们首先解决水平移动的碰撞侦测。
如果一个碰撞被侦测到,那么只需要让sprite沿原路修正一下即可,从而限制了该sprite与title的边缘。
Sprite的水平移动碰撞解决之后,那么同样对于sprite的垂直移动问题,也是一样的解决方式。代码参见GameManager类的updateCreature方法。
updateCreature()方法也可以用于没有的飞行的creature的下降碰撞处理。因为下降总是会影响creature的,但是如果creature站在title上,那么该效果不明显,因为creature与tile之间是标准的碰撞。
当一个碰撞被侦测到并且被校正之后,我们会呼叫creature的collideVertical()和collideHorizontal()方法。通过这些方法用来修改或者保存该creature的速率,防止再次发生碰撞。
对于sprite的碰撞,如果sprite是一个player(玩家),那么它与其它sprite碰撞时,比如power-up和坏蛋在此示例游戏程序中,我们忽略这些碰撞,只是调整玩家的侦测碰撞,这样可以我们看到哪个玩家的sprite边界与另外一个sprite的边界发生了重叠。
完成该功能的方法是GameManager类中的isCollision()方法。因为TileMap包含了所有的sprite列表,所以我们可以从这个列表中检查它们与否与玩家发生了碰撞。如下图
完整代码参见GameManager类中的getSpriteCollision方法。
当玩家与一个sprite发生完全碰撞时,如果该sprite是power-up,那么你可给玩家加点、播放声音,或者所有power-up支持的技能。如果sprite是一个坏蛋,那么我们可杀死坏蛋或者玩家。这时玩家落下,或者跳下时,换句话说,玩家的垂直运行是增加的:
boolean canKill = (oldY < player.getY());
当玩家行走时碰撞到坏蛋,那么玩家死亡。
现在我们已经把创建了所有的游戏的元素,比如键盘输入、声音、音乐等这些基本元素。
在控制台中运行命令:java –jar bee.jar
或者双击bee.jar即可运行程序!
完成Java 2D游戏比较简单,我们只要完成三个核心类的书写,那么就有具备一个游戏引擎的功能,剩下的就是研究、扩展我们的Player类和Creature类就中完成各种游戏中的人物、NPC的功能。
如果大家感兴趣,想要完整的代码,那么使用下面的联系方式:
QQ咨询:胖达QQ:3038443845 微信加:laojiujun 老九君
记得给大黍❤️关注+点赞+收藏+评论+转发❤️
作者:老九学堂—技术大黍
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。