现在的位置: 首页 > 综合 > 正文

snake与LunarLander源代码分析

2011年07月15日 ⁄ 综合 ⁄ 共 28192字 ⁄ 字号 评论关闭

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

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.             

抱歉!评论已关闭.