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,如下图:
1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
运行效果如下图:
1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” /> 1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
二、工程结构和类图
其实 Snake的工程蛮简单的,源文件就三个: Snake.java SnakeView.java TileView.java。 Snake类是这个游戏的入口点, TitleView类进行游戏的绘画, SnakeView类则是对游戏控制操作的处理。 Coordinate, RefreshHandler是 2个辅助类,也是 SnakeView类中的内部类。其中, Coordinate是一个点的坐标( x, y), RefreshHandler将 RefreshHandler对象绑定某个线程并给它发送消息。如下图:
1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
任何游戏都需要有个引擎来推动游戏的运行,最简化的游戏引擎就是:在一个线程中 While循环,检测用户操作,对用户的操作作出反应,更新游戏的界面,直到用户退出游戏。
在 Snake这个游戏中,辅助类 RefreshHandler继承自 Handler,用来把 RefreshHandler与当前线程进行绑定,从而可以直接给线程发送消息并处理消息。注意一点: Handle对消息的处理都是异步。 RefreshHandler在 Handler的基础上增加 sleep()接口,用来每隔一个时间段后给当前线程发送一个消息。 handleMessage()方法在接受消息后,根据当前的游戏状态重绘界面,运行机制如下:
1.Android SDK Sample-Snake详解” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解” />
这比较类似定时器的概念,在特定的时刻发送消息,根据消息处理相应的事件。 update()与 sleep()间接的相互调用就构成了一个循环。这里要注意: mRedrawHandle绑定的是 Avtivity所在的线程,也就是程序的主线程;另外由于 sleep()是个异步函数,所以 update()与 sleep()之间的相互调用才没有构成死循环。
最后分析下游戏数据的保存机制,如下:
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平台上,并且比较 Android和 J2ME的区别和相通之处,让从事过 J2ME开发的朋友对 Android开发有个更加直观的认识。
欢迎关注我的微信公众号:
如无特殊说明,文章均为本站原创,转载请注明出处!
谢谢!
拭目以待,更多的惊喜奥~