【贪吃蛇—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,如下图:

【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第1张图片

运行效果如下图:

【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第2张图片

 

 【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第3张图片

 

二、工程结构和类图

 

其实Snake的工程蛮简单的,源文件就三个:Snake.java SnakeView.java TileView.javaSnake类是这个游戏的入口点,TitleView类进行游戏的绘画,SnakeView类则是对游戏控制操作的处理。CoordinateRefreshHandler2个辅助类,也是SnakeView类中的内部类。其中,Coordinate是一个点的坐标(xy),RefreshHandlerRefreshHandler对象绑定某个线程并给它发送消息。如下图: 【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第4张图片

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

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

 【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第5张图片

 

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

 

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

【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解_第6张图片 

这里考虑了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( 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      }

 

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( 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

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

你可能感兴趣的:(android,sdk)