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
1 /**
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
1 /**
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 }
1 /**
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 }
1 /**
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 }
1 /**
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
1 /**
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 }
四、工程文件下載
工程源代碼
五、小結及下期預告:
本次詳細解析了Android SDK 自帶 Sample——Snake的結構和功能。下次將會把這個游戲移植到J2ME平台上,並且比較Android和J2ME的區別和相通之處,讓從事過J2ME開發的朋友對Android開發有個更加直觀的認識。