【贪吃蛇—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.javaSnake类是这个游戏的入口点,TitleView类进行游戏的绘画,SnakeView类则是对游戏控制操作的处理。CoordinateRefreshHandler2个辅助类,也是SnakeView类中的内部类。其中,Coordinate是一个点的坐标(xy),RefreshHandlerRefreshHandler对象绑定某个线程并给它发送消息。如下图:

任何游戏都需要有个引擎来推动游戏的运行,最简化的游戏引擎就是:在一个线程中While循环,检测用户操作,对用户的操作作出反应,更新游戏的界面,直到用户退出游戏。

Snake这个游戏中,辅助类RefreshHandler继承自Handler,用来把RefreshHandler与当前线程进行绑定,从而可以直接给线程发送消息并处理消息。注意一点:Handle对消息的处理都是异步。RefreshHandlerHandler的基础上增加sleep()接口,用来每隔一个时间段后给当前线程发送一个消息。handleMessage()方法在接受消息后,根据当前的游戏状态重绘界面,运行机制如下:

 

 

这比较类似定时器的概念,在特定的时刻发送消息,根据消息处理相应的事件。update()sleep()间接的相互调用就构成了一个循环。这里要注意:mRedrawHandle绑定的是Avtivity所在的线程,也就是程序的主线程;另外由于sleep()是个异步函数,所以update()sleep()之间的相互调用才没有构成死循环。

 

最后分析下游戏数据的保存机制,如下:

 

这里考虑了Activity的生命周期:如果用户在游戏期间离开游戏界面,游戏暂停;或者由于内存比较紧张,Android关闭游戏释放内存,那么当用户返回游戏界面的时候恢复到上次离开时的界面。

 

三、源码解析

 

详细解析下源代码,由于代码量不大,以注释的方式列出如下:

1、Snake.java

View Code
 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

View Code
  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(77));
178         mSnakeTrail.add(new Coordinate(67));
179         mSnakeTrail.add(new Coordinate(57));
180         mSnakeTrail.add(new Coordinate(47));
181         mSnakeTrail.add(new Coordinate(37));
182         mSnakeTrail.add(new Coordinate(27));
183 
184         // 新的方向 :北方
185         mNextDirection = NORTH;
186 
187         // 2个随机位置的苹果
188         addRandomApple();
189         addRandomApple();
190 
191         // 移动延迟
192         mMoveDelay = 600;
193         // 初始得分0
194         mScore = 0;
195     }

 

View Code
  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     }

 

View Code
  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     }

 

View Code
  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(11);
 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

View Code
  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(00, 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平台上,并且比较AndroidJ2ME的区别和相通之处,让从事过J2ME开发的朋友对Android开发有个更加直观的认识。

posted @ 2011-03-31 16:13  deaboway  阅读(1553)  评论(6编辑  收藏  举报