snake与LunarLander源代码分析
这个一个每个人都喜欢的简单的小游戏
Snake是游戏的实现类,通过控制小蛇在花园中游走寻找苹果,注意,每吃掉一个苹果,小蛇身体不但会变的更长,还会移动的更敏捷,一旦撞上四周的墙或是碰到自己就会结束这次游戏。
代码结构分析:
Snake : 主游戏窗口
SnakeView : 游戏视图类,是实现游戏的主体类
TileView : 一个处理图片或其它
Coordinate :这是一个包括两个参数,用于记录X轴和Y轴简单类,其中包括一个比较函数.
RefshHandler :用于更新视图
Snake
这个类是游戏的主游戏窗口,是框架容器,
- 游戏的开始:oncreate此外的亮点是:setContentView(R.layout.snake_layout);设置窗口的布局文件,这里Android123给大家说明的是,这里 的snake_layout使用了自定义资源标签的方式,大家注意学习:这里我们可以看到来自SnakeView这个派生类的名称,由于Android内 部的R.资源不包含SnakeView类,所以我们必须写清楚Package,比如 com.exmple.android.snake.SnakeView 然后和其他控件使用一样,都是一个id然后宽度、高度、以及自定义的标签tileSize(尾巴长度),如下:
<com.example.android.snake.SnakeView
android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tileSize="12"
/>
- onPause:关于这点,大家可以参考下在我blog中关于active生命周期http://xusaomaiss.javaeye.com/admin/blogs/379826
在玩游戏过程中,如果有来电或是其它事件中断,这时应该把当前状态保存。以便返回时,还可以继续玩游戏。这就使用onSaveInstanceState实现保存当前状态。
TileView
注:此部分解析来自: Android示例程序Snake贪食蛇代码分析(三)
TileView,从名称上不难看出这是一个方砖类,就是生成一个方块。 TileView使用了Android平台的显示基类View,View类是直接从java.lang.Object派生出来的,是各种控件比如 TextView、EditView的基类,当然包括我们的窗口Activity类,这些在SDK文档中都说的比较清楚。
这里定义了 5个int型全局的变量,分别是方砖的数量mTileSize;方砖水平x防线的数量mXTileCount;以及竖直y方向上的方砖数量 mYTileCount,下面是一个相对偏移位置mXOffset和mYOffset;这里主让要大家了解如何自定义View在 Android开发中,在一个View类中主要是重写onSizeChanged方法来控制改变部分,以及onDraw实现画布的修改,实现的简写如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {}
@Override
public void onDraw(Canvas canvas) { super.onDraw(canvas);}
我们自定义的TileView类需要自己添加一个构造方法,根据需要,我们还重载了一种包含样式的方法,这里大家可以多看下Gallery控件的实现,就好理解了,下面是基本框架。
public TileView(Context context, AttributeSet attrs, int defStyle)
{super(context, attrs, defStyle);}
public TileView(Context context, AttributeSet attrs) { super(context, attrs);}
在贪食蛇游戏中我们知道Snake是移动的,所以加入了一个清除显示的clearTiles方法,通过一个二维数组保存一个gird网格型的运动轨迹,下一次我们将会讲解android贪食蛇的游戏逻辑和完整的关联拼接实现。
SnakeView
在这个类中实现的游戏的实体,从游戏需求的角色,这个游戏包括了如下方面:
1. 随机产生小苹果,apples这里是复数,当然是是大于1个苹果,所以代码中产生了两个苹果。
2. 游戏状态管理
3. 画蛇,view的更新
4. 吃掉苹果后小蛇状态的变化
5. 画围墙
如果实现吃掉苹果小蛇速度变快?
关键是:mMoveDelay这个变量,以下是涉及到这个变量的函数,
每次吃掉苹果后,就会updateSnake一下,里面就把时间处理了:mMoveDelay *= 0.9;
小蛇其实就是一个数组,google的代码就是好注释写的清楚:
/**
* mSnakeTrail: a list of Coordinates that make up the snake's body
* mAppleList: the secret location of the juicy apples the snake craves.
*/
private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();
private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();
mSnakeTrail:一个由Coordinates列表组织的蛇身.
mAppleList:存放鲜美多汁的苹果列表
通过这个数组画出小蛇不难,问题是如何判断游戏是否结束?
问题是如何判断游戏的状态
所有以下的代码来自updateSnake
1. 吃了苹果
// Look for apples
int applecount = mAppleList.size();
for (int appleindex = 0; appleindex < applecount; appleindex++) {
Coordinate c = mAppleList.get(appleindex);
if (c.equals(newHead)) {
mAppleList.remove(c);
addRandomApple();
mScore++;
mMoveDelay *= 0.9;
growSnake = true;
}
}
2. 碰到了自己
// Look for collisions with itself
int snakelength = mSnakeTrail.size();
for (int snakeindex = 0; snakeindex < snakelength; snakeindex++) {
Coordinate c = mSnakeTrail.get(snakeindex);
if (c.equals(newHead)) {
setMode(LOSE);
return;
}
}
3.碰到墙了
// Collision detection
// For now we have a 1-square wall around the entire arena
if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2)
|| (newHead.y > mYTileCount - 2)) {
setMode(LOSE);
return;
}
源代码分析
Snake状态分析:
在snakeView中定义了snake游戏的几种状态:
private int mMode = READY;
public static final int PAUSE = 0; //暂定
public static final int READY = 1; //准备好了
public static final int RUNNING = 2;//正在运行
public static final int LOSE = 3; //结束,输了游戏
各种游戏状态
rady running
pausedlose
以上状态是通过:void setMode(int newMode)函数实现。
如何实现画出小方块:
参看:http://yuefeng.javaeye.com/blog/206706
public class DrawView extends View {
private final int mTileSize = 12;
private final String TAG="DEMO";
private Paint pa = new Paint();
private Bitmap mTileArray;
void loadImage(){
Resources r = this.getContext().getResources();
Drawable tile = r.getDrawable(R.drawable.redstar);
Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
tile.setBounds(0, 0, mTileSize, mTileSize);
tile.draw(canvas);
mTileArray = bitmap;
}
public DrawView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
loadImage();
x = 10;
y = 10;
Log.i(TAG, "DrawView 2");
}
//如果没有这段代码,大家可以试一下,改用上面的代码,程序能否通过。
public DrawView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
loadImage();
Log.i(TAG, "DrawView 3");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "onDraw 1");
canvas.drawBitmap(mTileArray, x, y, pa);
}
}
通过上面的文章可以画出小方块,但注意到SnakeView一共有两构造函数,那个函数才真正起作用呢?
l public SnakeView(Context context, AttributeSet attrs)
l public SnakeView(Context context, AttributeSet attrs, int defStyle)
通过加log的方式,判断是第一个构造函数起作用。
在第一个构造函数上方有一段注释:通过XML文件构造出SnakeView
* Constructs a SnakeView based on inflation from XML
如果不使用这个构造函数,将会造成错误,可以试一下,看一下结果是怎样!本人得到如下的错误提示:
05-21 14:13:26.079: ERROR/AndroidRuntime(711): Caused by: java.lang.NoSuchMethodException: DrawView
按键处理:
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
Log.i(TAG, "KEYCODE_DPAD_UP");
}
return super.onKeyDown(keyCode, event);
}
如何让我们的小方块动起来?
实现小方块动起来的秘密在于view的public void invalidate ()
大家可以参看SDK文档中关于View中Drawing中的一小段话
To force a view to draw, call invalidate().//为了让view重画,可以调用invalidate函数
方法:
在DrawView类中添加两个成员:
private int x,y;
同时实现get,set方法,
在构造函数中添加他们的初始值,
修改onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "onDraw 1");
canvas.drawBitmap(mTileArray, x, y, pa);
}
4.修改onKeyDown函数
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
Log.i(TAG, "KEYCODE_DPAD_UP");
dv.setX(dv.getX()+10);
dv.invalidate();
}
return super.onKeyDown(keyCode, event);
}
最后运行结果如下图:
附:网络上关于snake分析的三篇文章
上一次我们大概讲解了下Android SDK中的演示程序Snake游戏的主框架,今天我看来看下实现的基础类TileView,从名称上不难看出这是一个方砖类,就是生成一个方块。 TileView使用了Android平台的显示基类View,View类是直接从java.lang.Object派生出来的,是各种控件比如 TextView、EditView的基类,当然包括我们的窗口Activity类,这些在SDK文档中都说的比较清楚。
这里定义了 5个int型全局的变量,分别是方砖的数量mTileSize;方砖水平x防线的数量mXTileCount;以及竖直y方向上的方砖数量 mYTileCount,下面是一个相对偏移位置mXOffset和mYOffset;这里android123主让要大家了解如何自定义View在 Android开发中,在一个View类中主要是重写onSizeChanged方法来控制改变部分,以及onDraw实现画布的修改,实现的简写如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {}
@Override
public void onDraw(Canvas canvas) { super.onDraw(canvas);}
我们自定义的TileView类需要自己添加一个构造方法,根据需要,我们还重载了一种包含样式的方法,这里大家可以多看下Gallery控件的实现,就好理解了,下面是基本框架。
public TileView(Context context, AttributeSet attrs, int defStyle)
{super(context, attrs, defStyle);}
public TileView(Context context, AttributeSet attrs) { super(context, attrs);}
在贪食蛇游戏中我们知道Snake是移动的,所以加入了一个清除显示的clearTiles方法,通过一个二维数组保存一个gird网格型的运动轨迹,下一次我们将会讲解android贪食蛇的游戏逻辑和完整的关联拼接实现。
今天我们分析下最复杂的SnakeView的设计,它是派生于TileView方砖类,TileView构建是基于Android直接的显示类View,如果不明白的可以查看Android示例程序Snake贪食蛇代码分析(二)一文有关TileView类的实现, 首先我们看到整个游戏分 READY、PAUSE 、RUNNING 、LOSE四种mMode状态模式,分别对应准备、暂停、运行中、结束(死亡),毕竟贪食蛇没有胜利这个结果。
整个Snake的运行分4个方向,NORTH、SOUTH 、EAST、WEST分别对应了北、南、东、西四个方向,其中变量mDirection对应当 前的方向,而mNextDirection对应下个运行时的位置。这里星星分3种,使用的是一个Drawable图片,分RED_STAR、 YELLOW_STAR和GREEN_STAR三种颜色,游戏的星星出现位置由Random随机数生成器来决定,这里Random一般和Timer系统时 钟来随机生成更真实一些,通过一个Handler对象来控制画面的更新,使用了this.update();和this.invalidate();这两 个本地方法,Update和invaildate均为android.view.View类的本地方法。这里资源的使用通过Resources r = this.getContext().getResources();获取了r对象的实例,通过 r.getDrawable(R.drawable.redstar)获取资源名为redstar的资源,返回的是一个Drawable对象。
对于按键信息,直接重写View类的onKeyDown方法,这里KeyEvent传递的是按键的映射,比如KEYCODE_DPAD_UP向上,KeyEvent.KEYCODE_DPAD_DOWN向下等等,详细的查看SDK中的onKeyDown
@Override
public boolean onKeyDown(int keyCode, KeyEvent msg) {
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {}
}
整个游戏的控制流程就是上面这些,对于游戏的逻辑而言比较简单,这个贪食蛇并没有包含3D设计和类似Nokia的能量走廊、6边形轨迹,有空了我们一起来完善一个3D的贪食蛇游戏
下面这篇文章来自:http://www.jizhuomi.com/android/example/177.html
贪吃蛇剖析(一:暂停/继续、穿墙和全屏)
本文开始将为大家剖析Android示例程序-Snake贪吃蛇。贪吃蛇游戏大部分人都玩过,它是怎样实现的呢?Android示例程序给出了代码,下面进行详细分析。
游戏暂停/继续机制
由于原来的代码中在游戏运行时没有提供控制选项(比如暂停/继续),因此除非你死了,否则只能玩到底。我这里对代码进行一些修改,加入一个Option Menu来提供暂停/继续机制。
首先加入一个变量记录游戏当前状态:
private int mState = SnakeView.READY;
然后重载onCreateOptionsMenu函数,创建一个控制菜单项,并对其进行处理,提供暂停/继续机制。
Java代码
- /*
- * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
- * @Author:phinecos
- * @Date:2009-08-28
- */
- @Override
- public boolean onOptionsItemSelected(MenuItem item)
- {
- switch (item.getItemId())
- {
- case MENU_CONTROL:
- {
- if (mState == SnakeView.PAUSE)
- {//此前状态是"停止",则转为"运行"
- mState = SnakeView.RUNNING;
- mSnakeView.setMode(SnakeView.RUNNING);
- item.setIcon(android.R.drawable.ic_media_pause).setTitle(R.string.cmd_pause);
- }
- else if(mState == SnakeView.RUNNING)
- {//此前状态是"运行",则转为“暂停"
- mState = SnakeView.PAUSE;
- mSnakeView.setMode(SnakeView.PAUSE);
- item.setIcon(android.R.drawable.ic_media_play).setTitle(R.string.cmd_run);
- }
- else if(mState == SnakeView.READY)
- {//此前是"初始状态",则转为"运行"
- mState = SnakeView.RUNNING;
- }
- return true;
- }
- }
- return super.onOptionsItemSelected(item);
- }
- /*
- * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
- * @Author:phinecos
- * @Date:2009-08-28
- */
- @Override
- public boolean onCreateOptionsMenu(Menu menu)
- {
- super.onCreateOptionsMenu(menu);
- menu.add(0, MENU_CONTROL, 0, R.string.cmd_pause).setIcon(android.R.drawable.ic_media_pause);
- return true;
- }
修改后运行截图如下:
当然,这段代码还是有问题的,游戏刚开始时,必须先点击菜单确认,再按上方向键才能开始。(以后再来修改。。。)
穿墙贪食蛇
第二个修改是把这个普通的贪食蛇改成可以穿墙(呵呵,这样就可以不死了。。。)。想必要修改的就是撞墙检测那段代码?没错,就是下面这段!
Java代码
- // Collision detection
- // For now we have a 1-square wall around the entire arena
- if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2)|| (newHead.y > mYTileCount - 2))
- {//撞墙
- setMode(LOSE);
- return;
- }
原来的版本是发现撞墙时就直接判定为失败,我这里做个小小的修改,让它可以穿墙而去:
Java代码
- private void updateSnake()
- {
- boolean growSnake = false;
- // grab the snake by the head
- Coordinate head = mSnakeTrail.get(0);
- Coordinate newHead = new Coordinate(1, 1);
- mDirection = mNextDirection;
- switch (mDirection)
- {
- case EAST:
- {
- newHead = new Coordinate(head.x + 1, head.y);
- break;
- }
- case WEST:
- {
- newHead = new Coordinate(head.x - 1, head.y);
- break;
- }
- case NORTH:
- {
- newHead = new Coordinate(head.x, head.y - 1);
- break;
- }
- case SOUTH:
- {
- newHead = new Coordinate(head.x, head.y + 1);
- break;
- }
- }
- //穿墙的处理
- if (newHead.x == 0)
- {//穿左边的墙
- newHead.x = mXTileCount - 2;
- }
- else if (newHead.y == 0)
- {//穿上面的墙
- newHead.y = mYTileCount - 2;
- }
- else if (newHead.x == mXTileCount - 1)
- {//穿右边的墙
- newHead.x = 1;
- }
- else if (newHead.y == mYTileCount - 1)
- {//穿下面的墙
- newHead.y = 1;
- }
- // 判断是否撞到自己
- int snakelength = mSnakeTrail.size();
- for (int snakeindex = 0; snakeindex < snakelength; snakeindex++)
- {
- Coordinate c = mSnakeTrail.get(snakeindex);
- if (c.equals(newHead))
- {
- setMode(LOSE);
- return;
- }
- }
- // 判断是否吃掉“苹果”
- int applecount = mAppleList.size();
- for (int appleindex = 0; appleindex < applecount; appleindex++)
- {
- Coordinate c = mAppleList.get(appleindex);
- if (c.equals(newHead))
- {
- mAppleList.remove(c);
- addRandomApple();
- mScore++;
- mMoveDelay *= 0.9;
- growSnake = true;
- }
- }
- // push a new head onto the ArrayList and pull off the tail
- mSnakeTrail.add(0, newHead);
- // except if we want the snake to grow
- if (!growSnake)
- {
- mSnakeTrail.remove(mSnakeTrail.size() - 1);
- }
- int index = 0;
- for (Coordinate c : mSnakeTrail)
- {
- if (index == 0)
- {
- setTile(YELLOW_STAR, c.x, c.y);
- }
- else
- {
- setTile(RED_STAR, c.x, c.y);
- }
- index++;
- }
- }
其实修改后的代码非常简单,就是把新节点的值做些处理,让它移动到对应的行/列的头部或尾部即可。下面就是修改后的“穿墙”贪食蛇的运行截图:
全屏机制
游戏一般都是全屏的,原始代码也考虑到标题栏太过难看了,于是使用下面这句代码就去掉了标题栏:
Java代码
- requestWindowFeature(Window.FEATURE_NO_TITLE);
可还是没有达到全屏的效果,在Android1.5中实现全屏效果非常简单,只需要一句代码即可实现:
Java代码
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
运行效果如下图所示:
贪吃蛇剖析(二:FrameLayout与RelativeLayout)
前一节中将了贪吃蛇Snake游戏的暂停/继续、穿墙和全屏功能的实现,本文继续分析此示例程序中体现的Android Layout机制。
1、FrameLayout
先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前 一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。
有点绕口而且难理解,下面还是通过一个实例来理解吧。我们仿照Snake项目中使用的界面一样,建立一个简单的FrameLayout,其中包含两个Views元素:ImageView和TextView,而后面的TextView还包含在一个RelativeLayout中。
XML/HTML代码
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <ImageView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:scaleType="center" android:src="@drawable/img0"/>
- <RelativeLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <TextView
- android:text="Hello Android"
- android:visibility="visible"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:gravity="center_horizontal"
- android:textColor="#ffffffff"
- android:textSize="24sp"/>
- </RelativeLayout>
- </FrameLayout>
效果如下图所示:
2、UI优化
Android的tools目录下提供了许多实用工具,这里介绍其中一个用于查看当前UI结构视图的工具hierarchyviewer。打开tools/hierarchyviewer.bat后,查看上面这个示例的UI结构图可得:
我们可以很明显的看到由红色线框所包含的结构出现了两个framelayout节点,很明显这两个完全意义相同的节点造成了资源浪费(这里可以提醒大家在 开发工程中可以习惯性的通过hierarchyViewer查看当前UI资源的分配情况),那么如何才能解决这种问题呢(就当前例子是如何去掉多余的 frameLayout节点)?这时候就要用到<merge />标签来处理类似的问题了。我们将上边xml代码中的framLayout替换成merge:
XML/HTML代码
- <merge xmlns:android="http://schemas.android.com/apk/res/android">
- <ImageView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:scaleType="center" android:src="@drawable/img0"/>
- <RelativeLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <TextView
- android:text="Hello Android"
- android:visibility="visible"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:gravity="center_horizontal"
- android:textColor="#ffffffff"
- android:textSize="24sp"/>
- </RelativeLayout>
- </merge >
运行程序后在Emulator中显示的效果是一样的,可是通过hierarchyviewer查看的UI结构是有变化的,当初多余的 FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(这里需 要提醒大家注意:所有的Activity视图的根节点都是frameLayout)。如果你所创建的Layout并不是用framLayout作为根节点 (而是应用LinerLayout等定义root标签),就不能应用上边的例子通过merge来优化UI结构。
3、RelativeLayout
RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式 来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。
解释起来也比较麻烦,不过我做个对比实验可以明白它的用处了,试着把上面例子里的RelativeLayout节点去掉看看,效果如下图所示,可以看到 由于FrameLayout的原因,都在左上角靠拢了,而使用了RelativeLayout,则可以让TextView相对于屏幕居中。
4、Snake的界面分析
有了上述Layout的基础知识,我们再来看Snake的布局文件就很好理解了,就是一个SnakeView和一个TextView,启动后,后者会覆盖在前者上面。
XML/HTML代码
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <com.example.android.snake.SnakeView
- android:id="@+id/snake"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- tileSize="24"
- />
- <RelativeLayout
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <TextView
- android:id="@+id/text"
- android:text="@string/snake_layout_text_text"
- android:visibility="visible"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:gravity="center_horizontal"
- android:textColor="#ff8888ff"
- android:textSize="24sp"/>
- </RelativeLayout>
- </FrameLayout>
也就是这样的效果:
那么相应的代码是如何实现这个效果的呢? SnakeView有一个私有变量存放覆盖其上的TextView:
Java代码
- private TextView mStatusText;
在Snake这个Activity的onCreate方法中,首先将Layout文件中的SnakeView和TextView关联起来:
Java代码
- setContentView(R.layout.snake_layout);
- mSnakeView = (SnakeView) findViewById(R.id.snake);
- mSnakeView.setTextView((TextView) findViewById(R.id.text));
然后设置SnakeView的状态为Ready:
Java代码
- mSnakeView.setMode(SnakeView.READY);
这一句代码会调用下述函数:
Java代码
- public void setMode(int newMode)
- {
- int oldMode = mMode;
- mMode = newMode;
- if (newMode == RUNNING & oldMode != RUNNING)
- {//游戏进入“运行”状态,则隐藏文字信息
- mStatusText.setVisibility(View.INVISIBLE);
- update();
- return;
- }
- //根据新状态,设置待显示的文字信息
- Resources res = getContext().getResources();
- CharSequence str = "";
- if (newMode == PAUSE)
- {//新状态为“暂停”
- str = res.getText(R.string.mode_pause);
- }
- if (newMode == READY)
- {//新状态为“准备开始”
- str = res.getText(R.string.mode_ready);
- }
- if (newMode == LOSE)
- {//新状态为“游戏失败”
- str = res.getString(R.string.mode_lose_prefix) + mScore
- + res.getString(R.string.mode_lose_suffix);
- }
- //设置文字信息并显示
- mStatusText.setText(str);
- mStatusText.setVisibility(View.VISIBLE);
- }
在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
贪吃蛇剖析(三:界面UI、游戏逻辑和Handler)
往往我们在程序设计的时候喜欢将界面与处理分开,这样降低耦合性,易于维护扩展。在贪吃蛇Snake这个示例程序中同样将界面UI和游戏逻辑进行了分离, 它的实现方式就是,用父类TileView来实现比较基础的界面UI部分,而TileView类的子类SnakeView类完成了游戏控制逻辑部分,这样 就成功的将两者进行了分离,对后面的扩展和维护奠定了良好的基础。
界面UI
首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的 状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。TileView的数据成员如下:
Java代码
- //方格的大小
- protected static int mTileSize;
- //方格的行数和列数
- protected static int mXTileCount;
- protected static int mYTileCount;
- //xy坐标系的偏移量
- private static int mXOffset;
- private static int mYOffset;
- //存储三种方格的图标文件
- private Bitmap[] mTileArray;
- //二维方格地图
- private int[][] mTileGrid;
那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。
Java代码
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh)
- {
- //计算屏幕中可放置的方格的行数和列数
- mXTileCount = (int) Math.floor(w / mTileSize);
- mYTileCount = (int) Math.floor(h / mTileSize);
- mXOffset = ((w - (mTileSize * mXTileCount)) / 2);
- mYOffset = ((h - (mTileSize * mYTileCount)) / 2);
- mTileGrid = new int[mXTileCount][mYTileCount];
- clearTiles();
- }
注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应 的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。
第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。
Java代码
- public void onDraw(Canvas canvas)
- {
- super.onDraw(canvas);
- for (int x = 0; x < mXTileCount; x += 1)
- {
- for (int y = 0; y < mYTileCount; y += 1)
- {
- if (mTileGrid[x][y] > 0)
- {
- canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
- mXOffset + x * mTileSize,
- mYOffset + y * mTileSize,
- mPaint);
- }
- }
- }
- }
onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。
游戏逻辑
再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。
Java代码
- private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();//组成贪食蛇的方格列表
- private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();//苹果方格列表
由于SnakeView从TileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。
Java代码
- private void initNewGame()
- {
- //清空蛇和苹果占据的方格
- mSnakeTrail.clear();
- mAppleList.clear();
- //目前组成蛇的方格式固定的,而且方向也固定朝北
- mSnakeTrail.add(new Coordinate(7, 7));
- mSnakeTrail.add(new Coordinate(6, 7));
- mSnakeTrail.add(new Coordinate(5, 7));
- mSnakeTrail.add(new Coordinate(4, 7));
- mSnakeTrail.add(new Coordinate(3, 7));
- mSnakeTrail.add(new Coordinate(2, 7));
- mNextDirection = NORTH;
- //随即加入苹果
- for (int i = 0; i < nApples; ++i)
- {
- addRandomApple();
- }
- //初始化运动速率和玩家成绩
- mMoveDelay = 600;
- mScore = 0;
- }
想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负 责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器 等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。
这里要着重说明下private long mMoveDelay = 600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?
可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个 改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用 onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以 设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的“拍照“的效 果。
Handler的使用
写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现 setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用Timer。Snake 中使用了前者。
Java代码
- class RefreshHandler extends Handler
- {
- @Override
- public void handleMessage(Message msg)
- {//“苏醒”后的处理
- SnakeView.this.update();
- SnakeView.this.invalidate();
- }
- public void sleep(long delayMillis)
- {//休眠delayMillis毫秒
- this.removeMessages(0);
- sendMessageDelayed(obtainMessage(0), delayMillis);
- }
- };
而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在 Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做 “引擎“不为过。
Java代码
- public void update()
- {
- if (mMode == RUNNING)
- {
- long now = System.currentTimeMillis();
- if (now - mLastMove > mMoveDelay)
- {
- clearTiles();
- updateWalls();
- updateSnake();
- updateApples();
- mLastMove = now;
- }
- mRedrawHandler.sleep(mMoveDelay);
- }
- }
既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为 “运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这 一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。
一点困惑
可是个人认为Snake下面这段代码读起来有点怪,有点像一个“先有鸡,还是先有蛋?“的问题,导致我的思维逻辑上出现一个“怪圈“。
Java代码
- public void handleMessage(Message msg)
- {
- SnakeView.this.update();
- SnakeView.this.invalidate();
- }
按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。
哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6 毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去 呢),更何况在update中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢? 也就是说invalidate根本没有执行???
按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次 对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的, 不过关于这一个迷惑我的地方,希望有朋友能指点我一下!
记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?
此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一 次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。
LunarLander游戏
前面有几篇文章写的是对Android示例程序贪吃蛇Snake程序的剖析,本文继续分析Android自带的另一个小游戏LunarLander的程序。在贪吃蛇Snake程序中采用了“定时器+系统调用onDraw”的架构,而LunarLander程序采用的是“多线程+强制自行绘制”的架构思路,比前者更为实用。
与贪吃蛇Snake程序的对比
就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来 实现,这里是LunarView。读过贪吃蛇程序剖析文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最 大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可 却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联 产承包制”时代。
此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不 断发生的事件在进行就可以,例如Snake中使用的定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产 生这种“差异源”的。
如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武 器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是 在Canvas(画布)中自行绘制出来的(背景,人物等)。
SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部 件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操 作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的:The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. 使用的SurfaceView的时候,一般情况下还要对其进行创建、销毁、改变时的情况进行监视,这就要用到 SurfaceHolder.Callback。
Java代码
- class LunarView extends SurfaceView implements SurfaceHolder.Callback
- {
- public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
- //在surface的大小发生改变时激发
- public void surfaceCreated(SurfaceHolder holder){}
- //在创建时激发,一般在这里调用画图的线程。
- public void surfaceDestroyed(SurfaceHolder holder) {}
- //销毁时激发,一般在这里将画图的线程停止、释放。
- }
surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程。
Java代码
- private LunarThread thread; // 实际工作线程
- thread = new LunarThread(holder, context, new Handler() {
- @Override
- public void handleMessage(Message m)
- {
- mStatusText.setVisibility(m.getData().getInt("viz"));
- mStatusText.setText(m.getData().getString("text"));
- }
- });
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在 Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。由于有了 LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
Java代码
- public void surfaceChanged(SurfaceHolder holder, int format, int width,int height){
- thread.setSurfaceSize(width, height);
- }
- public void surfaceCreated(SurfaceHolder holder)
- {//启动工作线程结束
- thread.setRunning(true);
- thread.start();
- }
- public void surfaceDestroyed(SurfaceHolder holder)
- {
- boolean retry = true;
- thread.setRunning(false);
- while (retry)
- {
- try
- {//等待工作线程结束,主线程才结束
- thread.join();
- retry = false;
- }
- catch (InterruptedException e)
- {
- }
- }
- }
工作线程LunarThread
由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。
再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View 的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾Android示例程序剖析之Snake贪吃蛇(三:界面UI、游戏逻辑和Handler)中SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
Java代码
- public void setState(int mode, CharSequence message)
- {
- synchronized (mSurfaceHolder)
- {
- mMode = mode;
- if (mMode == STATE_RUNNING)
- {//运行中,隐藏界面文字信息
- Message msg = mHandler.obtainMessage();
- Bundle b = new Bundle();
- b.putString("text", "");
- b.putInt("viz", View.INVISIBLE);
- msg.setData(b);
- mHandler.sendMessage(msg);
- }
- else
- {//根据当前状态设置文字信息
- mRotating = 0;
- mEngineFiring = false;
- Resources res = mContext.getResources();
- CharSequence str = "";
- if (mMode == STATE_READY)
- str = res.getText(R.string.mode_ready);
- else if (mMode == STATE_PAUSE)
- str = res.getText(R.string.mode_pause);
- else if (mMode == STATE_LOSE)
- str = res.getText(R.string.mode_lose);
- else if (mMode == STATE_WIN)
- str = res.getString(R.string.mode_win_prefix)
- + mWinsInARow + " "
- + res.getString(R.string.mode_win_suffix);
- if (message != null) {
- str = message + "\n" + str;
- }
- if (mMode == STATE_LOSE)
- mWinsInARow = 0;
- Message msg = mHandler.obtainMessage();
- Bundle b = new Bundle();
- b.putString("text", str.toString());
- b.putInt("viz", View.VISIBLE);
- msg.setData(b);
- mHandler.sendMessage(msg);
- }
- }
- }
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
Java代码
- public void run()
- {
- while (mRun)
- {
- Canvas c = null;
- try
- {
- //锁定待绘制区域
- c = mSurfaceHolder.lockCanvas(null);
- synchronized (mSurfaceHolder)
- {
- if (mMode == STATE_RUNNING)
- updatePhysics();//更新底层数据,判断游戏状态
- doDraw(c);//强制重绘制
- }
- }
- finally
- {
- if (c != null) {
- mSurfaceHolder.unlockCanvasAndPost(c);
- }
- }
- }
- }
这里要注意的是最后要调用unlockCanvasAndPost来结束锁定画图,并提交改变。
强制自行绘制
doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
Java代码
- canvas.save();
- canvas.rotate((float) mHeading, (float) mX, mCanvasHeight
- - (float) mY);
- if (mMode == STATE_LOSE) {
- mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
- + mLanderHeight);
- mCrashedImage.draw(canvas);
- } else if (mEngineFiring) {
- mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
- + mLanderHeight);
- mFiringImage.draw(canvas);
- } else {
- mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
- + mLanderHeight);
- mLanderImage.draw(canvas);
- }
- canvas.restore();
在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有 的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。
暂停/继续机制
LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为 当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
Java代码
- public void pause()
- {//暂停
- synchronized (mSurfaceHolder)
- {
- if (mMode == STATE_RUNNING)
- setState(STATE_PAUSE);
- }
- }
- public void unpause()
- {// 继续
- // Move the real time clock up to now
- synchronized (mSurfaceHolder)
- {
- mLastTime = System.currentTimeMillis() + 100;
- }
- setState(STATE_RUNNING);
- }
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句:
Java代码
- if (mLastTime > now) return;
- double elapsed = (now - mLastTime) / 1000.0;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。