原创作者: deaboway   阅读:7418次   评论:1条   更新时间:2011-06-09    
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,如下图:



运行效果如下图:





二、工程结构和类图

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



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

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



运行机制

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

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



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

三、源码解析

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

1、 Snake.java

    /** 
     * <p>Title: Snake</p> 
     * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p> 
     * @author Gavin 标注 
     */  
    package com.deaboway.snake;  
    import android.app.Activity;  
    import android.os.Bundle;  
    import android.widget.TextView;  
    /** 
     * Snake: a simple game that everyone can enjoy. 
     *  
     * This is an implementation of the classic Game "Snake", in which you control a 
     * serpent roaming around the garden looking for apples. Be careful, though, 
     * because when you catch one, not only will you become longer, but you'll move 
     * faster. Running into yourself or the walls will end the game. 
     *  
     */  
    // 贪吃蛇: 经典游戏,在一个花园中找苹果吃,吃了苹果会变长,速度变快。碰到自己和墙就挂掉。  
    public class Snake extends Activity {  
        private SnakeView mSnakeView;  
        private static String ICICLE_KEY = "snake-view";  
        /** 
         * Called when Activity is first created. Turns off the title bar, sets up 
         * the content views, and fires up the SnakeView. 
         *  
         */  
        // 在 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

/**
 * <p>Title: Snake</p>
 * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p>
 * @author Gavin 标注
 */

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;

/**
 * SnakeView: implementation of a simple game of Snake
 * 
 * 
 */
public class SnakeView extends TileView {

	private static final String TAG = "Deaboway";

	/**
	 * Current mode of application: READY to run, RUNNING, or you have already
	 * lost. static final ints are used instead of an enum for performance
	 * reasons.
	 */
	// 游戏状态,默认值是准备状态
	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;

	/**
	 * Labels for the drawables that will be loaded into the TileView class
	 */
	// 三种游戏元
	private static final int RED_STAR = 1;
	private static final int YELLOW_STAR = 2;
	private static final int GREEN_STAR = 3;

	/**
	 * mScore: used to track the number of apples captured mMoveDelay: number of
	 * milliseconds between snake movements. This will decrease as apples are
	 * captured.
	 */
	// 游戏得分
	private long mScore = 0;

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

	/**
	 * mLastMove: tracks the absolute time when the snake last moved, and is
	 * used to determine if a move should be made based on mMoveDelay.
	 */
	// 最后一次移动时的毫秒时刻
	private long mLastMove;

	/**
	 * mStatusText: text shows to the user in some run states
	 */
	// 显示游戏状态的文本组件
	private TextView mStatusText;

	/**
	 * 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>();

	/**
	 * Everyone needs a little randomness in their life
	 */
	// 随机数
	private static final Random RNG = new Random();

	/**
	 * Create a simple handler that we can use to cause animation to happen. We
	 * set ourselves as a target and we can use the sleep() function to cause an
	 * update/invalidate to occur at a later date.
	 */
	// 创建一个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);
		}
	};

	/**
	 * Constructs a SnakeView based on inflation from XML
	 * 
	 * @param context
	 * @param attrs
	 */
	// 构造函数
	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;
	}

	/**
	 * Given a ArrayList of coordinates, we need to flatten them into an array
	 * of ints before we can stuff them into a map for flattening and storage.
	 * 
	 * @param cvec
	 *            : a ArrayList of Coordinate objects
	 * @return : a simple array containing the x/y values of the coordinates as
	 *         [x1,y1,x2,y2,x3,y3...]
	 */
	// 坐标数组转整数数组,把Coordinate对象的x y放到一个int数组中——用来保存状态
	private int[] coordArrayListToArray(ArrayList<Coordinate> cvec) {
		int count = cvec.size();
		int[] rawArray = new int[count * 2];
		for (int index = 0; index < count; index++) {
			Coordinate c = cvec.get(index);
			rawArray[2 * index] = c.x;
			rawArray[2 * index + 1] = c.y;
		}
		return rawArray;
	}

	/**
	 * Save game state so that the user does not lose anything if the game
	 * process is killed while we are in the background.
	 * 
	 * @return a Bundle with this view's state
	 */
	// 保存状态
	public Bundle saveState() {

		Bundle map = new Bundle();

		map.putIntArray("mAppleList", coordArrayListToArray(mAppleList));
		map.putInt("mDirection", Integer.valueOf(mDirection));
		map.putInt("mNextDirection", Integer.valueOf(mNextDirection));
		map.putLong("mMoveDelay", Long.valueOf(mMoveDelay));
		map.putLong("mScore", Long.valueOf(mScore));
		map.putIntArray("mSnakeTrail", coordArrayListToArray(mSnakeTrail));

		return map;
	}

	/**
	 * Given a flattened array of ordinate pairs, we reconstitute them into a
	 * ArrayList of Coordinate objects
	 * 
	 * @param rawArray
	 *            : [x1,y1,x2,y2,...]
	 * @return a ArrayList of Coordinates
	 */
	// 整数数组转坐标数组,把一个int数组中的x y放到Coordinate对象数组中——用来恢复状态
	private ArrayList<Coordinate> coordArrayToArrayList(int[] rawArray) {
		ArrayList<Coordinate> coordArrayList = new ArrayList<Coordinate>();

		int coordCount = rawArray.length;
		for (int index = 0; index < coordCount; index += 2) {
			Coordinate c = new Coordinate(rawArray[index], rawArray[index + 1]);
			coordArrayList.add(c);
		}
		return coordArrayList;
	}

	/**
	 * Restore game state if our process is being relaunched
	 * 
	 * @param icicle
	 *            a Bundle containing the game state
	 */
	// 恢复状态
	public void restoreState(Bundle icicle) {

		setMode(PAUSE);

		mAppleList = coordArrayToArrayList(icicle.getIntArray("mAppleList"));
		mDirection = icicle.getInt("mDirection");
		mNextDirection = icicle.getInt("mNextDirection");
		mMoveDelay = icicle.getLong("mMoveDelay");
		mScore = icicle.getLong("mScore");
		mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail"));
	}

	/*
	 * handles key events in the game. Update the direction our snake is
	 * traveling based on the DPAD. Ignore events that would cause the snake to
	 * immediately turn back on itself.
	 * 
	 * (non-Javadoc)
	 * 
	 * @see android.view.View#onKeyDown(int, android.os.KeyEvent)
	 */
	// 监听用户键盘操作,并处理这些操作
	// 按键事件处理,确保贪吃蛇只能90度转向,而不能180度转向
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent msg) {

		// 向上键
		if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
			// 准备状态或者失败状态时
			if (mMode == READY | mMode == LOSE) {
				/*
				 * At the beginning of the game, or the end of a previous one,
				 * we should start a new game.
				 */
				// 初始化游戏
				initNewGame();
				// 设置游戏状态为运行
				setMode(RUNNING);
				// 更新
				update();
				// 返回
				return (true);
			}

			// 暂停状态时
			if (mMode == PAUSE) {
				/*
				 * If the game is merely paused, we should just continue where
				 * we left off.
				 */
				// 设置成运行状态
				setMode(RUNNING);
				update();
				// 返回
				return (true);
			}

			// 如果是运行状态时,如果方向原有方向不是向南,那么方向转向北
			if (mDirection != SOUTH) {
				mNextDirection = NORTH;
			}
			return (true);
		}

		// 向下键
		if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
			// 原方向不是向上时,方向转向南
			if (mDirection != NORTH) {
				mNextDirection = SOUTH;
			}
			// 返回
			return (true);
		}

		// 向左键
		if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
			// 原方向不是向右时,方向转向西
			if (mDirection != EAST) {
				mNextDirection = WEST;
			}
			// 返回
			return (true);
		}

		// 向右键
		if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
			// 原方向不是向左时,方向转向东
			if (mDirection != WEST) {
				mNextDirection = EAST;
			}
			// 返回
			return (true);
		}

		// 按其他键时按原有功能返回
		return super.onKeyDown(keyCode, msg);
	}

	/**
	 * Sets the TextView that will be used to give information (such as "Game
	 * Over" to the user.
	 * 
	 * @param newView
	 */
	// 设置状态显示View
	public void setTextView(TextView newView) {
		mStatusText = newView;
	}

	/**
	 * Updates the current mode of the application (RUNNING or PAUSED or the
	 * like) as well as sets the visibility of textview for notification
	 * 
	 * @param newMode
	 */
	// 设置游戏状态
	public void setMode(int newMode) {

		// 把当前游戏状态存入oldMode
		int oldMode = mMode;
		// 把游戏状态设置为新状态
		mMode = newMode;

		// 如果新状态是运行状态,且原有状态为不运行,那么就开始游戏
		if (newMode == RUNNING & oldMode != RUNNING) {
			// 设置mStatusTextView隐藏
			mStatusText.setVisibility(View.INVISIBLE);
			// 更新
			update();
			return;
		}

		Resources res = getContext().getResources();
		CharSequence str = "";

		// 如果新状态是暂停状态,那么设置文本内容为暂停内容
		if (newMode == PAUSE) {
			str = res.getText(R.string.mode_pause);
		}

		// 如果新状态是准备状态,那么设置文本内容为准备内容
		if (newMode == READY) {
			str = res.getText(R.string.mode_ready);
		}

		// 如果新状态时失败状态,那么设置文本内容为失败内容
		if (newMode == LOSE) {
			// 把上轮的得分显示出来
			str = res.getString(R.string.mode_lose_prefix) + mScore
					+ res.getString(R.string.mode_lose_suffix);
		}

		// 设置文本
		mStatusText.setText(str);
		// 显示该View
		mStatusText.setVisibility(View.VISIBLE);
	}

	/**
	 * Selects a random location within the garden that is not currently covered
	 * by the snake. Currently _could_ go into an infinite loop if the snake
	 * currently fills the garden, but we'll leave discovery of this prize to a
	 * truly excellent snake-player.
	 * 
	 */
	// 添加苹果
	private void addRandomApple() {
		// 新的坐标
		Coordinate newCoord = null;
		// 防止新苹果出席在蛇身下
		boolean found = false;
		// 没有找到合适的苹果,就在循环体内一直循环,直到找到合适的苹果
		while (!found) {
			// 为苹果再找一个坐标,先随机一个X值
			int newX = 1 + RNG.nextInt(mXTileCount - 2);
			// 再随机一个Y值
			int newY = 1 + RNG.nextInt(mYTileCount - 2);
			// 新坐标
			newCoord = new Coordinate(newX, newY);

			// Make sure it's not already under the snake
			// 确保新苹果不在蛇身下,先假设没有发生冲突
			boolean collision = false;

			int snakelength = mSnakeTrail.size();
			// 和蛇占据的所有坐标比较
			for (int index = 0; index < snakelength; index++) {
				// 只要和蛇占据的任何一个坐标相同,即认为发生冲突了
				if (mSnakeTrail.get(index).equals(newCoord)) {
					collision = true;
				}
			}
			// if we're here and there's been no collision, then we have
			// a good location for an apple. Otherwise, we'll circle back
			// and try again
			// 如果有冲突就继续循环,如果没冲突flag的值就是false,那么自然会退出循环,新坐标也就诞生了
			found = !collision;
		}

		if (newCoord == null) {
			Log.e(TAG, "Somehow ended up with a null newCoord!");
		}
		// 生成一个新苹果放在苹果列表中(两个苹果有可能会重合——这时候虽然看到的是一个苹果,但是呢,分数就是两个分数。)
		mAppleList.add(newCoord);
	}

	/**
	 * Handles the basic update loop, checking to see if we are in the running
	 * state, determining if a move should be made, updating the snake's
	 * location.
	 */
	// 更新 各种动作,特别是 贪吃蛇 的位置, 还包括:墙、苹果等的更新
	public void update() {
		// 如果是处于运行状态
		if (mMode == RUNNING) {

			long now = System.currentTimeMillis();

			// 如果当前时间距离最后一次移动的时间超过了延迟时间
			if (now - mLastMove > mMoveDelay) {
				//
				clearTiles();
				updateWalls();
				updateSnake();
				updateApples();
				mLastMove = now;
			}
			// Handler 会话进程sleep一个延迟时间单位
			mRedrawHandler.sleep(mMoveDelay);
		}

	}

	/**
	 * Draws some walls.
	 * 
	 */
	// 更新墙
	private void updateWalls() {
		for (int x = 0; x < mXTileCount; x++) {
			// 给上边线的每个贴片位置设置一个绿色索引标识
			setTile(GREEN_STAR, x, 0);
			// 给下边线的每个贴片位置设置一个绿色索引标识
			setTile(GREEN_STAR, x, mYTileCount - 1);
		}
		for (int y = 1; y < mYTileCount - 1; y++) {
			// 给左边线的每个贴片位置设置一个绿色索引标识
			setTile(GREEN_STAR, 0, y);
			// 给右边线的每个贴片位置设置一个绿色索引标识
			setTile(GREEN_STAR, mXTileCount - 1, y);
		}
	}

	/**
	 * Draws some apples.
	 * 
	 */
	// 更新苹果
	private void updateApples() {
		for (Coordinate c : mAppleList) {
			setTile(YELLOW_STAR, c.x, c.y);
		}
	}

	/**
	 * Figure out which way the snake is going, see if he's run into anything
	 * (the walls, himself, or an apple). If he's not going to die, we then add
	 * to the front and subtract from the rear in order to simulate motion. If
	 * we want to grow him, we don't subtract from the rear.
	 * 
	 */
	// 更新蛇
	private void updateSnake() {
		// 生长标志
		boolean growSnake = false;

		// 得到蛇头坐标
		Coordinate head = mSnakeTrail.get(0);
		// 初始化一个新的蛇头坐标
		Coordinate newHead = new Coordinate(1, 1);

		// 当前方向改成新的方向
		mDirection = mNextDirection;

		// 根据方向确定蛇头新坐标
		switch (mDirection) {
		// 如果方向向东(右),那么X加1
		case EAST: {
			newHead = new Coordinate(head.x + 1, head.y);
			break;
		}
			// 如果方向向西(左),那么X减1
		case WEST: {
			newHead = new Coordinate(head.x - 1, head.y);
			break;
		}
			// 如果方向向北(上),那么Y减1
		case NORTH: {
			newHead = new Coordinate(head.x, head.y - 1);
			break;
		}
			// 如果方向向南(下),那么Y加1
		case SOUTH: {
			newHead = new Coordinate(head.x, head.y + 1);
			break;
		}
		}

		// 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)) {
			// 设置游戏状态为Lose
			setMode(LOSE);
			// 返回
			return;

		}

		// 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)) {
				// 设置游戏状态为Lose
				setMode(LOSE);
				// 返回
				return;
			}
		}

		// 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++;
				// 延迟是以前的90%
				mMoveDelay *= 0.9;
				// 蛇增长标志改为真
				growSnake = true;
			}
		}

		// push a new head onto the ArrayList and pull off the tail
		// 在蛇头的位置增加一个新坐标
		mSnakeTrail.add(0, newHead);
		// except if we want the snake to grow
		// 如果没有增长
		if (!growSnake) {
			// 如果蛇头没增长则删去最后一个坐标,相当于蛇向前走了一步
			mSnakeTrail.remove(mSnakeTrail.size() - 1);
		}

		int index = 0;
		// 重新设置一下颜色,蛇头是黄色的(同苹果一样),蛇身是红色的
		for (Coordinate c : mSnakeTrail) {
			if (index == 0) {
				setTile(YELLOW_STAR, c.x, c.y);
			} else {
				setTile(RED_STAR, c.x, c.y);
			}
			index++;
		}

	}

	/**
	 * Simple class containing two integer values and a comparison function.
	 * There's probably something I should use instead, but this was quick and
	 * easy to build.
	 * 
	 */
	// 坐标内部类——原作者说这是临时做法
	private class Coordinate {
		public int x;
		public int y;

		// 构造函数
		public Coordinate(int newX, int newY) {
			x = newX;
			y = newY;
		}

		// 重写equals
		public boolean equals(Coordinate other) {
			if (x == other.x && y == other.y) {
				return true;
			}
			return false;
		}

		// 重写toString
		@Override
		public String toString() {
			return "Coordinate: [" + x + "," + y + "]";
		}
	}

}


3、 TileView.java

    /** 
     * <p>Title: Snake</p> 
     * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p> 
     * @author Gavin 标注 
     */  
    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;  
    /** 
     * TileView: a View-variant designed for handling arrays of "icons" or other 
     * drawables. 
     *  
     */  
    // View 变种,用来处理 一组 贴片—— “icons”或其它可绘制的对象  
    public class TileView extends View {  
        /** 
         * Parameters controlling the size of the tiles and their range within view. 
         * Width/Height are in pixels, and Drawables will be scaled to fit to these 
         * dimensions. X/Y Tile Counts are the number of tiles that will be drawn. 
         */  
        protected static int mTileSize;  
        // X轴的贴片数量  
        protected static int mXTileCount;  
        // Y轴的贴片数量  
        protected static int mYTileCount;  
        // X偏移量  
        private static int mXOffset;  
        // Y偏移量  
        private static int mYOffset;  
        /** 
         * A hash that maps integer handles specified by the subclasser to the 
         * drawable that will be used for that reference 
         */  
        // 贴片图像的图像数组  
        private Bitmap[] mTileArray;  
        /** 
         * A two-dimensional array of integers in which the number represents the 
         * index of the tile that should be drawn at that locations 
         */  
        // 保存每个贴片的索引——二维数组  
        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();  
        }  
        /** 
         * Rests the internal array of Bitmaps used for drawing tiles, and sets the 
         * maximum index of tiles to be inserted 
         *  
         * @param tilecount 
         */  
        // 设置贴片图片数组  
        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();  
        }  
        /** 
         * Function to set the specified Drawable as the tile for a particular 
         * integer key. 
         *  
         * @param key 
         * @param tile 
         */  
        // 给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;  
        }  
        /** 
         * Resets all tiles to 0 (empty) 
         *  
         */  
        // 清空所有贴片  
        public void clearTiles() {  
            for (int x = 0; x < mXTileCount; x++) {  
                for (int y = 0; y < mYTileCount; y++) {  
                    // 全部设置为0  
                    setTile(0, x, y);  
                }  
            }  
        }  
        /** 
         * Used to indicate that a particular tile (set with loadTile and referenced 
         * by an integer) should be drawn at the given x/y coordinates during the 
         * next invalidate/draw cycle. 
         *  
         * @param tileindex 
         * @param x 
         * @param 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://download.csdn.net/source/3145349

五、小结及下期预告:

本次详细解析了 Android SDK 自带 Sample—— Snake的结构和功能。下次将会把这个游戏移植到 J2ME平台上,并且比较 Android和 J2ME的区别和相通之处,让从事过 J2ME开发的朋友对 Android开发有个更加直观的认识。
评论 共 1 条 请登录后发表评论
1 楼 Mr__fang 2011-12-23 12:17

发表评论

您还没有登录,请您登录后再发表评论

文章信息

  • deaboway在2011-06-09创建
  • deaboway在2011-06-09更新
  • 标签: android, 安卓, 移动开发, 游戏开发, snake, 贪吃蛇
Global site tag (gtag.js) - Google Analytics