编写推箱子游戏程序(第五步)——指挥搬运工走动1
叶常春(iamdouble@163.com)
一、本文目标
上一篇文章结尾,实现了绘制游戏区域和搬运工的功能。这一功能是玩家点击开始游戏
按钮并选择关卡后触发的,如图 1 所示。下文中,我们把选择关卡后触发的界面叫做游戏界
面。
图 1 玩家点击开始游戏按钮并选择关卡后触发游戏界面
本文在游戏界面实现玩家指挥搬运工走动的功能,描述如下:
1. 当玩家手指触摸搬运工的上方(下方、左侧、右侧)单元格,则搬运工将走到上方
1本文遵循 Apache License 2.0 协议。你可以修改和再发布本文档,但须保留原著者和采用 Apache License 2.0
协议。
(下方、左侧、右侧)单元格。如图 2 所示。
2. 不允许出边界。
3. 玩家触摸到搬运工上方(下方、左侧、右侧)单元格之外的区域,则视为无效指令,
搬运工不走动。
图 2 游戏界面上,搬运工走动功能的效果图
你将学到的知识内容有:
1. 利用回调函数 onTouchEvent 处理屏幕触摸事件。
2. 利用 invalidate 或 postInvalidate 方法刷新视图。
二、实现思路和步骤
2.1 实现思路
在游戏界面,玩家触摸手机屏幕的时候,将引发 Android 系统执行 onTouchEvent 回调函
数,在此回调函数内执行以下工作:
1. 判断触摸位置是否落在搬运工的上方(下方、左侧、右侧)单元格。
2. 若是,则修改搬运工的位置到上方(下方、左侧、右侧)单元格;要求重新绘制画
面。
游戏画面重新绘制后,玩家将看到搬运工走动了。
那么,如何判断触摸位置是否落在搬运工的上方(下方、左侧、右侧)单元格呢?以判
断是否落在上方单元格为例,答案是:
1. 记住搬运工当前所处的单元格(记作(mManRow, mManColumn),也就是说搬运工处
在 mManRow 行 mManColumn 列)。
2. 我们知道,每一单元格是一个正方形,记它的宽度为 mCellWidth。搬运工上方单元
格的矩形区域 above 的左上角是:
1) 左端:mManColumn * mCellWidth
2) 上端:(mManRow – 1) * mCellWidth
上方单元格的矩形区域 above 的右下角是:
3) 右端:(mManColumn + 1) * mCellWidth
4) 下端:mManRow * mCellWidth
3. 获取通过 onTouchEvent 回调函数的参数传入的触摸位置(touch_x, touch_y)。
4. 如果触摸位置(touch_x, touch_y)落在上方单元格 above 内,则得出“是”的结论,
否则得出“否”的结论。
类似地,我们可以判断触摸位置是否落在搬运工下方(below)、左侧(left)、右侧(right)
的单元格内。
2.2 实现步骤
我们遵循以下步骤来实现游戏界面上玩家指挥搬运工走动功能:
1. 实现搬运工向下走动。
2. 实现搬运工向右走动。
3. 禁止搬运工走出边界。
4. 搬运工向上、向左走动功能留作作业。
三、实现搬运工向下走动功能
往下阅读前,在你的脑子里再过一遍上一章讲的实现思路。你理顺了没有?
3.1 onTouchEvent 方法的实现
上面提到,玩家在游戏界面触摸手机屏幕的时候,将引发 Android 系统执行 onTouchEvent
回调函数。这一回调函数是 View 类的方法。我们需要在 GameView 类内添加此回调函数(做
法参阅“本系列文章《Android Studio 用法》的‘重载/覆盖(Override)父类方法’一节”)。
代码如表 1 所示。
表 1 GameView 类的 onTouchEvent 回调函数
com.yescorp.moveboxgame.GameView.java
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_DOWN)
return true;
int touch_x = (int) event.getX(); //触摸点的x坐标
int touch_y = (int) event.getY(); //触摸点的y坐标
if (touch_blow_to_man(touch_x, touch_y, mManRow, mManColumn))
mManRow++;
postInvalidate();
1
2
3
4
5
6
7
8
9
return true;
10
11
}
对表 1 的代码,说明如下:
第 1 行,onTouchEvent 返回 boolean 类型值,即 true 或 false,返回 true 表明触摸事件
已经处理完毕,返回 false 表明触摸事件需要进一步处理。在这里,不存在进一步处理
的情形,故返回 true 就可以了。第 3 行和第 10 行就是这么做的。
第 1 行中,MotionEvent 类型的 event 参数是一类事件对象,它内部包含动作信息和触
摸点的坐标信息。
第 2 行,是判断触摸动作是否是按下(ACTION_DOWN),若不是,则忽略这一事件,不
处理这一事件。我们用手指触摸屏幕,滑动手指,直至离开屏幕,会产生三类动作:按
下(ACTION_DOWN)、滑动(ACTION_MOVE)和弹起(ACTION_UP)。在这里,我们不
处理后两类动作。关于触摸事件,参阅“参考文献(1)”。
第 2,3 行的作用就是,忽略动作类型为滑动(ACTION_MOVE)和弹起(ACTION_UP)的
触摸事件。这样,第 4~10 行代码针对的是动作类型为按下(ACTION_DOWN)的触摸事
件。
第 5,6 行得到触摸点的 x, y 坐标。(int)是类型强制转换。由于 event.getX()返回的是 float
型的值,所以需要从 float 转为 int。
第 7 行 是 判 断 触 摸 点 是 否 落 在 搬 运 工 的 下 方 单 元 格 。touch_blow_to_man(touch_x,
touch_y, mManRow, mManColumn)的作用是判断触摸点(touch_x, touch_y)是否落在搬
运工的下方单元格。touch_blow_to_man 函数的代码实现在后面讲述。变量 mManRow
和 mManColumn 的作用是记住搬运工当前所处的单元格的行号和列号。行号和列号都
从 0 开始计数。row 的中文意思是行,column 的中文意思是列。变量 mManRow 和
mManColumn 是 GameView 类的成员变量,定义如下:
private int mManRow = 0;
private int mManColumn = 0;
这两个变量都初始化为 0,这使得选择关卡后进入游戏界面时,搬运工落在左上角单元
格。
第 8 行在第 7 行的判断成立的情况下执行,作用是使搬运工向下走一步——行号增 1,
列号不变。
第 9 行 postInvalidate 方法的作用是要求使游戏界面(即 GameView 视图)失效。这将
引发 GameView 的 onDraw 方法的执行。我们知道,绘制画面的操作只能放在 onDraw
方法内。那么,在 onDraw 方法外要求重新绘制画面,该怎么做呢?这里的情形是,在
onTouchEvent 方法内,搬运工向下走了一步,要求重新绘制画面呈现搬运工的新位置,
该怎么做呢?答案是,调用 invalidate 方法或 postInvalidate 方法。关于这两个方法,参
阅参考文献(2)。
第 9 行执行后,将引发 GameView 的 onDraw 方法执行一次。onDraw 方法内,将获取搬
运工的新位置,而后绘制搬运工。onDraw 方法的代码实现在后面说明。
第 10 行,返回 true 值表明触摸事件处理完毕。
参考文献:
(1) Android 中 TouchEvent 触摸事件机制。
http://www.open-open.com/lib/view/open1470468705188.html。
(2) Android 笔记:invalidate()和 postInvalidate() 的区别及使用
http://blog.csdn.net/mars2639/article/details/6650876 。
3.2 touch_below_to_man 方法的实现
接上一节,touch_below_to_man(touch_x, touch_y, mManRow, mManColumn)方法的作用
是判断触摸点(touch_x, touch_y)是否落在搬运工的下方单元格。搬运工目前处在(mManRow,
mManColumn)单元格内。这一方法的代码如表 2 所示。
表 2 GameView 类的 touch_blow_to_man 方法
com.yescorp.moveboxgame.GameView.java
private boolean touch_blow_to_man(int touch_x, int touch_y, int manRow, int manColumn)
{
int belowRow = manRow + 1;
Rect belowRect = getRect(belowRow, manColumn);
return belowRect.contains(touch_x, touch_y);
}
1
2
3
4
5
对于表 2 中的代码,说明如下:
第 1 行,touch_below_to_man 方法返回 boolean 类型值。返回 true 表明触摸点落在搬
运工的下方单元格。返回 false 表明没有落在搬运工的下方单元格。前两个参数是触摸
点(touch_x, touch_y)。后两个参数是搬运工目前所处的单元格。
第 2 行,求得下方单元格的行号。
第 3 行,调用 getRect 方法得到单元格的矩形区域。getRect 方法的定义在下面讲述。矩
形区域用 Rect 类型的 belowRect 保存。Rect 类是 Android SDK 预定义类,使用方法参阅
本节参考文献(1)。
第 4 行是调用 Rect 类的 contains(x, y)方法来得出坐标点(x, y)是否落在矩形区域内。
belowRect.contains(touch_x, touch_y) 是 得 出 坐 标 点 (touch_x, touch_y) 是 否 落 在
belowRect 内,若是,则返回 true,否则返回 false。
参考文献:
(1) android.graphics.Rect 类的详解
http://blog.csdn.net/huangxiaominglipeng/article/details/21597575 。
3.3 getRect 方法的实现
接上一节,getRect 方法的作用是得到单元格(row, column)的矩形区域。代码如表 3 所示。
表 3 GameView 类的 getRect 方法
com.yescorp.moveboxgame.GameView.java
private Rect getRect(int row, int column) {
int left = (int)(column * mCellWidth);
1
2
int top = (int) (row * mCellWidth);
int right = (int)((column + 1) * mCellWidth);
int bottom = (int)((row + 1) * mCellWidth);
return new Rect(left, top, right, bottom);
3
4
5
6
7
}
对表 3 的代码,说明如下:
第 1 行是 getRect 方法的签名。它返回 Rect 类型的值,即一个矩形区域。参数 row 和
column 分别是单元格的行号和列号,都是从 0 开始编号。这一函数的作用就是得出单
元格在屏幕中的矩形区域。
第 2 行,left 变量存储单元格左边界的 x 坐标值。列号 column 乘以单元格宽度,就是
左边界的 x 坐标值。这一坐标值是一个浮点数,而 left 是整型变量,故使用(int)进行类
型转换。
第 3,4,5 行与第 2 行类似,分别得到单元格上边界的 y 坐标值(存入 top 变量)、右
边界的 x 坐标值(存入 right 变量),下边界的 y 坐标值(存入 bottom 变量)。
第 6 行,是生成一个覆盖单元格的矩形区域对象,并返回。
3.4 onDraw 方法的实现
上面提到,onTouchEvent 方法内会调用 postInvalidate()方法,引发 onDraw 方法执行一
次,作用是刷新 GameView 视图。
为更新搬运工的位置, onDraw 方法的代码做了一处修改:用表 4 中第 19 行代码替换
第 18 行代码。对这一修改,说明如下。
第 18 行是修改前的代码。这行代码的作用是在屏幕左上角绘制搬运工。这一行代
码被删除。
第 19 行代码是根据搬运工所处的单元格(mManRow, mManColumn),求得覆盖该
单元格的矩形区域,存入 destRect。getRect 方法的说明见上一节。
第 20 行,将搬运工图片绘制到 destRect 对应的区域。
表 4 GameView 类的 onDraw 方法
com.yescorp.moveboxgame.GameView.java
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//背景色
Paint background = new Paint();
background.setColor(getResources().getColor(R.color.background));
canvas.drawRect(0, 0, getWidth(), getHeight(), background);
//绘制游戏区域
Paint linePaint = new Paint();
linePaint.setColor(Color.BLACK);
for (int r = 0; r <= CELL_NUM_PER_LINE; r++)
canvas.drawLine(0, r * mCellWidth, getWidth(), r * mCellWidth, linePaint);
1
2
3
4
5
6
7
8
9
10
11
12
for (int c = 0; c <= CELL_NUM_PER_LINE; c++)
canvas.drawLine(c * mCellWidth, 0, c * mCellWidth, CELL_NUM_PER_LINE* mCellWidth, linePaint);
//绘制搬运工
Rect srcRect = new Rect(0, 0, GameBitmaps.ManBitmap.getWidth(), GameBitmaps.ManBitmap.getHeight());
RectdestRect=newRect(0,0,(int)mCellWidth,(int)mCellWidth);
Rect destRect = getRect(mManRow, mManColumn);
canvas.drawBitmap(GameBitmaps.ManBitmap, srcRect, destRect, null);
13
14
15
16
17
18
19
20
21
}
至此,实现了搬运工向下走动功能。在手机模拟器上或者你自己的真机上跑一跑看看吧。
在手机模拟器上,要用“鼠标点击”来模拟手指触摸。
3.5 GameBitmaps 类
表 4 中使用了 GameBitmaps.ManBitmap ,见第 17,20 行。GameBitmaps 是一个辅助类,功能
是加载和管理图片资源。GameBitmaps 类放在 GameBitmaps.java 文件内,代码如下:
public class GameBitmaps {
public static Bitmap ManBitmap= null;
//需要为每一幅图片安排一个 static 变量
public static void loadGameBitmaps(Resources res){
if (ManBitmap== null)
//如果为 null 加载图片;否则说明已经加载过了。
ManBitmap= BitmapFactory.decodeResource(res, R.drawable.eggman_48x48);
}
//释放图片对象占据的内存
public static void releaseGameBitmaps(){
if (ManBitmap!= null) {
ManBitmap.recycle();
ManBitmap= null;
}
}
}
上面的代码只是处理一幅图片。如果有更多图片,则需要:
1. 为每一幅图片定义一个静态(static)变量。
2. 在 loadGameBitmaps 方法内加载每一幅图片。
3. 在 releaseGameBitmaps 方法内释放每一幅图片。
GameView 类的构造函数要调用加载图片的 loadGameBitmaps 方法。如下:
public class GameView extends View{
private float mCellWidth;
public static final int CELL_NUM_PER_LINE= 12;
private Bitmap ManBitmap = null;
//改成使用 GameBitmaps 类的图片对象
public GameView(Context context) {
super(context);
ManBitmap= BitmapFactory.decodeResource(res, R.drawable.eggman_48x48);
GameBitmaps.loadGameBitmaps(getResources());
//加载图片。getResources()获取资源管理器对象。
}
……
}
//省略了若干代码
四、实现搬运工向右走动功能
往下阅读前,在你的脑子里再过一遍第二章讲的实现思路。你理顺了没有?
要实现搬运工向右走动功能,做法是在上文表 1 代码的第 8 行之后,增加以下两行代码:
if (touch_right_to_man(touch_x, touch_y, mManRow, mManColumn))
//按在右侧
mManColumn++;
这两行代码的作用是,判断玩家的触摸点(touch_x, touch_y)是否在搬运工的右侧,若是,则
向右侧走动一步。列号加 1(mManColumn++),正是向右侧走动一步。
touch_right_to_man 方法的定义如下:
private boolean touch_right_to_man(int touch_x, int touch_y, int manRow, int manColumn) {
int rightColumn = manColumn + 1;
Rect rightRect = getRect(manRow, rightColumn);
return rightRect.contains(touch_x, touch_y);
//右侧单元格列号
//求右侧单元格的矩形区域
//落在右侧单元格内吗?
}
对照上一章 3.2 节“touch_below_to_man 方法的实现”,我们很容易明白 touch_right_to_man
方法的代码的作用。这里不再赘述。
对于 getRect 方法和 onDraw 方法,相对于第三章,这里无需做任何修改。
至此,实现了搬运工向右走动功能。在手机模拟器上或者你自己的真机上跑一跑看看吧。
在手机模拟器上,要用“鼠标点击”来模拟手指触摸。
五、禁止搬运工走出边界
前面实现了搬运工向下、向右走动的功能,相信你很快能实现右上、向左走动功能。接
下来,我们来实现禁止搬运工走出游戏区域边界。
走出游戏区域边界的效果如图 3 所示。你看搬运工都跑哪里去了……呃,出问题了。