从零开始Android游戏编程(第二版) 第八章 地图的设计和实现

第八章 地图的设计和实现

这本来是第十章,前面计划还有两章的内容,一是跟第四章一样,完成一个Asteroid游戏作为小结,总结一下前面讲过的Sprite的用法,并演示NPC和子弹的处理方法。但是,在写第七章的最后一个例子的时候,把本来简单的碰撞检测的例子扩充了一下,加入了NPC和子弹,基本和Asteroid的功能差不多了,所以就把原定的Asteroid砍掉了。另外一章是讲解程序的生命周期,内容相对简单,但考虑到上一章讲Sprite时已经引入了TiledLayer,如果接着讲地图应该会更连贯些,所以把生命周期向后移了一章。

那么,就先让我们看看地图的设计和实现。

如果我们需要的地图很小又很少,完全可以将整个地图画在一张图片上。但是如果地图很多,绘制和管理地图的工作就会很麻烦,这时我们就需要用到另外一种技术——图块(Tile)。所谓Tile,就是将地图中的公共元素提取出来,然后在显示的时候组合这些元素形成完整的地图,这就是本章要介绍的主要内容。

如下图是组成坦克大战地图的所有元素(16x16像素):

clip_image002

接下来让我们看一幅游戏中的场景:

clip_image003

可以看到,整个游戏场景就是由上面那些Tile构成的。这样,摆在我们面前的任务就很简单了:将地图依照Tile的大小分成若干格,将对应的Tile填到格子中。

在前面讲Sprite的时候,我们知道可以将组合在一张图片中的关键桢编号,以后就可以通过编号来使用这个桢(参看上一章桢动画的相关内容),这种方法也同样适用于Tile。而2D地图很容易让我们想到二维数组。也就是说我们可以将Tile的编号放到二维数组中,这样我们就可以通过历遍数组元素,像显示Sprite那样将整张地图一块一块的显示出来。

以上面的那张游戏截图为例,一共13行13列,我们可以定义一个13x13的二维数组(因为地图上有空白区域,所以我们将Tile的编号从1开始,用0表示空白)。

让我们找到GameView_Old.java,增加成员变量map[][]:

int[][] map = {

{0,0,0,2,0,0,0,2,0,0,0,0,0},

{0,1,0,2,0,0,0,1,0,1,0,1,0},

{0,1,0,0,0,0,1,1,0,1,2,1,0},

{0,0,0,1,0,0,0,0,0,2,0,0,0},

{3,0,0,1,0,0,2,0,0,1,3,1,2},

{3,3,0,0,0,1,0,0,2,0,3,0,0},

{0,1,1,1,3,3,3,2,0,0,3,1,0},

{0,0,0,2,3,1,0,1,0,1,0,1,0},

{2,1,0,2,0,1,0,1,0,0,0,1,0},

{0,1,0,1,0,1,1,1,0,1,2,1,0},

{0,1,0,1,0,1,1,1,0,0,0,0,0},

{0,1,0,0,0,1,1,1,0,1,0,1,0},

{0,1,0,1,0,1,6,1,0,1,1,1,0},

};

在资源中增加tile.png

clip_image005

在构造函数中初始化bitmap对象

res = context.getResources();

bmp = BitmapFactory.decodeResource(res, R.drawable.tile);

在onDraw中绘图

//用来显示图块的Rect对象

Rect src = new Rect(0, 0, 0, 16);

Rect dst = new Rect();

for(int i=0; i<13; i++) {

for(int j=0; j<13; j++) {

//根据Tile的编号得到对应的位置

src.left = (map[i][j]-1) * 16;

src.right = src.left + 16;

//根据地图上的编号计算对应的屏幕位置

dst.left = j * 16;

dst.right = dst.left + 16;

dst.top = i * 16;

dst.bottom = dst.top + 16;

canvas.drawBitmap(bmp, src, dst, paint);

}

}

最后不要忘了修改Main.java中的setContentView

GameView_Old gameView;

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

gameView = new GameView_Old(this);

setContentView(gameView);

}

好了,运行一下程序看看结果:

clip_image007

clip_image003[1]

对比一下原图,除了基地四周的砖块之外两者并无二致,可以说我们的Tile地图实践基本成功。

现在我们知道了使用Tile显示地图的原理,实际上,我们不需要每次都很麻烦的写那么多代码,还记得前面说过的TiledLayer么?其中早已封装了上述操作。不仅如此,TiledLayer还能显示动态的地图呢。下面就让我们看一看TiledLayer的基本用法。其实,它与Sprite的用法非常相似(我们需要将TiledLayer.java加入到项目中,请使用本章附带程序的TiledLayer.java文件,上一章的TiledLayer并不能正确工作):

这次让我们使用GameView,把刚刚的数组map拷贝到GameView中,并声明一个TiledLayer类型变量

//背景

TiledLayer backGround;

在构造函数中初始化TiledLayer

// 背景图

backGround = new TiledLayer(13, 13, BitmapFactory.decodeResource(res,

R.drawable.tile), 16, 16);

TiledLayer的构造函数有5个参数,分别是地图的行列数(是以Tile为单位的),包含Tile的bitmap对象,Tile的宽度和高度。

通过setCell方法将定义在数组中的Tile编号传递给TiledLayer。

for(int i=0; i<13; i++) {

for(int j=0; j<13; j++) {

backGround.setCell(i, j, map[i][j]);

}

}

最后,只需要在run函数中调用paint方法,就可以将TiledLayer显示出来了。当然,你可以像控制Sprite那样控制TiledLayer显示的位置。

backGround.paint(c);

让我们看一下运行的效果

clip_image009

下面让我们来学习如何实现动态地图。所谓动态地图跟前面讲到的桢动画是一个道理,就是循环显示几个关键桢。让我们看一下前面Tiles的图片

clip_image010

我们会发现有两张水域的图片,这就是为动态地图准备的,组合起来之后应该会有如下的效果:

clip_image011

那么,我们如何在TiledLayer中实现动态地图呢?TiledLayer为我们准备了这样几个函数:

createAnimatedTile:创建动态图块。很多人会迷惑于这个函数的名字,说是创建动态图块,可是创建在哪儿啊?创建出来怎么用呢?只有天知道。其实,这个函数的主要功能也就是为动态图块分配了一个存储结构。你不调用它还会报错,调用了其实也没什么用。createAnimatedTile返回一个动态图块的编号,从-1开始依次递减,第一次调用返回-1,这样就分配了一个编号为-1的动态图块。第二次调用会返回-2,依次类推。这个返回值一般没有用,因为我们做地图的时候肯定已经确定了动态图块的位置,这个编号早就写到了数组中了。以后你就可以通过这个编号来控制相应的图块。函数有一个参数,指定一个Tile的编号,动态图块最初显示的就是这个Tile。而正是通过改变这个Tile来实现动画的。请看下面代码:

backGround.createAnimatedTile(4);

我们初始化了一个动态图块,编号是-1,参数4表示当前显示Tile序列图中的第四个Tile,就是第一张水域的图片。

setAnimatedTile:动态图块的内容就是使用这个函数改变的。函数的第一个参数是动态图块的编号,如刚刚的-1。第二个参数是Tile的编号。

说到这里,大家应该清楚动态地图的用法了吧:

首先在地图数组中确定需要显示动态图块的位置,填入相应的编号,例如我们将上一张地图的第三行增加三块水域

int[][] map = {

{0,0,0,2,0,0,0,2,0,0,0,0,0},

{0,1,0,2,0,0,0,1,0,1,0,1,0},

{0,1,-1,-1,-1,0,1,1,0,1,2,1,0},

{0,0,0,1,0,0,0,0,0,2,0,0,0},

{3,0,0,1,0,0,2,0,0,1,3,1,2},

{3,3,0,0,0,1,0,0,2,0,3,0,0},

{0,1,1,1,3,3,3,2,0,0,3,1,0},

{0,0,0,2,3,1,0,1,0,1,0,1,0},

{2,1,0,2,0,1,0,1,0,0,0,1,0},

{0,1,0,1,0,1,1,1,0,1,2,1,0},

{0,1,0,1,0,1,1,1,0,0,0,0,0},

{0,1,0,0,0,1,1,1,0,1,0,1,0},

{0,1,0,1,0,1,6,1,0,1,1,1,0},

};

然后在GameView的构造函数中初始化TiledLayer,除了在setCell之前调用createAnimatedTile之外,没有其他区别。

最后就是在run函数中调用setAnimatedTile,不断地改变图块了

if(backGround.getAnimatedTile(-1) == 4) {

backGround.setAnimatedTile(-1, 5);

} else {

backGround.setAnimatedTile(-1, 4);

}

backGround.paint(c);

来让我们看一下运行的效果

clip_image012

其实笔者觉得TiledLayer完全也可以像Sprite那样使用桢序列和nextFrame来实现动态效果,似乎更易用一些,有兴趣的读者可以自己修改TiledLayer实现这个功能。

到这里,我们已经掌握了显示地图的方法,但是,这个地图还不能真正运用到我们的游戏中。读者肯定也看到了,在TiledLayer的第一个例子中,我们的坦克可以穿墙而过,显然,这个地图还缺少最基本的功能——阻挡。

有一种简单的方案可以实现阻挡,让我们看一下Sprite类,其中有一个方法:

public final boolean collidesWith(TiledLayer t, boolean pixelLevel)

检测Sprite与TiledLayer的碰撞。这种检测是以Tile为单位的,当与Sprite重合的Tiles编号不为0时函数返回true,否则返回false。

下面让我们做一个小例子测试一下:

打开GameView_Old,增加一个Sprite类型的成员变量

// 主角

Sprite player;

在构造函数中初始化player

// 初始化主角

player = new Sprite(BitmapFactory.decodeResource(res,

R.drawable.player1), 16, 16);

player.setFrameSequence(new int[] { 0, 1 });

在onDraw中绘制player

player.paint(c);

backGround.paint(c);

在onKeyDown中控制Sprite的运动

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

// TODO Auto-generated method stub

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP:

x = player.getX();

y = player.getY();

player.move(0, -16);

if(!player.collidesWith(backGround, false)) {

y -= 16;

}

player.setTransform(Sprite.TRANS_NONE);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

x = player.getX();

y = player.getY();

player.move(0, 16);

if(!player.collidesWith(backGround, false)) {

y += 16;

}

player.setTransform(Sprite.TRANS_ROT180);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_LEFT:

x = player.getX();

y = player.getY();

player.move(-16, 0);

if(!player.collidesWith(backGround, false)) {

x -= 16;

}

player.setTransform(Sprite.TRANS_ROT270);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT:

x = player.getX();

y = player.getY();

player.move(16, 0);

if(!player.collidesWith(backGround, false)) {

x += 16;

}

player.setTransform(Sprite.TRANS_ROT90);

player.setPosition(x, y);

break;

}

postInvalidate(); // 通知系统刷新屏幕

return super.onKeyDown(keyCode, event);

}

可以看到,坦克只能在空白区域运动,这回不能上墙了。

但是这种方法还是比较粗糙的,很多时候不能实现我们的目的,比如坦克大战中,水和砖头是坦克不能通过的,但是掩体是可以通过的,还有子弹可以通过水域,这时候还是检测Tile的编号来的准确些。就是说,我们事先确定好那些编号的Tile可以通过,哪些不能。然后模仿collidesWith方法根据Tank的位置取得它下一步要到达的Tile的编号,并判断坦克是否被阻挡。

让我们用这个方案重写onKeyDown方法(为了简化教程,我们假设每次player和tile都是完全重合的):

首先我们先定义一个函数用来判断Tank是否可以通过

// 判断坦克是否可以通过

private boolean tankPass(int x, int y) {

// 不超过地图范围

if (x < 0 || x > 12 * 16 || y < 0 || y > 12 * 16) {

return false;

}

int tid = map[y / 16][x / 16];

if (tid == 1 || tid == 2 || tid == -1)

return false;

return true;

}

然后在onKeyDown中运用新的方案

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

// TODO Auto-generated method stub

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP:

x = player.getX();

y = player.getY();

player.move(0, -16);

if (tankPass(player.getX(), player.getY())) {

y -= 16;

}

player.setTransform(Sprite.TRANS_NONE);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

x = player.getX();

y = player.getY();

player.move(0, 16);

if (tankPass(player.getX(), player.getY())) {

y += 16;

}

player.setTransform(Sprite.TRANS_ROT180);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_LEFT:

x = player.getX();

y = player.getY();

player.move(-16, 0);

if (tankPass(player.getX(), player.getY())) {

x -= 16;

}

player.setTransform(Sprite.TRANS_ROT270);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT:

x = player.getX();

y = player.getY();

player.move(16, 0);

if (tankPass(player.getX(), player.getY())) {

x += 16;

}

player.setTransform(Sprite.TRANS_ROT90);

player.setPosition(x, y);

break;

}

postInvalidate(); // 通知系统刷新屏幕

return super.onKeyDown(keyCode, event);

}

现在让我们运行一下看看吧,这回终于能够达到了我们想要的效果,我们的坦克正藏在掩体下面,并且它不能通过墙和水域。

clip_image014

到此为止,关于地图的内容就讲解完毕了。这些内容并不复杂,首先介绍了使用Tile组成地图的原理,然后介绍了TiledLayer已经动态地图,最后演示了阻挡的实现方法。本章的大部分例子使用了GameView_Old,并没有使用游戏循环,也没有涉及到地图的平滑滚动,在以后需要的时候会补充这部分知识。

本章示例程序http://u.115.com/file/f12827d8db

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值