【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解
Snake也是一个经典游戏了,Nokia蓝屏机的王牌游戏之一。Android SDK 1.5就有了它的身影。我们这里就来详细解析一下Android SDK Sample中的Snake工程。本工程基于SDK 2.3.3版本中的工程,路径为:%Android_SDK_HOME% /samples/android-10/Snake
一、Eclipse工程
通过File-New Project-Android-Android Project,选择“Create project from existing sample”创建自己的应用SnakeAndroid,如下图:
运行效果如下图:
二、工程结构和类图
其实Snake的工程蛮简单的,源文件就三个:Snake.java SnakeView.java TileView.java。Snake类是这个游戏的入口点,TitleView类进行游戏的绘画,SnakeView类则是对游戏控制操作的处理。Coordinate,RefreshHandler是2个辅助类,也是SnakeView类中的内部类。其中,Coordinate是一个点的坐标(x,y),RefreshHandler将RefreshHandler对象绑定某个线程并给它发送消息。如下图:
任何游戏都需要有个引擎来推动游戏的运行,最简化的游戏引擎就是:在一个线程中While循环,检测用户操作,对用户的操作作出反应,更新游戏的界面,直到用户退出游戏。
在Snake这个游戏中,辅助类RefreshHandler继承自Handler,用来把RefreshHandler与当前线程进行绑定,从而可以直接给线程发送消息并处理消息。注意一点:Handle对消息的处理都是异步。RefreshHandler在Handler的基础上增加sleep()接口,用来每隔一个时间段后给当前线程发送一个消息。handleMessage()方法在接受消息后,根据当前的游戏状态重绘界面,运行机制如下:
这比较类似定时器的概念,在特定的时刻发送消息,根据消息处理相应的事件。update()与sleep()间接的相互调用就构成了一个循环。这里要注意:mRedrawHandle绑定的是Avtivity所在的线程,也就是程序的主线程;另外由于sleep()是个异步函数,所以update()与sleep()之间的相互调用才没有构成死循环。
最后分析下游戏数据的保存机制,如下:
这里考虑了Activity的生命周期:如果用户在游戏期间离开游戏界面,游戏暂停;或者由于内存比较紧张,Android关闭游戏释放内存,那么当用户返回游戏界面的时候恢复到上次离开时的界面。
三、源码解析
详细解析下源代码,由于代码量不大,以注释的方式列出如下:
1、Snake.java
2 * <p>Title: Snake</p>
3 * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p>
4 * @author Gavin 标注
5 */
6
7 package com.deaboway.snake;
8
9 import android.app.Activity;
10 import android.os.Bundle;
11 import android.widget.TextView;
12
13 /**
14 * Snake: a simple game that everyone can enjoy.
15 *
16 * This is an implementation of the classic Game "Snake", in which you control a
17 * serpent roaming around the garden looking for apples. Be careful, though,
18 * because when you catch one, not only will you become longer, but you'll move
19 * faster. Running into yourself or the walls will end the game.
20 *
21 */
22 // 贪吃蛇: 经典游戏,在一个花园中找苹果吃,吃了苹果会变长,速度变快。碰到自己和墙就挂掉。
23 public class Snake extends Activity {
24
25 private SnakeView mSnakeView;
26
27 private static String ICICLE_KEY = "snake-view";
28
29 /**
30 * Called when Activity is first created. Turns off the title bar, sets up
31 * the content views, and fires up the SnakeView.
32 *
33 */
34 // 在 activity 第一次创建时被调用
35 @Override
36 public void onCreate(Bundle savedInstanceState) {
37
38 super.onCreate(savedInstanceState);
39 setContentView(R.layout.snake_layout);
40
41 mSnakeView = (SnakeView) findViewById(R.id.snake);
42 mSnakeView.setTextView((TextView) findViewById(R.id.text));
43
44 // 检查存贮状态以确定是重新开始还是恢复状态
45 if (savedInstanceState == null) {
46 // 存储状态为空,说明刚启动可以切换到准备状态
47 mSnakeView.setMode(SnakeView.READY);
48 } else {
49 // 已经保存过,那么就去恢复原有状态
50 Bundle map = savedInstanceState.getBundle(ICICLE_KEY);
51 if (map != null) {
52 // 恢复状态
53 mSnakeView.restoreState(map);
54 } else {
55 // 设置状态为暂停
56 mSnakeView.setMode(SnakeView.PAUSE);
57 }
58 }
59 }
60
61 // 暂停事件被触发时
62 @Override
63 protected void onPause() {
64 super.onPause();
65 // Pause the game along with the activity
66 mSnakeView.setMode(SnakeView.PAUSE);
67 }
68
69 // 状态保存
70 @Override
71 public void onSaveInstanceState(Bundle outState) {
72 // 存储游戏状态到View里
73 outState.putBundle(ICICLE_KEY, mSnakeView.saveState());
74 }
75
76 }
2、SnakeView.java
2 * <p>Title: Snake</p>
3 * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p>
4 * @author Gavin 标注
5 */
6
7 package com.deaboway.snake;
8
9 import java.util.ArrayList;
10 import java.util.Random;
11
12 import android.content.Context;
13 import android.content.res.Resources;
14 import android.os.Handler;
15 import android.os.Message;
16 import android.util.AttributeSet;
17 import android.os.Bundle;
18 import android.util.Log;
19 import android.view.KeyEvent;
20 import android.view.View;
21 import android.widget.TextView;
22
23 /**
24 * SnakeView: implementation of a simple game of Snake
25 *
26 *
27 */
28 public class SnakeView extends TileView {
29
30 private static final String TAG = "Deaboway";
31
32 /**
33 * Current mode of application: READY to run, RUNNING, or you have already
34 * lost. static final ints are used instead of an enum for performance
35 * reasons.
36 */
37 // 游戏状态,默认值是准备状态
38 private int mMode = READY;
39
40 // 游戏的四个状态 暂停 准备 运行 和 失败
41 public static final int PAUSE = 0;
42 public static final int READY = 1;
43 public static final int RUNNING = 2;
44 public static final int LOSE = 3;
45
46 // 游戏中蛇的前进方向,默认值北方
47 private int mDirection = NORTH;
48 // 下一步的移动方向,默认值北方
49 private int mNextDirection = NORTH;
50
51 // 游戏方向设定 北 南 东 西
52 private static final int NORTH = 1;
53 private static final int SOUTH = 2;
54 private static final int EAST = 3;
55 private static final int WEST = 4;
56
57 /**
58 * Labels for the drawables that will be loaded into the TileView class
59 */
60 // 三种游戏元
61 private static final int RED_STAR = 1;
62 private static final int YELLOW_STAR = 2;
63 private static final int GREEN_STAR = 3;
64
65 /**
66 * mScore: used to track the number of apples captured mMoveDelay: number of
67 * milliseconds between snake movements. This will decrease as apples are
68 * captured.
69 */
70 // 游戏得分
71 private long mScore = 0;
72
73 // 移动延迟
74 private long mMoveDelay = 600;
75
76 /**
77 * mLastMove: tracks the absolute time when the snake last moved, and is
78 * used to determine if a move should be made based on mMoveDelay.
79 */
80 // 最后一次移动时的毫秒时刻
81 private long mLastMove;
82
83 /**
84 * mStatusText: text shows to the user in some run states
85 */
86 // 显示游戏状态的文本组件
87 private TextView mStatusText;
88
89 /**
90 * mSnakeTrail: a list of Coordinates that make up the snake's body
91 * mAppleList: the secret location of the juicy apples the snake craves.
92 */
93 // 蛇身数组(数组以坐标对象为元素)
94 private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();
95
96 // 苹果数组(数组以坐标对象为元素)
97 private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();
98
99 /**
100 * Everyone needs a little randomness in their life
101 */
102 // 随机数
103 private static final Random RNG = new Random();
104
105 /**
106 * Create a simple handler that we can use to cause animation to happen. We
107 * set ourselves as a target and we can use the sleep() function to cause an
108 * update/invalidate to occur at a later date.
109 */
110 // 创建一个Refresh Handler来产生动画: 通过sleep()来实现
111 private RefreshHandler mRedrawHandler = new RefreshHandler();
112
113 // 一个Handler
114 class RefreshHandler extends Handler {
115
116 // 处理消息队列
117 @Override
118 public void handleMessage(Message msg) {
119 // 更新View对象
120 SnakeView.this.update();
121 // 强制重绘
122 SnakeView.this.invalidate();
123 }
124
125 // 延迟发送消息
126 public void sleep(long delayMillis) {
127 this.removeMessages(0);
128 sendMessageDelayed(obtainMessage(0), delayMillis);
129 }
130 };
131
132 /**
133 * Constructs a SnakeView based on inflation from XML
134 *
135 * @param context
136 * @param attrs
137 */
138 // 构造函数
139 public SnakeView(Context context, AttributeSet attrs) {
140 super(context, attrs);
141 // 构造时初始化
142 initSnakeView();
143 }
144
145 public SnakeView(Context context, AttributeSet attrs, int defStyle) {
146 super(context, attrs, defStyle);
147 initSnakeView();
148 }
149
150 // 初始化
151 private void initSnakeView() {
152 // 可选焦点
153 setFocusable(true);
154
155 Resources r = this.getContext().getResources();
156
157 // 设置贴片图片数组
158 resetTiles(4);
159
160 // 把三种图片存到Bitmap对象数组
161 loadTile(RED_STAR, r.getDrawable(R.drawable.redstar));
162 loadTile(YELLOW_STAR, r.getDrawable(R.drawable.yellowstar));
163 loadTile(GREEN_STAR, r.getDrawable(R.drawable.greenstar));
164
165 }
166
167 // 开始新的游戏——初始化
168 private void initNewGame() {
169 // 清空ArrayList列表
170 mSnakeTrail.clear();
171 mAppleList.clear();
172
173 // For now we're just going to load up a short default eastbound snake
174 // that's just turned north
175 // 创建蛇身
176
177 mSnakeTrail.add(new Coordinate(7, 7));
178 mSnakeTrail.add(new Coordinate(6, 7));
179 mSnakeTrail.add(new Coordinate(5, 7));
180 mSnakeTrail.add(new Coordinate(4, 7));
181 mSnakeTrail.add(new Coordinate(3, 7));
182 mSnakeTrail.add(new Coordinate(2, 7));
183
184 // 新的方向 :北方
185 mNextDirection = NORTH;
186
187 // 2个随机位置的苹果
188 addRandomApple();
189 addRandomApple();
190
191 // 移动延迟
192 mMoveDelay = 600;
193 // 初始得分0
194 mScore = 0;
195 }
2 * Given a ArrayList of coordinates, we need to flatten them into an array
3 * of ints before we can stuff them into a map for flattening and storage.
4 *
5 * @param cvec
6 * : a ArrayList of Coordinate objects
7 * @return : a simple array containing the x/y values of the coordinates as
8 * [x1,y1,x2,y2,x3,y3...]
9 */
10 // 坐标数组转整数数组,把Coordinate对象的x y放到一个int数组中——用来保存状态
11 private int[] coordArrayListToArray(ArrayList<Coordinate> cvec) {
12 int count = cvec.size();
13 int[] rawArray = new int[count * 2];
14 for (int index = 0; index < count; index++) {
15 Coordinate c = cvec.get(index);
16 rawArray[2 * index] = c.x;
17 rawArray[2 * index + 1] = c.y;
18 }
19 return rawArray;
20 }
21
22 /**
23 * Save game state so that the user does not lose anything if the game
24 * process is killed while we are in the background.
25 *
26 * @return a Bundle with this view's state
27 */
28 // 保存状态
29 public Bundle saveState() {
30
31 Bundle map = new Bundle();
32
33 map.putIntArray("mAppleList", coordArrayListToArray(mAppleList));
34 map.putInt("mDirection", Integer.valueOf(mDirection));
35 map.putInt("mNextDirection", Integer.valueOf(mNextDirection));
36 map.putLong("mMoveDelay", Long.valueOf(mMoveDelay));
37 map.putLong("mScore", Long.valueOf(mScore));
38 map.putIntArray("mSnakeTrail", coordArrayListToArray(mSnakeTrail));
39
40 return map;
41 }
42
43 /**
44 * Given a flattened array of ordinate pairs, we reconstitute them into a
45 * ArrayList of Coordinate objects
46 *
47 * @param rawArray
48 * : [x1,y1,x2,y2,...]
49 * @return a ArrayList of Coordinates
50 */
51 // 整数数组转坐标数组,把一个int数组中的x y放到Coordinate对象数组中——用来恢复状态
52 private ArrayList<Coordinate> coordArrayToArrayList(int[] rawArray) {
53 ArrayList<Coordinate> coordArrayList = new ArrayList<Coordinate>();
54
55 int coordCount = rawArray.length;
56 for (int index = 0; index < coordCount; index += 2) {
57 Coordinate c = new Coordinate(rawArray[index], rawArray[index + 1]);
58 coordArrayList.add(c);
59 }
60 return coordArrayList;
61 }
62
63 /**
64 * Restore game state if our process is being relaunched
65 *
66 * @param icicle
67 * a Bundle containing the game state
68 */
69 // 恢复状态
70 public void restoreState(Bundle icicle) {
71
72 setMode(PAUSE);
73
74 mAppleList = coordArrayToArrayList(icicle.getIntArray("mAppleList"));
75 mDirection = icicle.getInt("mDirection");
76 mNextDirection = icicle.getInt("mNextDirection");
77 mMoveDelay = icicle.getLong("mMoveDelay");
78 mScore = icicle.getLong("mScore");
79 mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail"));
80 }
81
82 /*
83 * handles key events in the game. Update the direction our snake is
84 * traveling based on the DPAD. Ignore events that would cause the snake to
85 * immediately turn back on itself.
86 *
87 * (non-Javadoc)
88 *
89 * @see android.view.View#onKeyDown(int, android.os.KeyEvent)
90 */
91 // 监听用户键盘操作,并处理这些操作
92 // 按键事件处理,确保贪吃蛇只能90度转向,而不能180度转向
93 @Override
94 public boolean onKeyDown(int keyCode, KeyEvent msg) {
95
96 // 向上键
97 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
98 // 准备状态或者失败状态时
99 if (mMode == READY | mMode == LOSE) {
100 /*
101 * At the beginning of the game, or the end of a previous one,
102 * we should start a new game.
103 */
104 // 初始化游戏
105 initNewGame();
106 // 设置游戏状态为运行
107 setMode(RUNNING);
108 // 更新
109 update();
110 // 返回
111 return (true);
112 }
113
114 // 暂停状态时
115 if (mMode == PAUSE) {
116 /*
117 * If the game is merely paused, we should just continue where
118 * we left off.
119 */
120 // 设置成运行状态
121 setMode(RUNNING);
122 update();
123 // 返回
124 return (true);
125 }
126
127 // 如果是运行状态时,如果方向原有方向不是向南,那么方向转向北
128 if (mDirection != SOUTH) {
129 mNextDirection = NORTH;
130 }
131 return (true);
132 }
133
134 // 向下键
135 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
136 // 原方向不是向上时,方向转向南
137 if (mDirection != NORTH) {
138 mNextDirection = SOUTH;
139 }
140 // 返回
141 return (true);
142 }
143
144 // 向左键
145 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
146 // 原方向不是向右时,方向转向西
147 if (mDirection != EAST) {
148 mNextDirection = WEST;
149 }
150 // 返回
151 return (true);
152 }
153
154 // 向右键
155 if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
156 // 原方向不是向左时,方向转向东
157 if (mDirection != WEST) {
158 mNextDirection = EAST;
159 }
160 // 返回
161 return (true);
162 }
163
164 // 按其他键时按原有功能返回
165 return super.onKeyDown(keyCode, msg);
166 }
2 * Sets the TextView that will be used to give information (such as "Game
3 * Over" to the user.
4 *
5 * @param newView
6 */
7 // 设置状态显示View
8 public void setTextView(TextView newView) {
9 mStatusText = newView;
10 }
11
12 /**
13 * Updates the current mode of the application (RUNNING or PAUSED or the
14 * like) as well as sets the visibility of textview for notification
15 *
16 * @param newMode
17 */
18 // 设置游戏状态
19 public void setMode(int newMode) {
20
21 // 把当前游戏状态存入oldMode
22 int oldMode = mMode;
23 // 把游戏状态设置为新状态
24 mMode = newMode;
25
26 // 如果新状态是运行状态,且原有状态为不运行,那么就开始游戏
27 if (newMode == RUNNING & oldMode != RUNNING) {
28 // 设置mStatusTextView隐藏
29 mStatusText.setVisibility(View.INVISIBLE);
30 // 更新
31 update();
32 return;
33 }
34
35 Resources res = getContext().getResources();
36 CharSequence str = "";
37
38 // 如果新状态是暂停状态,那么设置文本内容为暂停内容
39 if (newMode == PAUSE) {
40 str = res.getText(R.string.mode_pause);
41 }
42
43 // 如果新状态是准备状态,那么设置文本内容为准备内容
44 if (newMode == READY) {
45 str = res.getText(R.string.mode_ready);
46 }
47
48 // 如果新状态时失败状态,那么设置文本内容为失败内容
49 if (newMode == LOSE) {
50 // 把上轮的得分显示出来
51 str = res.getString(R.string.mode_lose_prefix) + mScore
52 + res.getString(R.string.mode_lose_suffix);
53 }
54
55 // 设置文本
56 mStatusText.setText(str);
57 // 显示该View
58 mStatusText.setVisibility(View.VISIBLE);
59 }
60
61 /**
62 * Selects a random location within the garden that is not currently covered
63 * by the snake. Currently _could_ go into an infinite loop if the snake
64 * currently fills the garden, but we'll leave discovery of this prize to a
65 * truly excellent snake-player.
66 *
67 */
68 // 添加苹果
69 private void addRandomApple() {
70 // 新的坐标
71 Coordinate newCoord = null;
72 // 防止新苹果出席在蛇身下
73 boolean found = false;
74 // 没有找到合适的苹果,就在循环体内一直循环,直到找到合适的苹果
75 while (!found) {
76 // 为苹果再找一个坐标,先随机一个X值
77 int newX = 1 + RNG.nextInt(mXTileCount - 2);
78 // 再随机一个Y值
79 int newY = 1 + RNG.nextInt(mYTileCount - 2);
80 // 新坐标
81 newCoord = new Coordinate(newX, newY);
82
83 // Make sure it's not already under the snake
84 // 确保新苹果不在蛇身下,先假设没有发生冲突
85 boolean collision = false;
86
87 int snakelength = mSnakeTrail.size();
88 // 和蛇占据的所有坐标比较
89 for (int index = 0; index < snakelength; index++) {
90 // 只要和蛇占据的任何一个坐标相同,即认为发生冲突了
91 if (mSnakeTrail.get(index).equals(newCoord)) {
92 collision = true;
93 }
94 }
95 // if we're here and there's been no collision, then we have
96 // a good location for an apple. Otherwise, we'll circle back
97 // and try again
98 // 如果有冲突就继续循环,如果没冲突flag的值就是false,那么自然会退出循环,新坐标也就诞生了
99 found = !collision;
100 }
101
102 if (newCoord == null) {
103 Log.e(TAG, "Somehow ended up with a null newCoord!");
104 }
105 // 生成一个新苹果放在苹果列表中(两个苹果有可能会重合——这时候虽然看到的是一个苹果,但是呢,分数就是两个分数。)
106 mAppleList.add(newCoord);
107 }
108
109 /**
110 * Handles the basic update loop, checking to see if we are in the running
111 * state, determining if a move should be made, updating the snake's
112 * location.
113 */
114 // 更新 各种动作,特别是 贪吃蛇 的位置, 还包括:墙、苹果等的更新
115 public void update() {
116 // 如果是处于运行状态
117 if (mMode == RUNNING) {
118
119 long now = System.currentTimeMillis();
120
121 // 如果当前时间距离最后一次移动的时间超过了延迟时间
122 if (now - mLastMove > mMoveDelay) {
123 //
124 clearTiles();
125 updateWalls();
126 updateSnake();
127 updateApples();
128 mLastMove = now;
129 }
130 // Handler 会话进程sleep一个延迟时间单位
131 mRedrawHandler.sleep(mMoveDelay);
132 }
133
134 }
135
136 /**
137 * Draws some walls.
138 *
139 */
140 // 更新墙
141 private void updateWalls() {
142 for (int x = 0; x < mXTileCount; x++) {
143 // 给上边线的每个贴片位置设置一个绿色索引标识
144 setTile(GREEN_STAR, x, 0);
145 // 给下边线的每个贴片位置设置一个绿色索引标识
146 setTile(GREEN_STAR, x, mYTileCount - 1);
147 }
148 for (int y = 1; y < mYTileCount - 1; y++) {
149 // 给左边线的每个贴片位置设置一个绿色索引标识
150 setTile(GREEN_STAR, 0, y);
151 // 给右边线的每个贴片位置设置一个绿色索引标识
152 setTile(GREEN_STAR, mXTileCount - 1, y);
153 }
154 }
155
156 /**
157 * Draws some apples.
158 *
159 */
160 // 更新苹果
161 private void updateApples() {
162 for (Coordinate c : mAppleList) {
163 setTile(YELLOW_STAR, c.x, c.y);
164 }
165 }
2 * Figure out which way the snake is going, see if he's run into anything
3 * (the walls, himself, or an apple). If he's not going to die, we then add
4 * to the front and subtract from the rear in order to simulate motion. If
5 * we want to grow him, we don't subtract from the rear.
6 *
7 */
8 // 更新蛇
9 private void updateSnake() {
10 // 生长标志
11 boolean growSnake = false;
12
13 // 得到蛇头坐标
14 Coordinate head = mSnakeTrail.get(0);
15 // 初始化一个新的蛇头坐标
16 Coordinate newHead = new Coordinate(1, 1);
17
18 // 当前方向改成新的方向
19 mDirection = mNextDirection;
20
21 // 根据方向确定蛇头新坐标
22 switch (mDirection) {
23 // 如果方向向东(右),那么X加1
24 case EAST: {
25 newHead = new Coordinate(head.x + 1, head.y);
26 break;
27 }
28 // 如果方向向西(左),那么X减1
29 case WEST: {
30 newHead = new Coordinate(head.x - 1, head.y);
31 break;
32 }
33 // 如果方向向北(上),那么Y减1
34 case NORTH: {
35 newHead = new Coordinate(head.x, head.y - 1);
36 break;
37 }
38 // 如果方向向南(下),那么Y加1
39 case SOUTH: {
40 newHead = new Coordinate(head.x, head.y + 1);
41 break;
42 }
43 }
44
45 // Collision detection
46 // For now we have a 1-square wall around the entire arena
47 // 冲突检测 新蛇头是否四面墙重叠,那么游戏结束
48 if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2)
49 || (newHead.y > mYTileCount - 2)) {
50 // 设置游戏状态为Lose
51 setMode(LOSE);
52 // 返回
53 return;
54
55 }
56
57 // Look for collisions with itself
58 // 冲突检测 新蛇头是否和自身坐标重叠,重叠的话游戏也结束
59 int snakelength = mSnakeTrail.size();
60
61 for (int snakeindex = 0; snakeindex < snakelength; snakeindex++) {
62 Coordinate c = mSnakeTrail.get(snakeindex);
63 if (c.equals(newHead)) {
64 // 设置游戏状态为Lose
65 setMode(LOSE);
66 // 返回
67 return;
68 }
69 }
70
71 // Look for apples
72 // 看新蛇头和苹果们是否重叠
73 int applecount = mAppleList.size();
74 for (int appleindex = 0; appleindex < applecount; appleindex++) {
75 Coordinate c = mAppleList.get(appleindex);
76 if (c.equals(newHead)) {
77 // 如果重叠,苹果坐标从苹果列表中移除
78 mAppleList.remove(c);
79 // 再立刻增加一个新苹果
80 addRandomApple();
81 // 得分加一
82 mScore++;
83 // 延迟是以前的90%
84 mMoveDelay *= 0.9;
85 // 蛇增长标志改为真
86 growSnake = true;
87 }
88 }
89
90 // push a new head onto the ArrayList and pull off the tail
91 // 在蛇头的位置增加一个新坐标
92 mSnakeTrail.add(0, newHead);
93 // except if we want the snake to grow
94 // 如果没有增长
95 if (!growSnake) {
96 // 如果蛇头没增长则删去最后一个坐标,相当于蛇向前走了一步
97 mSnakeTrail.remove(mSnakeTrail.size() - 1);
98 }
99
100 int index = 0;
101 // 重新设置一下颜色,蛇头是黄色的(同苹果一样),蛇身是红色的
102 for (Coordinate c : mSnakeTrail) {
103 if (index == 0) {
104 setTile(YELLOW_STAR, c.x, c.y);
105 } else {
106 setTile(RED_STAR, c.x, c.y);
107 }
108 index++;
109 }
110
111 }
112
113 /**
114 * Simple class containing two integer values and a comparison function.
115 * There's probably something I should use instead, but this was quick and
116 * easy to build.
117 *
118 */
119 // 坐标内部类——原作者说这是临时做法
120 private class Coordinate {
121 public int x;
122 public int y;
123
124 // 构造函数
125 public Coordinate(int newX, int newY) {
126 x = newX;
127 y = newY;
128 }
129
130 // 重写equals
131 public boolean equals(Coordinate other) {
132 if (x == other.x && y == other.y) {
133 return true;
134 }
135 return false;
136 }
137
138 // 重写toString
139 @Override
140 public String toString() {
141 return "Coordinate: [" + x + "," + y + "]";
142 }
143 }
144
145 }
3、TileView.java
2 * <p>Title: Snake</p>
3 * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p>
4 * @author Gavin 标注
5 */
6
7 package com.deaboway.snake;
8
9 import android.content.Context;
10 import android.content.res.TypedArray;
11 import android.graphics.Bitmap;
12 import android.graphics.Canvas;
13 import android.graphics.Paint;
14 import android.graphics.drawable.Drawable;
15 import android.util.AttributeSet;
16 import android.view.View;
17
18 /**
19 * TileView: a View-variant designed for handling arrays of "icons" or other
20 * drawables.
21 *
22 */
23 // View 变种,用来处理 一组 贴片—— “icons”或其它可绘制的对象
24 public class TileView extends View {
25
26 /**
27 * Parameters controlling the size of the tiles and their range within view.
28 * Width/Height are in pixels, and Drawables will be scaled to fit to these
29 * dimensions. X/Y Tile Counts are the number of tiles that will be drawn.
30 */
31
32 protected static int mTileSize;
33
34 // X轴的贴片数量
35 protected static int mXTileCount;
36 // Y轴的贴片数量
37 protected static int mYTileCount;
38
39 // X偏移量
40 private static int mXOffset;
41 // Y偏移量
42 private static int mYOffset;
43
44 /**
45 * A hash that maps integer handles specified by the subclasser to the
46 * drawable that will be used for that reference
47 */
48 // 贴片图像的图像数组
49 private Bitmap[] mTileArray;
50
51 /**
52 * A two-dimensional array of integers in which the number represents the
53 * index of the tile that should be drawn at that locations
54 */
55 // 保存每个贴片的索引——二维数组
56 private int[][] mTileGrid;
57
58 // Paint对象(画笔、颜料)
59 private final Paint mPaint = new Paint();
60
61 // 构造函数
62 public TileView(Context context, AttributeSet attrs, int defStyle) {
63 super(context, attrs, defStyle);
64
65 TypedArray a = context.obtainStyledAttributes(attrs,
66 R.styleable.TileView);
67
68 mTileSize = a.getInt(R.styleable.TileView_tileSize, 12);
69
70 a.recycle();
71 }
72
73 public TileView(Context context, AttributeSet attrs) {
74 super(context, attrs);
75
76 TypedArray a = context.obtainStyledAttributes(attrs,
77 R.styleable.TileView);
78
79 mTileSize = a.getInt(R.styleable.TileView_tileSize, 12);
80
81 a.recycle();
82 }
83
84 /**
85 * Rests the internal array of Bitmaps used for drawing tiles, and sets the
86 * maximum index of tiles to be inserted
87 *
88 * @param tilecount
89 */
90 // 设置贴片图片数组
91 public void resetTiles(int tilecount) {
92 mTileArray = new Bitmap[tilecount];
93 }
94
95 // 回调:当该View的尺寸改变时调用,在onDraw()方法调用之前就会被调用,所以用来设置一些变量的初始值
96 // 在视图大小改变的时候调用,比如说手机由垂直旋转为水平
97 @Override
98 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
99
100 // 定义X轴贴片数量
101 mXTileCount = (int) Math.floor(w / mTileSize);
102 mYTileCount = (int) Math.floor(h / mTileSize);
103
104 // X轴偏移量
105 mXOffset = ((w - (mTileSize * mXTileCount)) / 2);
106
107 // Y轴偏移量
108 mYOffset = ((h - (mTileSize * mYTileCount)) / 2);
109
110 // 定义贴片的二维数组
111 mTileGrid = new int[mXTileCount][mYTileCount];
112
113 // 清空所有贴片
114 clearTiles();
115 }
116
117 /**
118 * Function to set the specified Drawable as the tile for a particular
119 * integer key.
120 *
121 * @param key
122 * @param tile
123 */
124 // 给mTileArray这个Bitmap图片数组设置值
125 public void loadTile(int key, Drawable tile) {
126 Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize,
127 Bitmap.Config.ARGB_8888);
128 Canvas canvas = new Canvas(bitmap);
129 tile.setBounds(0, 0, mTileSize, mTileSize);
130 // 把一个drawable转成一个Bitmap
131 tile.draw(canvas);
132 // 在数组里存入该Bitmap
133 mTileArray[key] = bitmap;
134 }
135
136 /**
137 * Resets all tiles to 0 (empty)
138 *
139 */
140 // 清空所有贴片
141 public void clearTiles() {
142 for (int x = 0; x < mXTileCount; x++) {
143 for (int y = 0; y < mYTileCount; y++) {
144 // 全部设置为0
145 setTile(0, x, y);
146 }
147 }
148 }
149
150 /**
151 * Used to indicate that a particular tile (set with loadTile and referenced
152 * by an integer) should be drawn at the given x/y coordinates during the
153 * next invalidate/draw cycle.
154 *
155 * @param tileindex
156 * @param x
157 * @param y
158 */
159 // 给某个贴片位置设置一个状态索引
160 public void setTile(int tileindex, int x, int y) {
161 mTileGrid[x][y] = tileindex;
162 }
163
164 // onDraw 在视图需要重画的时候调用,比如说使用invalidate刷新界面上的某个矩形区域
165 @Override
166 public void onDraw(Canvas canvas) {
167
168 super.onDraw(canvas);
169 for (int x = 0; x < mXTileCount; x += 1) {
170 for (int y = 0; y < mYTileCount; y += 1) {
171 // 当索引大于零,也就是不空时
172 if (mTileGrid[x][y] > 0) {
173 // mTileGrid中不为零时画此贴片
174 canvas.drawBitmap(mTileArray[mTileGrid[x][y]], mXOffset + x
175 * mTileSize, mYOffset + y * mTileSize, mPaint);
176 }
177 }
178 }
179
180 }
181 }
四、工程文件下载
为了方便大家阅读,可以到如下地址下载工程源代码:
http://ishare.iask.sina.com.cn/f/14312223.html
五、小结及下期预告:
本次详细解析了Android SDK 自带 Sample——Snake的结构和功能。下次将会把这个游戏移植到J2ME平台上,并且比较Android和J2ME的区别和相通之处,让从事过J2ME开发的朋友对Android开发有个更加直观的认识。