• snake与LunarLander源代码分析


    这个一个每个人都喜欢的简单的小游戏

    Snake是游戏的实现类,通过控制小蛇在花园中游走寻找苹果,注意,每吃掉一个苹果,小蛇身体不但会变的更长,还会移动的更敏捷,一旦撞上四周的墙或是碰到自己就会结束这次游戏。

    代码结构分析:

    Snake            : 主游戏窗口

    SnakeView     : 游戏视图类,是实现游戏的主体类

    TileView        : 一个处理图片或其它

    Coordinate   :这是一个包括两个参数,用于记录X轴和Y轴简单类,其中包括一个比较函数.

    RefshHandler :用于更新视图

    Snake

           这个类是游戏的主游戏窗口,是框架容器,

    1. 游戏的开始:oncreate此外的亮点是:setContentView(R.layout.snake_layout);设置窗口的布局文件,这里Android123给大家说明的是,这里 的snake_layout使用了自定义资源标签的方式,大家注意学习:这里我们可以看到来自SnakeView这个派生类的名称,由于Android内 部的R.资源不包含SnakeView类,所以我们必须写清楚Package,比如 com.exmple.android.snake.SnakeView 然后和其他控件使用一样,都是一个id然后宽度、高度、以及自定义的标签tileSize(尾巴长度),如下:

     <com.example.android.snake.SnakeView

         android:id="@+id/snake"

           android:layout_width="fill_parent"

                    android:layout_height="fill_parent"

                    tileSize="12"

                    />

    1. onPause:关于这点,大家可以参考下在我blog中关于active生命周期http://xusaomaiss.javaeye.com/admin/blogs/379826

    在玩游戏过程中,如果有来电或是其它事件中断,这时应该把当前状态保存。以便返回时,还可以继续玩游戏。这就使用onSaveInstanceState实现保存当前状态。

     

    TileView

    注:此部分解析来自: Android示例程序Snake贪食蛇代码分析(三)

    TileView,从名称上不难看出这是一个方砖类,就是生成一个方块。 TileView使用了Android平台的显示基类View,View类是直接从java.lang.Object派生出来的,是各种控件比如 TextView、EditView的基类,当然包括我们的窗口Activity类,这些在SDK文档中都说的比较清楚。

      这里定义了 5个int型全局的变量,分别是方砖的数量mTileSize;方砖水平x防线的数量mXTileCount;以及竖直y方向上的方砖数量 mYTileCount,下面是一个相对偏移位置mXOffsetmYOffset;这里主让要大家了解如何自定义View在 Android开发中,在一个View类中主要是重写onSizeChanged方法来控制改变部分,以及onDraw实现画布的修改,实现的简写如下:

    @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {}
    @Override
        public void onDraw(Canvas canvas) { super.onDraw(canvas);}
      我们自定义的TileView类需要自己添加一个构造方法,根据需要,我们还重载了一种包含样式的方法,这里大家可以多看下Gallery控件的实现,就好理解了,下面是基本框架。
    public TileView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}
    public TileView(Context context, AttributeSet attrs) { super(context, attrs);}
      在贪食蛇游戏中我们知道Snake是移动的,所以加入了一个清除显示的clearTiles方法,通过一个二维数组保存一个gird网格型的运动轨迹,下一次我们将会讲解android贪食蛇的游戏逻辑和完整的关联拼接实现。

     

    SnakeView

    在这个类中实现的游戏的实体,从游戏需求的角色,这个游戏包括了如下方面:

    1.  随机产生小苹果,apples这里是复数,当然是是大于1个苹果,所以代码中产生了两个苹果。

    2.  游戏状态管理

    3.  画蛇,view的更新

    4.  吃掉苹果后小蛇状态的变化

    5.  画围墙

     

    如果实现吃掉苹果小蛇速度变快?

     

    关键是:mMoveDelay这个变量,以下是涉及到这个变量的函数,

     

    每次吃掉苹果后,就会updateSnake一下,里面就把时间处理了:mMoveDelay *= 0.9;

     

    小蛇其实就是一个数组,google的代码就是好注释写的清楚:

    /**

         * mSnakeTrail: a list of Coordinates that make up the snake's body

         * mAppleList: the secret location of the juicy apples the snake craves.

         */

    private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();

    private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();

    mSnakeTrail:一个由Coordinates列表组织的蛇身.

    mAppleList:存放鲜美多汁的苹果列表

    通过这个数组画出小蛇不难,问题是如何判断游戏是否结束?

     

    问题是如何判断游戏的状态

     

    所有以下的代码来自updateSnake

    1.  吃了苹果

    // Look for apples

            int applecount = mAppleList.size();

            for (int appleindex = 0; appleindex < applecount; appleindex++) {

                Coordinate c = mAppleList.get(appleindex);

                if (c.equals(newHead)) {

                    mAppleList.remove(c);

                    addRandomApple();

                   

                    mScore++;

                    mMoveDelay *= 0.9;

                    growSnake = true;

                }

            }

    2.  碰到了自己

    // Look for collisions with itself

            int snakelength = mSnakeTrail.size();

            for (int snakeindex = 0; snakeindex < snakelength; snakeindex++) {

                Coordinate c = mSnakeTrail.get(snakeindex);

                if (c.equals(newHead)) {

                    setMode(LOSE);

                    return;

                }

            }

    3.碰到墙了

    // Collision detection

            // For now we have a 1-square wall around the entire arena

     if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2)

                    || (newHead.y > mYTileCount - 2)) {

                setMode(LOSE);

                return;

     

            }

     

    源代码分析

    Snake状态分析:

    在snakeView中定义了snake游戏的几种状态:

        private int mMode = READY;     

        public static final int PAUSE = 0;  //暂定

        public static final int READY = 1;  //准备好了

        public static final int RUNNING = 2;//正在运行

    public static final int LOSE = 3;  //结束,输了游戏

    各种游戏状态

     rady  running

    pausedlose

    以上状态是通过:void setMode(int newMode)函数实现。

    如何实现画出小方块:

    参看:http://yuefeng.javaeye.com/blog/206706

     

    public class DrawView extends View {

       

        private final int mTileSize = 12;

       

        private final String TAG="DEMO";

       

        private Paint pa = new Paint();

     

        private Bitmap mTileArray;

     

        void loadImage(){

           Resources r = this.getContext().getResources();

          

           Drawable tile = r.getDrawable(R.drawable.redstar);

          

           Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize,

                  Bitmap.Config.ARGB_8888);

           Canvas canvas = new Canvas(bitmap);

           tile.setBounds(0, 0, mTileSize, mTileSize);

           tile.draw(canvas);

     

           mTileArray = bitmap;

          

          

        }

    public DrawView(Context context, AttributeSet attrs, int defStyle) {

           super(context, attrs, defStyle);

           // TODO Auto-generated constructor stub

           loadImage();

           x = 10;

           y = 10;

           Log.i(TAG, "DrawView 2");

        }

       

    //如果没有这段代码,大家可以试一下,改用上面的代码,程序能否通过。

        public DrawView(Context context, AttributeSet attrs) {

           super(context, attrs);

           // TODO Auto-generated constructor stub

           loadImage();

           Log.i(TAG, "DrawView 3");

       

        }

        @Override

        protected void onDraw(Canvas canvas) {

           super.onDraw(canvas);

           Log.i(TAG, "onDraw 1");

           canvas.drawBitmap(mTileArray, x, y, pa);

        }

    }

    通过上面的文章可以画出小方块,但注意到SnakeView一共有两构造函数,那个函数才真正起作用呢?

    public SnakeView(Context context, AttributeSet attrs)

    public SnakeView(Context context, AttributeSet attrs, int defStyle)

    通过加log的方式,判断是第一个构造函数起作用。

    在第一个构造函数上方有一段注释:通过XML文件构造出SnakeView

         * Constructs a SnakeView based on inflation from XML

    如果不使用这个构造函数,将会造成错误,可以试一下,看一下结果是怎样!本人得到如下的错误提示:

    05-21 14:13:26.079: ERROR/AndroidRuntime(711): Caused by: java.lang.NoSuchMethodException: DrawView

    按键处理:

    public boolean onKeyDown(int keyCode, KeyEvent event) {

           // TODO Auto-generated method stub

            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {

                Log.i(TAG, "KEYCODE_DPAD_UP");       

            } 

           return super.onKeyDown(keyCode, event);      

        }

    如何让我们的小方块动起来?

    实现小方块动起来的秘密在于view的public void invalidate ()

    大家可以参看SDK文档中关于View中Drawing中的一小段话

    To force a view to draw, call invalidate().//为了让view重画,可以调用invalidate函数

    方法:

    在DrawView类中添加两个成员:

        private int x,y;

    同时实现get,set方法,

    在构造函数中添加他们的初始值,

    修改onDraw

    @Override

        protected void onDraw(Canvas canvas) {

           super.onDraw(canvas);

           Log.i(TAG, "onDraw 1");

           canvas.drawBitmap(mTileArray, x, y, pa);

        }

    4.修改onKeyDown函数

    @Override

        public boolean onKeyDown(int keyCode, KeyEvent event) {

           // TODO Auto-generated method stub

            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {

                Log.i(TAG, "KEYCODE_DPAD_UP");

                dv.setX(dv.getX()+10);

                dv.invalidate();

            } 

           return super.onKeyDown(keyCode, event);      

        }

     

    最后运行结果如下图:

     

     

     

     

     

    附:网络上关于snake分析的三篇文章

     

    上一次我们大概讲解了下Android SDK中的演示程序Snake游戏的主框架,今天我看来看下实现的基础类TileView,从名称上不难看出这是一个方砖类,就是生成一个方块。 TileView使用了Android平台的显示基类View,View类是直接从java.lang.Object派生出来的,是各种控件比如 TextView、EditView的基类,当然包括我们的窗口Activity类,这些在SDK文档中都说的比较清楚。

      这里定义了 5个int型全局的变量,分别是方砖的数量mTileSize;方砖水平x防线的数量mXTileCount;以及竖直y方向上的方砖数量 mYTileCount,下面是一个相对偏移位置mXOffset和mYOffset;这里android123主让要大家了解如何自定义View在 Android开发中,在一个View类中主要是重写onSizeChanged方法来控制改变部分,以及onDraw实现画布的修改,实现的简写如下:

    @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {}


    @Override
        public void onDraw(Canvas canvas) { super.onDraw(canvas);}

      我们自定义的TileView类需要自己添加一个构造方法,根据需要,我们还重载了一种包含样式的方法,这里大家可以多看下Gallery控件的实现,就好理解了,下面是基本框架。
    public TileView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}

    public TileView(Context context, AttributeSet attrs) { super(context, attrs);}

      在贪食蛇游戏中我们知道Snake是移动的,所以加入了一个清除显示的clearTiles方法,通过一个二维数组保存一个gird网格型的运动轨迹,下一次我们将会讲解android贪食蛇的游戏逻辑和完整的关联拼接实现。

     

    今天我们分析下最复杂的SnakeView的设计,它是派生于TileView方砖类,TileView构建是基于Android直接的显示类View,如果不明白的可以查看Android示例程序Snake贪食蛇代码分析(二)一文有关TileView类的实现, 首先我们看到整个游戏分 READY、PAUSE 、RUNNING 、LOSE四种mMode状态模式,分别对应准备、暂停、运行中、结束(死亡),毕竟贪食蛇没有胜利这个结果。

      整个Snake的运行分4个方向,NORTH、SOUTH 、EAST、WEST分别对应了北、南、东、西四个方向,其中变量mDirection对应当 前的方向,而mNextDirection对应下个运行时的位置。这里星星分3种,使用的是一个Drawable图片,分RED_STAR、 YELLOW_STAR和GREEN_STAR三种颜色,游戏的星星出现位置由Random随机数生成器来决定,这里Random一般和Timer系统时 钟来随机生成更真实一些,通过一个Handler对象来控制画面的更新,使用了this.update();和this.invalidate();这两 个本地方法,Update和invaildate均为android.view.View类的本地方法。这里资源的使用通过Resources r = this.getContext().getResources();获取了r对象的实例,通过 r.getDrawable(R.drawable.redstar)获取资源名为redstar的资源,返回的是一个Drawable对象。

      对于按键信息,直接重写View类的onKeyDown方法,这里KeyEvent传递的是按键的映射,比如KEYCODE_DPAD_UP向上,KeyEvent.KEYCODE_DPAD_DOWN向下等等,详细的查看SDK中的onKeyDown

      @Override
        public boolean onKeyDown(int keyCode, KeyEvent msg) {

            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {}

      }

      整个游戏的控制流程就是上面这些,对于游戏的逻辑而言比较简单,这个贪食蛇并没有包含3D设计和类似Nokia的能量走廊、6边形轨迹,有空了我们一起来完善一个3D的贪食蛇游戏

    下面这篇文章来自:http://www.jizhuomi.com/android/example/177.html

    贪吃蛇剖析(一:暂停/继续、穿墙和全屏) 

           本文开始将为大家剖析Android示例程序-Snake贪吃蛇。贪吃蛇游戏大部分人都玩过,它是怎样实现的呢?Android示例程序给出了代码,下面进行详细分析。

           游戏暂停/继续机制

           由于原来的代码中在游戏运行时没有提供控制选项(比如暂停/继续),因此除非你死了,否则只能玩到底。我这里对代码进行一些修改,加入一个Option Menu来提供暂停/继续机制。

           首先加入一个变量记录游戏当前状态:

           private int mState = SnakeView.READY;

           然后重载onCreateOptionsMenu函数,创建一个控制菜单项,并对其进行处理,提供暂停/继续机制。

    Java代码

    1. /*     
    2.  * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)     
    3.  * @Author:phinecos     
    4.  * @Date:2009-08-28     
    5.  */      
    6. @Override      
    7. public boolean onOptionsItemSelected(MenuItem item)      
    8. {      
    9.     switch (item.getItemId())      
    10.     {      
    11.         case MENU_CONTROL:      
    12.         {      
    13.             if (mState == SnakeView.PAUSE)      
    14.             {//此前状态是"停止",则转为"运行"      
    15.                 mState = SnakeView.RUNNING;      
    16.                 mSnakeView.setMode(SnakeView.RUNNING);      
    17.                     item.setIcon(android.R.drawable.ic_media_pause).setTitle(R.string.cmd_pause);      
    18.             }      
    19.             else if(mState == SnakeView.RUNNING)      
    20.             {//此前状态是"运行",则转为“暂停"      
    21.                 mState = SnakeView.PAUSE;      
    22.                 mSnakeView.setMode(SnakeView.PAUSE);      
    23.                     item.setIcon(android.R.drawable.ic_media_play).setTitle(R.string.cmd_run);      
    24.             }      
    25.             else if(mState == SnakeView.READY)      
    26.             {//此前是"初始状态",则转为"运行"      
    27.                 mState = SnakeView.RUNNING;      
    28.             }      
    29.     return true;      
    30.         }      
    31.     }      
    32.     return super.onOptionsItemSelected(item);      
    33. }      
    34. /*     
    35.  * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)     
    36.  * @Author:phinecos     
    37.  * @Date:2009-08-28     
    38.  */      
    39. @Override      
    40. public boolean onCreateOptionsMenu(Menu menu)       
    41. {      
    42.      super.onCreateOptionsMenu(menu);      
    43.      menu.add(0, MENU_CONTROL, 0, R.string.cmd_pause).setIcon(android.R.drawable.ic_media_pause);      
    44.      return true;      
    45. }  

           修改后运行截图如下:

     

     

           当然,这段代码还是有问题的,游戏刚开始时,必须先点击菜单确认,再按上方向键才能开始。(以后再来修改。。。)

        穿墙贪食蛇

           第二个修改是把这个普通的贪食蛇改成可以穿墙(呵呵,这样就可以不死了。。。)。想必要修改的就是撞墙检测那段代码?没错,就是下面这段!

    Java代码

    1. // Collision detection   
    2. // For now we have a 1-square wall around the entire arena   
    3. if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2)|| (newHead.y > mYTileCount - 2))    
    4. {//撞墙   
    5.      setMode(LOSE);   
    6.      return;   
    7. }  

           原来的版本是发现撞墙时就直接判定为失败,我这里做个小小的修改,让它可以穿墙而去:

    Java代码

    1. private void updateSnake()   
    2. {   
    3.      boolean growSnake = false;   
    4.   
    5.      // grab the snake by the head   
    6.      Coordinate head = mSnakeTrail.get(0);   
    7.      Coordinate newHead = new Coordinate(1, 1);   
    8.   
    9.      mDirection = mNextDirection;   
    10.   
    11.      switch (mDirection)    
    12.      {   
    13.      case EAST:    
    14.      {   
    15.          newHead = new Coordinate(head.x + 1, head.y);   
    16.          break;   
    17.      }   
    18.      case WEST:    
    19.      {   
    20.          newHead = new Coordinate(head.x - 1, head.y);   
    21.          break;   
    22.      }   
    23.      case NORTH:    
    24.      {   
    25.          newHead = new Coordinate(head.x, head.y - 1);   
    26.          break;   
    27.      }   
    28.      case SOUTH:    
    29.      {   
    30.          newHead = new Coordinate(head.x, head.y + 1);   
    31.          break;   
    32.      }   
    33.      }   
    34.   
    35.      //穿墙的处理   
    36.       if (newHead.x == 0)   
    37.      {//穿左边的墙   
    38.          newHead.x = mXTileCount - 2;   
    39.      }   
    40.      else if (newHead.y == 0)   
    41.      {//穿上面的墙   
    42.          newHead.y = mYTileCount - 2;   
    43.      }   
    44.      else if (newHead.x == mXTileCount - 1)   
    45.      {//穿右边的墙   
    46.          newHead.x = 1;   
    47.      }   
    48.      else if (newHead.y == mYTileCount - 1)   
    49.      {//穿下面的墙   
    50.          newHead.y = 1;   
    51.      }   
    52.      // 判断是否撞到自己   
    53.       int snakelength = mSnakeTrail.size();   
    54.      for (int snakeindex = 0; snakeindex < snakelength; snakeindex++)    
    55.      {   
    56.          Coordinate c = mSnakeTrail.get(snakeindex);   
    57.          if (c.equals(newHead))   
    58.          {   
    59.              setMode(LOSE);   
    60.              return;   
    61.          }   
    62.      }   
    63.      // 判断是否吃掉“苹果”   
    64.       int applecount = mAppleList.size();   
    65.      for (int appleindex = 0; appleindex < applecount; appleindex++)   
    66.      {   
    67.          Coordinate c = mAppleList.get(appleindex);   
    68.          if (c.equals(newHead))    
    69.          {   
    70.              mAppleList.remove(c);   
    71.              addRandomApple();   
    72.              mScore++;   
    73.              mMoveDelay *= 0.9;   
    74.              growSnake = true;   
    75.          }   
    76.      }   
    77.      // push a new head onto the ArrayList and pull off the tail   
    78.      mSnakeTrail.add(0, newHead);   
    79.      // except if we want the snake to grow   
    80.      if (!growSnake)    
    81.      {   
    82.          mSnakeTrail.remove(mSnakeTrail.size() - 1);   
    83.      }   
    84.      int index = 0;   
    85.      for (Coordinate c : mSnakeTrail)    
    86.      {   
    87.          if (index == 0)   
    88.          {   
    89.              setTile(YELLOW_STAR, c.x, c.y);   
    90.          }    
    91.          else    
    92.          {   
    93.              setTile(RED_STAR, c.x, c.y);   
    94.          }   
    95.          index++;   
    96.      }   
    97. }  

           其实修改后的代码非常简单,就是把新节点的值做些处理,让它移动到对应的行/列的头部或尾部即可。下面就是修改后的“穿墙”贪食蛇的运行截图:

     

           全屏机制

           游戏一般都是全屏的,原始代码也考虑到标题栏太过难看了,于是使用下面这句代码就去掉了标题栏:

    Java代码

    1. requestWindowFeature(Window.FEATURE_NO_TITLE);  

           可还是没有达到全屏的效果,在Android1.5中实现全屏效果非常简单,只需要一句代码即可实现:

    Java代码

    1. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);  

           运行效果如下图所示:

     

    贪吃蛇剖析(二:FrameLayout与RelativeLayout)

           前一节中将了贪吃蛇Snake游戏的暂停/继续、穿墙和全屏功能的实现,本文继续分析此示例程序中体现的Android Layout机制。

           1、FrameLayout

           先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前 一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。

           有点绕口而且难理解,下面还是通过一个实例来理解吧。我们仿照Snake项目中使用的界面一样,建立一个简单的FrameLayout,其中包含两个Views元素:ImageViewTextView,而后面的TextView还包含在一个RelativeLayout中。

    XML/HTML代码

    1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     android:layout_width="fill_parent"  
    3.     android:layout_height="fill_parent">  
    4.     <ImageView  
    5.         android:layout_width="fill_parent"  
    6.         android:layout_height="fill_parent"    
    7.         android:scaleType="center" android:src="@drawable/img0"/>  
    8. <RelativeLayout  
    9.         android:layout_width="fill_parent"  
    10.         android:layout_height="fill_parent" >  
    11.         <TextView  
    12.             android:text="Hello Android"  
    13.             android:visibility="visible"  
    14.             android:layout_width="wrap_content"  
    15.             android:layout_height="wrap_content"  
    16.             android:layout_centerInParent="true"  
    17.             android:gravity="center_horizontal"  
    18.             android:textColor="#ffffffff"  
    19.             android:textSize="24sp"/>  
    20.     </RelativeLayout>  
    21. </FrameLayout>  

           效果如下图所示:

     

           2、UI优化

           Android的tools目录下提供了许多实用工具,这里介绍其中一个用于查看当前UI结构视图的工具hierarchyviewer。打开tools/hierarchyviewer.bat后,查看上面这个示例的UI结构图可得:

     

           我们可以很明显的看到由红色线框所包含的结构出现了两个framelayout节点,很明显这两个完全意义相同的节点造成了资源浪费(这里可以提醒大家在 开发工程中可以习惯性的通过hierarchyViewer查看当前UI资源的分配情况),那么如何才能解决这种问题呢(就当前例子是如何去掉多余的 frameLayout节点)?这时候就要用到<merge />标签来处理类似的问题了。我们将上边xml代码中的framLayout替换成merge:

    XML/HTML代码

    1. <merge  xmlns:android="http://schemas.android.com/apk/res/android">  
    2.     <ImageView  
    3.         android:layout_width="fill_parent"  
    4.         android:layout_height="fill_parent"    
    5.         android:scaleType="center" android:src="@drawable/img0"/>  
    6. <RelativeLayout  
    7.         android:layout_width="fill_parent"  
    8.         android:layout_height="fill_parent" >  
    9.         <TextView  
    10.             android:text="Hello Android"  
    11.             android:visibility="visible"  
    12.             android:layout_width="wrap_content"  
    13.             android:layout_height="wrap_content"  
    14.             android:layout_centerInParent="true"  
    15.             android:gravity="center_horizontal"  
    16.             android:textColor="#ffffffff"  
    17.             android:textSize="24sp"/>  
    18.     </RelativeLayout>  
    19. </merge >  

           运行程序后在Emulator中显示的效果是一样的,可是通过hierarchyviewer查看的UI结构是有变化的,当初多余的 FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(这里需 要提醒大家注意:所有的Activity视图的根节点都是frameLayout)。如果你所创建的Layout并不是用framLayout作为根节点 (而是应用LinerLayout等定义root标签),就不能应用上边的例子通过merge来优化UI结构。

     

           3、RelativeLayout

       RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式 来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。

       解释起来也比较麻烦,不过我做个对比实验可以明白它的用处了,试着把上面例子里的RelativeLayout节点去掉看看,效果如下图所示,可以看到 由于FrameLayout的原因,都在左上角靠拢了,而使用了RelativeLayout,则可以让TextView相对于屏幕居中。

     

           4、Snake的界面分析

           有了上述Layout的基础知识,我们再来看Snake的布局文件就很好理解了,就是一个SnakeView和一个TextView,启动后,后者会覆盖在前者上面。

    XML/HTML代码

    1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     android:layout_width="fill_parent"  
    3.     android:layout_height="fill_parent">  
    4.     <com.example.android.snake.SnakeView  
    5.      android:id="@+id/snake"  
    6.         android:layout_width="fill_parent"  
    7.                 android:layout_height="fill_parent"  
    8.                 tileSize="24"  
    9.                 />  
    10.     <RelativeLayout  
    11.         android:layout_width="fill_parent"  
    12.         android:layout_height="fill_parent" >  
    13.         <TextView  
    14.          android:id="@+id/text"  
    15.             android:text="@string/snake_layout_text_text"  
    16.             android:visibility="visible"  
    17.             android:layout_width="wrap_content"  
    18.             android:layout_height="wrap_content"  
    19.             android:layout_centerInParent="true"  
    20.             android:gravity="center_horizontal"  
    21.             android:textColor="#ff8888ff"  
    22.             android:textSize="24sp"/>  
    23.     </RelativeLayout>  
    24. </FrameLayout>  

           也就是这样的效果:

     

           那么相应的代码是如何实现这个效果的呢? SnakeView有一个私有变量存放覆盖其上的TextView:

    Java代码

    1. private TextView mStatusText;  

           在Snake这个Activity的onCreate方法中,首先将Layout文件中的SnakeView和TextView关联起来:

    Java代码

    1. setContentView(R.layout.snake_layout);   
    2. mSnakeView = (SnakeView) findViewById(R.id.snake);   
    3. mSnakeView.setTextView((TextView) findViewById(R.id.text));  

          然后设置SnakeView的状态为Ready:

    Java代码

    1. mSnakeView.setMode(SnakeView.READY);  

           这一句代码会调用下述函数:

    Java代码

    1. public void setMode(int newMode)    
    2. {   
    3.         int oldMode = mMode;   
    4.         mMode = newMode;   
    5.         if (newMode == RUNNING & oldMode != RUNNING)    
    6.         {//游戏进入“运行”状态,则隐藏文字信息   
    7.             mStatusText.setVisibility(View.INVISIBLE);   
    8.             update();   
    9.             return;   
    10.         }   
    11.         //根据新状态,设置待显示的文字信息   
    12.         Resources res = getContext().getResources();   
    13.         CharSequence str = "";   
    14.         if (newMode == PAUSE)    
    15.         {//新状态为“暂停”   
    16.             str = res.getText(R.string.mode_pause);   
    17.         }   
    18.         if (newMode == READY)    
    19.         {//新状态为“准备开始”   
    20.             str = res.getText(R.string.mode_ready);   
    21.         }   
    22.         if (newMode == LOSE)    
    23.         {//新状态为“游戏失败”   
    24.             str = res.getString(R.string.mode_lose_prefix) + mScore   
    25.                   + res.getString(R.string.mode_lose_suffix);   
    26.         }   
    27.         //设置文字信息并显示   
    28.         mStatusText.setText(str);   
    29.         mStatusText.setVisibility(View.VISIBLE);   
    30. }  

           在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。

    贪吃蛇剖析(三:界面UI、游戏逻辑和Handler)

           往往我们在程序设计的时候喜欢将界面与处理分开,这样降低耦合性,易于维护扩展。在贪吃蛇Snake这个示例程序中同样将界面UI和游戏逻辑进行了分离, 它的实现方式就是,用父类TileView来实现比较基础的界面UI部分,而TileView类的子类SnakeView类完成了游戏控制逻辑部分,这样 就成功的将两者进行了分离,对后面的扩展和维护奠定了良好的基础。

           界面UI

           首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的 状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。TileView的数据成员如下:

    Java代码

    1. //方格的大小   
    2. protected static int mTileSize;       
    3. //方格的行数和列数   
    4. protected static int mXTileCount;   
    5. protected static int mYTileCount;   
    6. //xy坐标系的偏移量   
    7. private static int mXOffset;   
    8. private static int mYOffset;   
    9. //存储三种方格的图标文件   
    10. private Bitmap[] mTileArray;    
    11. //二维方格地图   
    12. private int[][] mTileGrid;   

           那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。

    Java代码

    1. @Override  
    2. protected void onSizeChanged(int w, int h, int oldw, int oldh)    
    3. {   
    4.         //计算屏幕中可放置的方格的行数和列数   
    5.         mXTileCount = (int) Math.floor(w / mTileSize);   
    6.         mYTileCount = (int) Math.floor(h / mTileSize);   
    7.         mXOffset = ((w - (mTileSize * mXTileCount)) / 2);   
    8.         mYOffset = ((h - (mTileSize * mYTileCount)) / 2);   
    9.         mTileGrid = new int[mXTileCount][mYTileCount];   
    10.         clearTiles();   
    11. }  

           注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应 的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。

      第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。

    Java代码

    1. public void onDraw(Canvas canvas)    
    2. {   
    3.      super.onDraw(canvas);   
    4.      for (int x = 0; x < mXTileCount; x += 1)   
    5.      {   
    6.          for (int y = 0; y < mYTileCount; y += 1)   
    7.          {   
    8.              if (mTileGrid[x][y] > 0)   
    9.              {   
    10.                  canvas.drawBitmap(mTileArray[mTileGrid[x][y]],    
    11.                      mXOffset + x * mTileSize,   
    12.                      mYOffset + y * mTileSize,   
    13.                      mPaint);   
    14.              }   
    15.          }   
    16.      }   
    17. }  

           onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。

      游戏逻辑

      再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。

    Java代码

    1. private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();//组成贪食蛇的方格列表   
    2. private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();//苹果方格列表  

           由于SnakeView从TileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。

    Java代码

    1. private void initNewGame()   
    2.     {   
    3.         //清空蛇和苹果占据的方格   
    4.         mSnakeTrail.clear();   
    5.         mAppleList.clear();   
    6.         //目前组成蛇的方格式固定的,而且方向也固定朝北   
    7.         mSnakeTrail.add(new Coordinate(7, 7));   
    8.         mSnakeTrail.add(new Coordinate(6, 7));   
    9.         mSnakeTrail.add(new Coordinate(5, 7));   
    10.         mSnakeTrail.add(new Coordinate(4, 7));   
    11.         mSnakeTrail.add(new Coordinate(3, 7));   
    12.         mSnakeTrail.add(new Coordinate(2, 7));   
    13.         mNextDirection = NORTH;   
    14.   
    15.         //随即加入苹果   
    16.         for (int i = 0; i < nApples; ++i)   
    17.         {   
    18.             addRandomApple();   
    19.         }   
    20.         //初始化运动速率和玩家成绩   
    21.         mMoveDelay = 600;   
    22.         mScore = 0;   
    23. }  

           想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负 责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器 等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。

           这里要着重说明下private long mMoveDelay = 600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?

           可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个 改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用 onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以 设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的“拍照“的效 果。

      Handler的使用

       写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现 setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用Timer。Snake 中使用了前者。

    Java代码

    1. class RefreshHandler extends Handler    
    2. {   
    3.         @Override  
    4.         public void handleMessage(Message msg)    
    5.         {//“苏醒”后的处理   
    6.            SnakeView.this.update();   
    7.            SnakeView.this.invalidate();   
    8.         }   
    9.         public void sleep(long delayMillis)    
    10.         {//休眠delayMillis毫秒   
    11.             this.removeMessages(0);   
    12.             sendMessageDelayed(obtainMessage(0), delayMillis);   
    13.         }   
    14. };  

           而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在 Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做 “引擎“不为过。

    Java代码

    1. public void update()   
    2. {   
    3.     if (mMode == RUNNING)   
    4.     {   
    5.         long now = System.currentTimeMillis();   
    6.         if (now - mLastMove > mMoveDelay)    
    7.         {   
    8.             clearTiles();   
    9.             updateWalls();   
    10.             updateSnake();   
    11.             updateApples();   
    12.             mLastMove = now;   
    13.         }   
    14.         mRedrawHandler.sleep(mMoveDelay);   
    15.     }   
    16. }  

           既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为 “运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这 一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。

      一点困惑

      可是个人认为Snake下面这段代码读起来有点怪,有点像一个“先有鸡,还是先有蛋?“的问题,导致我的思维逻辑上出现一个“怪圈“。

    Java代码

    1. public void handleMessage(Message msg)    
    2. {   
    3.       SnakeView.this.update();   
    4.       SnakeView.this.invalidate();   
    5. }  

          按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。

       哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6 毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去 呢),更何况在update中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢? 也就是说invalidate根本没有执行???

      按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次 对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的, 不过关于这一个迷惑我的地方,希望有朋友能指点我一下!

           记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?

         此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一 次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。

    LunarLander游戏

           前面有几篇文章写的是对Android示例程序贪吃蛇Snake程序的剖析,本文继续分析Android自带的另一个小游戏LunarLander的程序。在贪吃蛇Snake程序中采用了“定时器+系统调用onDraw”的架构,而LunarLander程序采用的是“多线程+强制自行绘制”的架构思路,比前者更为实用。

           与贪吃蛇Snake程序的对比

           就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来 实现,这里是LunarView。读过贪吃蛇程序剖析文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最 大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可 却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联 产承包制”时代。

           此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不 断发生的事件在进行就可以,例如Snake中使用的定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产 生这种“差异源”的。

      SurfaceView初探

           如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武 器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是 在Canvas(画布)中自行绘制出来的(背景,人物等)。

       SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部 件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操 作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的:The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. 使用的SurfaceView的时候,一般情况下还要对其进行创建、销毁、改变时的情况进行监视,这就要用到 SurfaceHolder.Callback。

    Java代码

    1. class LunarView extends SurfaceView implements SurfaceHolder.Callback   
    2. {   
    3.     public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}   
    4. //在surface的大小发生改变时激发   
    5.     public void surfaceCreated(SurfaceHolder holder){}   
    6. //在创建时激发,一般在这里调用画图的线程。   
    7.     public void surfaceDestroyed(SurfaceHolder holder) {}   
    8. //销毁时激发,一般在这里将画图的线程停止、释放。   
    9. }  

           surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程。

    Java代码

    1. private LunarThread thread; // 实际工作线程   
    2. thread = new LunarThread(holder, context, new Handler() {   
    3.      @Override  
    4.      public void handleMessage(Message m)    
    5.      {   
    6.           mStatusText.setVisibility(m.getData().getInt("viz"));   
    7.           mStatusText.setText(m.getData().getString("text"));   
    8.      }   
    9. });   

       这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在 Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。由于有了 LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。

    Java代码

    1. public void surfaceChanged(SurfaceHolder holder, int format, int width,int height){   
    2.      thread.setSurfaceSize(width, height);   
    3. }   
    4. public void surfaceCreated(SurfaceHolder holder)   
    5. {//启动工作线程结束   
    6.       thread.setRunning(true);   
    7.      thread.start();   
    8. }   
    9. public void surfaceDestroyed(SurfaceHolder holder)   
    10. {   
    11.      boolean retry = true;   
    12.      thread.setRunning(false);   
    13.      while (retry)    
    14.      {   
    15.          try  
    16.          {//等待工作线程结束,主线程才结束   
    17.                thread.join();   
    18.              retry = false;   
    19.          }    
    20.          catch (InterruptedException e)    
    21.          {   
    22.          }   
    23.      }   
    24. }  

      工作线程LunarThread

      由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。

           再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View 的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾Android示例程序剖析之Snake贪吃蛇(三:界面UI、游戏逻辑和Handler)中SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。

    Java代码

    1. public void setState(int mode, CharSequence message)    
    2. {   
    3.      synchronized (mSurfaceHolder)   
    4.      {   
    5.           mMode = mode;   
    6.           if (mMode == STATE_RUNNING)   
    7.           {//运行中,隐藏界面文字信息   
    8.                Message msg = mHandler.obtainMessage();   
    9.                Bundle b = new Bundle();   
    10.                b.putString("text", "");   
    11.                b.putInt("viz", View.INVISIBLE);   
    12.                msg.setData(b);   
    13.                mHandler.sendMessage(msg);   
    14.           }    
    15.           else    
    16.           {//根据当前状态设置文字信息   
    17.                mRotating = 0;   
    18.                mEngineFiring = false;   
    19.                Resources res = mContext.getResources();   
    20.                CharSequence str = "";   
    21.                if (mMode == STATE_READY)   
    22.                   str = res.getText(R.string.mode_ready);   
    23.                else if (mMode == STATE_PAUSE)   
    24.                   str = res.getText(R.string.mode_pause);   
    25.                else if (mMode == STATE_LOSE)   
    26.                   str = res.getText(R.string.mode_lose);   
    27.                else if (mMode == STATE_WIN)   
    28.                   str = res.getString(R.string.mode_win_prefix)   
    29.                                 + mWinsInARow + " "  
    30.                                 + res.getString(R.string.mode_win_suffix);   
    31.                if (message != null) {   
    32.                    str = message + "\n" + str;   
    33.                }   
    34.                if (mMode == STATE_LOSE)    
    35.                     mWinsInARow = 0;   
    36.                Message msg = mHandler.obtainMessage();   
    37.                Bundle b = new Bundle();   
    38.                b.putString("text", str.toString());   
    39.                b.putInt("viz", View.VISIBLE);   
    40.                msg.setData(b);   
    41.                mHandler.sendMessage(msg);   
    42.           }   
    43.      }   
    44. }  

        下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。

    Java代码

    1. public void run()    
    2. {   
    3.       while (mRun)    
    4.       {   
    5.            Canvas c = null;   
    6.            try    
    7.            {   
    8.                //锁定待绘制区域   
    9.                c = mSurfaceHolder.lockCanvas(null);   
    10.                synchronized (mSurfaceHolder)   
    11.                {   
    12.                     if (mMode == STATE_RUNNING)    
    13.                         updatePhysics();//更新底层数据,判断游戏状态   
    14.                         doDraw(c);//强制重绘制   
    15.                   }   
    16.            }    
    17.            finally    
    18.            {   
    19.                 if (c != null) {   
    20.                     mSurfaceHolder.unlockCanvasAndPost(c);   
    21.                 }   
    22.            }   
    23.      }   
    24. }  

           这里要注意的是最后要调用unlockCanvasAndPost来结束锁定画图,并提交改变。

      强制自行绘制

           doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:

    Java代码

    1. canvas.save();   
    2. canvas.rotate((float) mHeading, (float) mX, mCanvasHeight   
    3.                     - (float) mY);   
    4. if (mMode == STATE_LOSE) {   
    5.       mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    6.                         + mLanderHeight);   
    7.       mCrashedImage.draw(canvas);   
    8. } else if (mEngineFiring) {   
    9.       mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    10.                         + mLanderHeight);   
    11.       mFiringImage.draw(canvas);   
    12. } else {   
    13.       mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    14.                         + mLanderHeight);   
    15.       mLanderImage.draw(canvas);   
    16. }   
    17. canvas.restore();   

           在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有 的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。

      暂停/继续机制

           LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为 当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。

    Java代码

    1. public void pause()   
    2. {//暂停   
    3.       synchronized (mSurfaceHolder)    
    4.      {   
    5.           if (mMode == STATE_RUNNING)   
    6.               setState(STATE_PAUSE);   
    7.      }   
    8. }   
    9. public void unpause()   
    10. {// 继续   
    11.       // Move the real time clock up to now   
    12.      synchronized (mSurfaceHolder)   
    13.      {   
    14.            mLastTime = System.currentTimeMillis() + 100;   
    15.      }   
    16.      setState(STATE_RUNNING);   
    17. }  

           这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句:

    Java代码

    1. if (mLastTime > now) return;   
    2. double elapsed = (now - mLastTime) / 1000.0;   

           至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。

  • 相关阅读:
    微信卡券领用中的问题
    abp的开发20180425
    typescript 接口的新认识
    Jquery构建Form表单Post提交数据的简单方法
    EF使用时异常:对一个或多个实体的验证失败。有关详细信息
    VS快捷键简单记录
    比较和排序 IComparable And IComparer
    wpf全局异常
    MailBee的简单使用
    json数据的获取(网络摘抄)
  • 原文地址:https://www.cnblogs.com/olvo/p/3103370.html
Copyright © 2020-2023  润新知