【贪吃蛇—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游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />

 

运行效果如下图:

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />【贪吃蛇—Java程序员写Android游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
 

二、工程结构和类图

 其实Snake的工程蛮简单的,源文件就三个:Snake.java
SnakeView.java TileView.javaSnake类是这个游戏的入口点,TitleView类进行游戏的绘画,SnakeView类则是对游戏控制操作的处理。CoordinateRefreshHandler2个辅助类,也是SnakeView类中的内部类。其中,Coordinate是一个点的坐标(xy),RefreshHandlerRefreshHandler对象绑定某个线程并给它发送消息。如下图:

 

 【贪吃蛇—Java程序员写Android游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />

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

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

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
 

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

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

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>1.Android <wbr>SDK <wbr>Sample-Snake详解1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />

 

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

 

三、源码解析

 

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

1、Snake.java

 

package com.deaboway.snake;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

// 贪吃蛇: 经典游戏,在一个花园中找苹果吃,吃了苹果会变长,速度变快。碰到自己和墙就挂掉。
public class Snake extends Activity {

    private
SnakeView mSnakeView;

    private
static String ICICLE_KEY = “snake-view”;

    
    // 在
activity 第一次创建时被调用
  
 @Override
    public void
onCreate(Bundle savedInstanceState) {

  
   
 super.onCreate(savedInstanceState);
  
   
 setContentView(R.layout.snake_layout);

  
   
 mSnakeView = (SnakeView)
findViewById(R.id.snake);
  
   
 mSnakeView.setTextView((TextView)
findViewById(R.id.text));

  
   
 // 检查存贮状态以确定是重新开始还是恢复状态
  
   
 if (savedInstanceState == null) {
  
   
   
 // 存储状态为空,说明刚启动可以切换到准备状态
  
   
   
 mSnakeView.setMode(SnakeView.READY);
  
   
 } else {
  
   
   
 // 已经保存过,那么就去恢复原有状态
  
   
   
 Bundle map =
savedInstanceState.getBundle(ICICLE_KEY);
  
   
   
 if (map != null) {
  
   
   
   
 // 恢复状态
  
   
   
   
 mSnakeView.restoreState(map);
  
   
   
 } else {
  
   
   
   
 // 设置状态为暂停
  
   
   
   
 mSnakeView.setMode(SnakeView.PAUSE);
  
   
   
 }
  
   
 }
    }

    //
暂停事件被触发时
  
 @Override
    protected
void onPause() {
  
   
 super.onPause();
  
   
 // Pause the game along with the activity
  
   
 mSnakeView.setMode(SnakeView.PAUSE);
    }

    //
状态保存
  
 @Override
    public void
onSaveInstanceState(Bundle outState) {
  
   
 // 存储游戏状态到View里
  
   
 outState.putBundle(ICICLE_KEY,
mSnakeView.saveState());
    }

}

 

2、SnakeView.java

 

package com.deaboway.snake;

import java.util.ArrayList;
import java.util.Random;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.TextView;

public class SnakeView extends TileView {

    private
static final String TAG = “Deaboway”;

    
    //
游戏状态,默认值是准备状态
    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;

    //
游戏中蛇的前进方向,默认值北方
    private int
mDirection = NORTH;
    //
下一步的移动方向,默认值北方
    private int
mNextDirection = NORTH;

    // 游戏方向设定 北
南 东 西
    private
static final int NORTH = 1;
    private
static final int SOUTH = 2;
    private
static final int EAST = 3;
    private
static final int WEST = 4;

    
    //
三种游戏元
    private
static final int RED_STAR = 1;
    private
static final int YELLOW_STAR = 2;
    private
static final int GREEN_STAR = 3;

    
    //
游戏得分
    private long
mScore = 0;

    //
移动延迟
    private long
mMoveDelay = 600;

    
    //
最后一次移动时的毫秒时刻
    private long
mLastMove;

    
    //
显示游戏状态的文本组件
    private
TextView mStatusText;

    
    //
蛇身数组(数组以坐标对象为元素)
    private
ArrayList<Coordinate> mSnakeTrail =
new ArrayList<Coordinate>();

    //
苹果数组(数组以坐标对象为元素)
    private
ArrayList<Coordinate> mAppleList =
new ArrayList<Coordinate>();

    
    // 随机数
    private
static final Random RNG = new Random();

    
    //
创建一个Refresh Handler来产生动画: 通过sleep()来实现
    private
RefreshHandler mRedrawHandler = new RefreshHandler();

    //
一个Handler
    class
RefreshHandler extends Handler {

  
   
 // 处理消息队列
  
   
 @Override
  
   
 public void handleMessage(Message msg) {
  
   
   
 // 更新View对象
  
   
   
 SnakeView.this.update();
  
   
   
 // 强制重绘
  
   
   
 SnakeView.this.invalidate();
  
   
 }

  
   
 // 延迟发送消息
  
   
 public void sleep(long delayMillis) {
  
   
   
 this.removeMessages(0);
  
   
   
 sendMessageDelayed(obtainMessage(0),
delayMillis);
  
   
 }
    };

    
    //
构造函数
    public
SnakeView(Context context, AttributeSet attrs) {
  
   
 super(context, attrs);
  
   
 // 构造时初始化
  
   
 initSnakeView();
    }

    public
SnakeView(Context context, AttributeSet attrs, int defStyle)
{
  
   
 super(context, attrs, defStyle);
  
   
 initSnakeView();
    }

    // 初始化
    private void
initSnakeView() {
  
   
 // 可选焦点
  
   
 setFocusable(true);

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

  
   
 // 设置贴片图片数组
  
   
 resetTiles(4);

  
   
 // 把三种图片存到Bitmap对象数组
  
   
 loadTile(RED_STAR,
r.getDrawable(R.drawable.redstar));
  
   
 loadTile(YELLOW_STAR,
r.getDrawable(R.drawable.yellowstar));
  
   
 loadTile(GREEN_STAR,
r.getDrawable(R.drawable.greenstar));

    }

    //
开始新的游戏——初始化
    private void
initNewGame() {
  
   
 // 清空ArrayList列表
  
   
 mSnakeTrail.clear();
  
   
 mAppleList.clear();

  
   
 // For now we’re just going to load up a short
default eastbound snake
  
   
 // that’s just turned north
  
   
 // 创建蛇身

  
   
 mSnakeTrail.add(new Coordinate(7, 7));
  
   
 mSnakeTrail.add(new Coordinate(6, 7));
  
   
 mSnakeTrail.add(new Coordinate(5, 7));
  
   
 mSnakeTrail.add(new Coordinate(4, 7));
  
   
 mSnakeTrail.add(new Coordinate(3, 7));
  
   
 mSnakeTrail.add(new Coordinate(2, 7));

  
   
 // 新的方向 :北方
  
   
 mNextDirection = NORTH;

  
   
 // 2个随机位置的苹果
  
   
 addRandomApple();
  
   
 addRandomApple();

  
   
 // 移动延迟
  
   
 mMoveDelay = 600;
  
   
 // 初始得分0
  
   
 mScore = 0;
    }

 

3、TileView.java

 

package com.deaboway.snake;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;

// View 变种,用来处理 一组 贴片—— “icons”或其它可绘制的对象
public class TileView extends View {

    

    protected
static int mTileSize;

    //
X轴的贴片数量
    protected
static int mXTileCount;
    //
Y轴的贴片数量
    protected
static int mYTileCount;

    //
X偏移量
    private
static int mXOffset;
    //
Y偏移量
    private
static int mYOffset;

    
    //
贴片图像的图像数组
    private
Bitmap[] mTileArray;

    
    //
保存每个贴片的索引——二维数组
    private
int[][] mTileGrid;

    //
Paint对象(画笔、颜料)
    private
final Paint mPaint = new Paint();

    //
构造函数
    public
TileView(Context context, AttributeSet attrs, int defStyle) {
  
   
 super(context, attrs, defStyle);

  
   
 TypedArray a =
context.obtainStyledAttributes(attrs,
  
   
   
   
 R.styleable.TileView);

  
   
 mTileSize =
a.getInt(R.styleable.TileView_tileSize, 12);

  
   
 a.recycle();
    }

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

  
   
 TypedArray a =
context.obtainStyledAttributes(attrs,
  
   
   
   
 R.styleable.TileView);

  
   
 mTileSize =
a.getInt(R.styleable.TileView_tileSize, 12);

  
   
 a.recycle();
    }

    
    //
设置贴片图片数组
    public void
resetTiles(int tilecount) {
  
   
 mTileArray = new Bitmap[tilecount];
    }

    //
回调:当该View的尺寸改变时调用,在onDraw()方法调用之前就会被调用,所以用来设置一些变量的初始值
    //
在视图大小改变的时候调用,比如说手机由垂直旋转为水平
  
 @Override
    protected
void onSizeChanged(int w, int h, int oldw, int oldh) {

  
   
 // 定义X轴贴片数量
  
   
 mXTileCount = (int) Math.floor(w /
mTileSize);
  
   
 mYTileCount = (int) Math.floor(h /
mTileSize);

  
   
 // X轴偏移量
  
   
 mXOffset = ((w – (mTileSize * mXTileCount)) /
2);

  
   
 // Y轴偏移量
  
   
 mYOffset = ((h – (mTileSize * mYTileCount)) /
2);

  
   
 // 定义贴片的二维数组
  
   
 mTileGrid = new
int[mXTileCount][mYTileCount];

  
   
 // 清空所有贴片
  
   
 clearTiles();
    }

    
    //
给mTileArray这个Bitmap图片数组设置值
    public void
loadTile(int key, Drawable tile) {
  
   
 Bitmap bitmap = Bitmap.createBitmap(mTileSize,
mTileSize,
  
   
   
   
 Bitmap.Config.ARGB_8888);
  
   
 Canvas canvas = new Canvas(bitmap);
  
   
 tile.setBounds(0, 0, mTileSize, mTileSize);
  
   
 // 把一个drawable转成一个Bitmap
  
   
 tile.draw(canvas);
  
   
 // 在数组里存入该Bitmap
  
   
 mTileArray[key] = bitmap;
    }

    
    //
清空所有贴片
    public void
clearTiles() {
  
   
 for (int x = 0; x < mXTileCount;
x++) {
  
   
   
 for (int y = 0; y < mYTileCount;
y++) {
  
   
   
   
 // 全部设置为0
  
   
   
   
 setTile(0, x, y);
  
   
   
 }
  
   
 }
    }

    
    //
给某个贴片位置设置一个状态索引
    public void
setTile(int tileindex, int x, int y) {
  
   
 mTileGrid[x][y] = tileindex;
    }

    // onDraw
在视图需要重画的时候调用,比如说使用invalidate刷新界面上的某个矩形区域
  
 @Override
    public void
onDraw(Canvas canvas) {

  
   
 super.onDraw(canvas);
  
   
 for (int x = 0; x < mXTileCount; x
+= 1) {
  
   
   
 for (int y = 0; y < mYTileCount; y
+= 1) {
  
   
   
   
 // 当索引大于零,也就是不空时
  
   
   
   
 if (mTileGrid[x][y] > 0) {
  
   
   
   
   
 // mTileGrid中不为零时画此贴片
  
   
   
   
   
 canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset + x
  
   
   
   
   
   
   
 * mTileSize, mYOffset + y * mTileSize,
mPaint);
  
   
   
   
 }
  
   
   
 }
  
   
 }

    }
}

 

四、工程文件下载

 为了方便大家阅读,可以到如下地址下载工程源代码:

  http://ishare.iask.sina.com.cn/f/14312223.html

五、小结及下期预告:

 本次详细解析了Android SDK
自带 Sample——Snake的结构和功能。下次将会把这个游戏移植到J2ME平台上,并且比较AndroidJ2ME的区别和相通之处,让从事过J2ME开发的朋友对Android开发有个更加直观的认识。