【贪吃蛇—Java程序员写Android游戏】系列 3. 用J2ME实现Android的Snake Sample详

本次会详细讲解将Android的Snake Sample移植到J2ME上,从而比较二者的区别和联系。

在《1.Android
SDK
Sample-Snake详解
》中,我们已经详细介绍了Android实现的Snake项目结构,现在我们要将这个项目用J2ME实现。

一、 J2ME vs. Android

Android的UI实用、方便,而且很美观,基本无需改动且定制方便。而J2ME的高级用户界面比较鸡肋,在现在大多数的应用里都看不到,多数稍微复杂点的界面都是手工画,或是用一些开源的高级UI库。接下来我们简单比较下二者的区别,为Snake项目从Android到J2ME的移植做准备。

1. 平台

J2ME

开发平台

Android

操作系统

2. 工程结构

J2ME

res:资源文件

src:源代码

Android

src:源代码

res\drawable:图片

res\raw:声音

res\values:字符串

assets:数据文件

3. 安装包

J2ME

jad,jar

Android

apk

4. 代码结构

J2ME

MIDlet,Canvas,采用继承的方式,只有一个MIDlet,一般只有一个Canvas

Android

Activity,View,采用继承的方式,只有一个Activity,一般只有一个View

5. 入口程序

J2ME

MIDlet类

Android

Activity类

6. 主程序结构

J2ME

package com.deaboway.j2me;

import javax.microedition.midlet.MIDlet;

import javax.microedition.midlet.MIDletStateChangeException;

public class MyMidlet extends MIDlet {

protected void destroyApp(boolean arg0) throws
MIDletStateChangeException {

// TODO Auto-generated method stub

}

protected void pauseApp() {

// TODO Auto-generated method stub

}

protected void startApp() throws MIDletStateChangeException
{

// TODO Auto-generated method stub

}

}

Android

package com.deaboway.android;

import android.app.Activity;

import android.os.Bundle;

public class myActivity extends Activity {

@Override

public void onCreate(Bundle icicle) {

super.onCreate(icicle);

setContentView(R.layout.main);

}

}

7. 生命周期-开始

J2ME

startApp(),活动状态,启动时调用,初始化。

Android

onCreate(),返回时也会调用此方法。

onCreate()后调用onStart(),onStart()后调用onResume(),此时Activity进入运行状态。

8. 生命周期-暂停

J2ME

pauseApp(),暂停状态,如来电时,调用该接口。

Android

onPause()。

9. 生命周期-销毁

J2ME

destroyApp(),销毁状态,退出时调用。

Android

onStop(),程序不可见时调用onDestroy(),程序销毁时调用。

10. 刷新

J2ME

高级UI组件由内部刷新实现。低级UI,canvas中通过调用线程结合repaint()来刷新,让线程不断循环。低级UI架构可以用MVC方式来实现,建议使用二级缓存。

Android

高级UIHandler类通过消息的机制刷新。onDraw()刷新接口,低级UI开发者用线程控制更新,在lockCanvas()和unlockCanvasAndPost()方法之间绘制。

如果去读API,我们可以发现J2ME中Canvas的repaint()与Android中View的invalidate()/postInvalidate()方法实现了相同的功能(连说明文字几乎都一样…),但是invalidate()/postInvalidate()两者却有着区别:invalidate()
只能在UI这个线程里通过调用onDraw(Canvas
canvas)来update屏幕显示,而postInvalidate()是要在non-UI线程里做同样的事情的。这就要求我们做判断,哪个调用是本
线程的,哪个不是,这在做多线程callback的时候尤为重要。而在J2ME中,不管怎样直接调用repaint()就好了。

11. 绘画

J2ME

Displayable类。J2me中所有可显示的组件都是直接或间接的继承了Displayable,直接的是Canvas和Screen。不同的继承导致了低级
UI和高级UI的区别。J2me中现成的UI组件都是直接或者间接继承了Screen。只要调用Display.getDisplay(MIDLet
instan).setCurrrent(Displayable
disp),就可以把组件显示到手机界面上。切换界面的时候也可以使用该接口。

Android

View类。可见的组件直接或者间接继承了android.view.View。通过
Activity.setContentView(View
view)就可以显示在android手机界面上,切换界面的时候也可以使用该接口。如果是直接继承了View而不是Android自带的UI组件,那么
还要自己去实现它的刷新,类似J2me的低级UI组件。

12. 画笔

J2ME

高级UI组件由内部刷新实现。低级UI,canvas中通过调用线程结合repaint()来刷新,让线程不断循环。低级UI架构可以用MVC方式来实现,建议使用二级缓存。

Android

Canvas类,Android绘
制的时候会传入一个参数Paint。该对象表示绘制的风格,比如颜色,字体大小,字体格式等。Android的Canvas不同于J2ME的Canvas,它更像于J2ME的Graphics,用来绘制。

13. 全屏

J2ME

Canvas中SetFullScreenMode()。

Android

getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);requestWindowFeature(Window.FEATURE_NO_TITLE);

14. 获得屏幕尺寸

J2ME

Canvas类的getHeight()和getWidth()

Android

Display d = getWindowManager().getDefaultDisplay();

screenWidth = d.getWidth();

screenHeight = d.getHeight();

15. 可绘区域

J2ME

int clipX = g.getClipX();

int clipY = g.getClipY();

int clipWidth = g.getClipWidth();

int clipHeight = g.getClipHeight();

g.clipRect(x, y, width, height);

g.setClip(clipX, clipY, clipWidth, clipHeight);//释放当前状态

Android

canvas.save();//保存当前状态

canvas.clipRect(x,y, x+width, y+height)

cavnas.resave();//释放当前状态

16. 清屏操作

J2ME

g.setColor(Color.WHITE);

g.fillRect(0,0,getWidth(),getHeight());

Android

// 首先定义paint

Paint paint = new Paint();

// 绘制矩形区域-实心矩形

// 设置颜色

paint.setColor(Color.WHITE);

// 设置样式-填充

paint.setStyle(Style.FILL);

// 绘制一个矩形

canvas.drawRect(new Rect(0, 0, getWidth(), getHeight()),
paint);

17. 双缓冲

J2ME

Image bufImage=Image.createImage(bufWidth, bufHeight);

Graphics bufGraphics=bufImage.getGraphics();

Android

Bitmap carBuffer = Bitmap.createBitmap(bufWidth, bufHeight,
Bitmap.Config.ARGB_4444);

Canvas carGp = new Canvas(carBuffer);

18. 图片类

J2ME

Image类,Image.createImage(path);

Android

BitMap类,BitmapFactory.decodeResource(getResources(),R.drawable.map0);

19. 绘制矩形

J2ME

drawRect的后两个参数为宽度和高度

Android

drawRect的后两个参数为结束点的坐标

20. 按键

J2ME

keyPressed()

keyRepeated()

keyReleased()

Android

onKeyDown()

onKeyUp()

onTracKballEvent()

21. 键值

J2ME

Canvas.LEFT…

Android

KeyEvent.KEYCODE_DPAD_LEFT…

22. 触笔

J2ME

pointerPressed(),pointerReleased(),pointerDragged()

Android

onTouchEvent()

23. 数据存储

J2ME

Record Management System (RMS)

Android

SQLite数据库,SharedPreferences类

24. 连接

J2ME

从Connector打开,可以直接在Connector.Open时设置连接是否可读写,以及超时设置

Android

从URL对象打开,必须使用setDoInput(boolean)和setDoOutput(boolean)方法设置,使用setConnectTimeout(int)不仅可以对连接超时进行设置,还能设置超时时间,参数为0时忽略连接超时

25. 游戏开发包

J2ME

javax.microedition.lcdui.game.*;

GameCanvas/Layer/LayerManager/ Sprite/TiledLayer

Android

无专门针对游戏的开发包。

26. 音效

J2ME

Player s=Manager.createPlayer(InputStream);

s.prepare();//创建

s.start();//播放

s.stop();//暂停

s.stop();//关闭

s.release();//释放

Android

MediaPlayer类处理背景音乐

SoundPool类处理一些简单的音效

27. 显示文本

J2ME

String

Android

TextView类

28. 打印信息

J2ME

System.out.println()

Android

Log类

二、 迁移关键点

1. 基础类和结构

J2ME程序的主体从Activity改变为MIDlet,TileView从View改变为Canvas,相关的方法也需要进行调整,但是主体结构和逻辑还是一致的。此外,有些J2ME不支持的类,需要做对应处理。

资源获取,从xml改为自行获取:

private void initSnakeView() {

// 获取图片资源

try {

imageRED_STAR =
Image.createImage(“/redstar.png”);

imageRED_STAR =
Utils.zoomImage(imageRED_STAR,mTileSize,mTileSize);

imageYELLOW_STAR =
Image.createImage(“/yellowstar.png”);

imageYELLOW_STAR =
Utils.zoomImage(imageYELLOW_STAR,mTileSize,mTileSize);

imageGREEN_STAR =
Image.createImage(“/greenstar.png”);

imageGREEN_STAR =
Utils.zoomImage(imageGREEN_STAR,mTileSize,mTileSize);

} catch(Exception e) {

Log.info(“Create Images: “+e);

}

// 设置贴片图片数组

resetTiles(4);

// 把三种图片存到Bitmap对象数组

loadTile(RED_STAR, imageRED_STAR);

loadTile(YELLOW_STAR, imageYELLOW_STAR);

loadTile(GREEN_STAR, imageGREEN_STAR);

}

ArrayList,用Vector替换掉:

// 坐标数组转整数数组,把Coordinate对象的x y放到一个int数组中——用来保存状态

private String coordVectorToString(Vector cvec) {

int count = cvec.size();

StringBuffer rawArray = new StringBuffer();

for (int index = 0; index < count;
index++) {

Coordinate c = (Coordinate) cvec.elementAt(index);

rawArray.append(c.x+”,”);

rawArray.append(c.y+”,”);

Log.info(“coordVectorToString(),
c.x=”+c.x+”,c.y=”+c.y);

}

Log.info(“coordVectorToString(),
rawArray.toString=”+rawArray);

return rawArray.toString();

}

// 整数数组转坐标数组,把一个int数组中的x y放到Coordinate对象数组中——用来恢复状态

// @J2ME 还是用Vector替换ArrayList

private Vector coordStringToVector(String raw) {

Vector coordArrayList = new Vector();

Log.info(“coordStringToVector(), raw=”+raw);

String[] rawArray = Utils.splitUtil(raw,”,”);

Log.info(“coordStringToVector(),
rawArray.length=”+rawArray.length);

int coordCount = rawArray.length;

for (int index = 0; index <
coordCount; index += 2) {

Coordinate c = new
Coordinate(Integer.parseInt(rawArray[index]),
Integer.parseInt(rawArray[index + 1]));

coordArrayList.addElement(c);

}

return coordArrayList;

}

Bundle,用RMS实现:

package com.deaboway.snake.util;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import javax.microedition.rms.RecordStore;

public class Bundle extends BaseRMS {

private static String[] SECTION = {

“”AL”:”,””DT”:”,

“”ND”:”,””MD”:”,

“”SC”:”,””ST”:”};

private static int LEN = SECTION.length;

private static boolean inited = false;

private static Bundle INSTANCE;

public static void INIT(){

if(inited)return;

inited = true;

INSTANCE = new Bundle();

INSTANCE.loadBundles();

}

public static Bundle getInstance(){

return INSTANCE;

}

private String[] CONTENT = new String[LEN];

private Bundle() {

super(“snake-view”);

}

public void loadBundles() {

try {

this.open();

this.close();

} catch (Exception e) {

try {

this.close();

RecordStore.deleteRecordStore(this.getRMSName());

this.open();

this.close();

} catch (Exception ex) {

}

}

}

public void resetBundles() {

try {

this.close();

RecordStore.deleteRecordStore(this.getRMSName());

this.open();

this.close();

} catch (Exception ex) {

}

}

public void updateBundles() throws Exception {

try {

this.openonly();

updateData();

if (this.getRecordStore() != null)

this.close();

} catch (Exception e) {

throw new Exception(this.getRMSName() + “::updateBundles::” +
e);

}

}

protected void loadData() throws Exception {

try {

byte[] record = this.getRecordStore().getRecord(1);

DataInputStream istream = new DataInputStream(

new ByteArrayInputStream(record, 0, record.length));

String content = istream.readUTF();

int[] start = new int[LEN+1];

for(int i =0;i<LEN;i++){

start[i] = content.indexOf(SECTION[i]);

}

start[LEN]=content.length();

for(int i =0;i<LEN;i++){

CONTENT[i] = content.substring(start[i]+5,start[i+1]);

Log.info(“CONTENT[“+i+”]=”+CONTENT[i]);

}

} catch (Exception e) {

throw new Exception(this.getRMSName() + “::loadData::” + e);

}

}

protected void createDefaultData() throws Exception {

try {

ByteArrayOutputStream bstream = new
ByteArrayOutputStream(12);

DataOutputStream ostream = new DataOutputStream(bstream);

CONTENT[0] = “9,20,9,7”;

CONTENT[1] = “1”;

CONTENT[2] = “1”;

CONTENT[3] = “600”;

CONTENT[4] = “0”;

CONTENT[5] = “7,7,6,7,5,7,4,7,3,7,2,7”;

StringBuffer sb = new StringBuffer();

for(int i=0;i<LEN;i++){

sb.append(SECTION[i]);

sb.append(CONTENT[i]);

}

ostream.writeUTF( sb.toString());

ostream.flush();

ostream.close();

byte[] record = bstream.toByteArray();

this.getRecordStore().addRecord(record, 0, record.length);

} catch (Exception e) {

throw new Exception(this.getRMSName() + “::createDefaultData::”
+ e);

}

}

protected void updateData() throws Exception {

try {

ByteArrayOutputStream bstream = new
ByteArrayOutputStream(12);

DataOutputStream ostream = new DataOutputStream(bstream);

StringBuffer sb = new StringBuffer();

for(int i=0;i<LEN;i++){

sb.append(SECTION[i]);

sb.append(CONTENT[i]);

}

ostream.writeUTF(sb.toString());

ostream.flush();

ostream.close();

byte[] record = bstream.toByteArray();

this.getRecordStore().setRecord(1, record, 0,
record.length);

} catch (Exception e) {

throw new Exception(this.getRMSName() + “::updateData::” +
e);

}

}

public String getValue(int key) {

return CONTENT[key];

}

public void setValue(int key, String value) {

CONTENT[key] = value;

}

}

Log,自己实现Log系统:

package com.deaboway.snake.util;

public class Log{

private static final int FATAL = 0;

private static final int ERROR = 1;

private static final int WARN = 2;

private static final int INFO = 3;

private static final int DEBUG = 4;

private static int LOG_LEVEL = INFO;

public static void info(String string){

if(LOG_LEVEL >= INFO){

System.out.println(“[Deaboway][ INFO] ” + string);

}

}

public static void debug(String string){

if(LOG_LEVEL >= DEBUG){

System.out.println(“[Deaboway][DEBUG] ” + string);

}

}

public static void warn(String string){

if(LOG_LEVEL >= WARN){

System.out.println(“[Deaboway][ WARN] ” + string);

}

}

public static void error(String string){

if(LOG_LEVEL >= ERROR){

System.out.println(“[Deaboway][ERROR] ” + string);

}

}

public static void fatal(String string){

if(LOG_LEVEL >= FATAL){

System.out.println(“[Deaboway][FATAL] ” + string);

}

}

}

2. TileView

(1)用Image替换BitMap,“private Image[] mTileArray;”

(2)private final Paint mPaint = new
Paint();不再需要了。直接在Graphics中drawImage即可

(3)onSizeChanged()
不会被自动调用,需要在构造函数中主动调用,以实现对应功能。onSizeChanged(this.getWidth(),this.getHeight());

(4)最重要的,用paint替换onDraw,呵呵,Canvas的核心啊!失去它你伤不起!!!咱也试试咆哮体!!!!!!

3. SnakeView

(1)J2ME 没有Handler,直接用Thread定期repaint()就OK。这里要啰嗦几句。

熟悉Windows编程的朋友可能知道Windows程序是消息驱动的,并且有全局的消息循环系统。而Android应用程序也是消息驱动的,按道理来说也应该提供消息循环机制。实际上Android中也实现了类似Windows的消息循环机制,它通过Looper、Handler来实现消息循环机制,Android消息循环是针对线程的(每个线程都可以有自己的消息队列和消息循环)。

Android系统中Looper负责管理线程的消息队列和消息循环。Handler的作用是把消息加入特定的(Looper)消息队列中,并分发和处理该消息队列中的消息。构造Handler的时候可以指定一个Looper对象,如果不指定则利用当前线程的Looper创建。

一个Activity中可以创建多个工作线程或者其他的组件,如果这些线程或者组件把他们的消息放入Activity的主线程消息队列,那么该消息就会在主线程中处理了。因为主线程一般负责界面的更新操作,并且Android系统中的weget不是线程安全的,所以这种方式可以很好的实现Android界面更新。在Android系统中这种方式有着广泛的运用。

如果另外一个线程要把消息放入主线程的消息队列,就需要通过Handle对象,只要Handler对象以主线程的Looper创建,那么调用Handler的sendMessage等接口,将会把消息放入队列都将是放入主线程的消息队列。并且将会在Handler主线程中调用该handler的handleMessage接口来处理消息。

之所以Android有这些处理,是因为Android平台来说UI控件都没有设计成为线程安全类型,所以需要引入一些同步的机制来使其刷新。而对于J2ME来说,Thread比较简单,直接匿名创建重写run方法,调用start方法执行即可。或者,也可以从Runnable接口继承。实现如下:

class RefreshHandler extends Thread {

public void run() {

while (true) {

try {

//delay一个延迟时间单位

Thread.sleep(mMoveDelay);

} catch (Exception e) {

e.printStackTrace();

}

// 更新View对象

SnakeView.this.update();

// 强制重绘

SnakeView.this.repaint();

}

}

};

(2)直接使用String代替TextView类,在Canvas的paint()中直接绘制各种提示信息。

(3)在一些地方需要主动调用repaint()进行强制重绘。

其它具体参考源代码。

4. Snake

本类就比较简单了,直接把源代码贴出来如下:

package com.deaboway.snake;

import javax.microedition.lcdui.Display;

import javax.microedition.midlet.MIDlet;

import javax.microedition.midlet.MIDletStateChangeException;

import com.deaboway.snake.util.BaseRMS;

import com.deaboway.snake.util.Bundle;

import com.deaboway.snake.util.Log;

public class Snake extends MIDlet {

public static Display display;

public static SnakeView mSnakeView;

public static MIDlet SNAKE;

public Snake() {

Bundle.INIT();

display=Display.getDisplay(this);

SNAKE = this;

mSnakeView = new SnakeView();

mSnakeView.setTextView(“”);

mSnakeView.setMode(SnakeView.READY);

display.setCurrent(mSnakeView);

}

protected void destroyApp(boolean arg0) throws
MIDletStateChangeException {

mSnakeView.saveState();

}

protected void pauseApp() {

mSnakeView.setMode(SnakeView.PAUSE);

}

protected void startApp() throws MIDletStateChangeException
{

// 检查存贮状态以确定是重新开始还是恢复状态

Log.debug(“startApp(), BaseRMS.FIRST=”+BaseRMS.FIRST);

if (BaseRMS.FIRST) {

// 存储状态为空,说明刚启动可以切换到准备状态

mSnakeView.setMode(SnakeView.READY);

} else {

// 已经保存过,那么就去恢复原有状态

// 恢复状态

if (!mSnakeView.restoreState()) {

// 恢复状态不成功,设置状态为暂停

mSnakeView.setMode(SnakeView.PAUSE);

}

}

}

}

本次就大概介绍这么多,源代码将在下次放出。下次主要讲解源代码的存储和维护,敬请期待。

【贪吃蛇—Java程序员写Android游戏】系列 2. 用J2ME实现Android的Snake Sample预览

为了让大家更好的理解J2ME和Android编程的差别,我用J2ME重新实现了Android的Snake Sample。

下次,我会详细介绍在将Snake从Android移植到J2ME上时,需要特别注意的问题,并对Android和J2ME的区别和联系进行粗略的比较。

本次,暂时把J2ME实现的运行画面列出如下。JAVA的好处就是一次编写多处运行,:-),我的PC上各种模拟器都有,因此在WTK、黑莓、Nokia
N97上都跑了跑。

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>2. <wbr>用J2ME实现Android的Snake <wbr>Sample预览2. 用J2ME实现Android的Snake Sample预览” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 2. 用J2ME实现Android的Snake Sample预览” />

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>2. <wbr>用J2ME实现Android的Snake <wbr>Sample预览2. 用J2ME实现Android的Snake Sample预览” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 2. 用J2ME实现Android的Snake Sample预览” />

【贪吃蛇—Java程序员写Android游戏】系列 <wbr>2. <wbr>用J2ME实现Android的Snake <wbr>Sample预览2. 用J2ME实现Android的Snake Sample预览” TITLE=”【贪吃蛇—Java程序员写Android游戏】系列 2. 用J2ME实现Android的Snake Sample预览” />

可运行的包也给大家一下:

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

注意:向上是开始,确定键(中键、Fire键)是暂停或者退出。

【贪吃蛇—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开发有个更加直观的认识。

php5配置与IIS中isapi筛选器不能加载PHP的解决办法

在WINDOWS 2003 SP2
中安装PHP就遇到了问题,IIS里面ISAPI加载不了,在isapi筛选器里添加PHP后重启IIS就不能访问网站了,打开网页就显示service
unavailable
打开ISAPI筛选器发现PHP是一个向下的红色箭头,说明没有加载。找了很久的原因,最后才发现是权限的问题。

第一步:把php-5.0.0-Win32.zip解压放到C:\php

第二步:php.ini-dist改名为php.ini,查找 ./ 找到第438行改成 extension_dir =
c:\php\ext

第三步:去掉565行;extension=php_mysql.dll前面的”;”注释符号

;extension=php_mcrypt.dll将前面的;号去掉

第四步:复制php.ini到Winnt目录下,再复制php5ts.dll和libmysql.dll到WINNT\system32\中

第五步:打开IIS,添加一个php后缀. (指向c:\php\php5isapi.dll) 
isapi筛选器里添加c:\php\php5isapi.dll

第六步:重新启动IIS

然后将 php安装目录/ext/php_mysql.dll  
php_mcrypt.dll 复制到 “c:/windows/system32/” 下

测试

<?php
echo phpinfo();
?>

isapi筛选器加载不了PHP的原因:

要将C盘的PHP目录的权限加上user,可写可修改,就可以加载

还有重要一点,如果按上面配置,打开PHP文件,出现“无法找到该页,您正在搜索的页面可能已经删除、更名或暂时不可用。”的话,这个记得在IIS里面的WEB服务扩展加上PHP的服务扩展。加一个PHP指向c:\php\php5isapi.dll,并设为允许。