随着通信技术的发展和手机的普及,手机游戏的开发技术越来越为人们所关注。以J2ME为开发平台,利用Java提供强大工具,不但可以在手机上实现静态HTML技术所无法实现的计算处理、数据存储、与服务器的通信等功能,而且能够开发各种手机游戏。本文在介绍J2ME及其体系结构的基础上,以贪吃蛇游戏为实例,描述了借助J2ME的MIDlet类库开发手机游戏的过程。
JAVA自从20世纪90年代早期诞生以来,以其强大的功能迅速为广大程序员们所接受。从2001年开始,日本的国内的移动电话开始搭载Java。使用本来就是一种程序语言的Java之后,就可以在移动电话上完成以往静态的HTML内容所无法达成的计算处理、数据存储、与服务器的通信等等。如果能利用Java上其他丰富功能,那么就可以实现更多的功能。随着手机游戏的发展,国内外各大开发商纷纷运用Java进行手机游戏开发。J2ME作为一种基于Java的便携设备开发平台,在各大手机开发公司得到了广泛应用。
本课题拟研究基于J2ME的手机游戏开发技术以及其特点,具体研究内容如下:
1、手机游戏开发以及J2ME的基本理论和J2ME类库的使用;
2、J2ME体系结构的研究;
3、MIDP移动信息设备简表的研究;
(1)、MIDP的目标硬件环境;
(2)、MIDP应用程序;
(3)、CLDC和MIDP库中的类。
4、J2ME API的研究;
(1)MIDlet应用程序的研究;
(2)MIDlet的类库研究;
(3)各事件发生器的应用。
5、开发工具的应用和开发环境的设置。
其中MIDP移动信息设备简表的研究和J2ME API的研究为重点,本课题将通过一个具体的手机游戏的开发来研究以上内容。
现在JAVA-JAVA2 Platform大致可分为J2SE、J2EE、J2ME三类。J2SE为JAVA-JAVA2 Platform 的标准版,通常在PC上使用的JAVA。J2EE是在J2SE的API上,扩展了给企业使用EJB与Servlet等主要使用在服务器上的功能。而J2ME则是面向家电和通信工具等微小设备。
J2ME的目标是微小设备,这类设备有许多种类,在这些设备的J2ME当中,定义了CDC(Connected Device Configuration),CLDC(Connected Limited Device Configuration)这两个Configuration。
CDC是以能用在个人网关、下一代移动电话、PDA(个人数字助理)、家电设备、POS终端、车辆导航系统等上运行为前提设计出来的。CLDC,是以能使用在移动电话、PDA(个人数字助理)、家电设备、POS终端等上面为前提设计出来的。
MIDP API包含以下五个部分,如表2-1所示:
表2-1 MIDP API
类 别 |
描 述 |
Application |
包括MIDlet类。 |
Timers |
主要包括Timers和Timers Task类。 |
Networking |
提供访问设备通信能力的接口。 |
Persistence |
通过记录管理系统(RMS)API访问用户永久存储。 |
User Interface |
包括MIDP LCDUI(液晶显示器用户界面)类。 |
MIDlet即MIDP的应用程序, MIDlet应用程序的核心是MIDlet类。为了创建一个MIDlet,必须从这个虚基类派生出自己的类。表2-2提供了从MIDlet类继承的方法。
表2-2 MIDlet类
方 法 |
描 述 |
访问JAR和JAD文件中的属性 |
|
String gerAppProperty(string key) |
返回JAR和JAD中与key相对应的属性的值。 |
Abstract void destoryApp (boolean unconditinal) |
应用程序管理器在应用程序关闭前,调用这个方法来给我们做一些事情的机会(例如保存状态和释放资源)。 |
Abstract void pauseApp() |
在用户暂停游戏时,应用程序管理器调用MIDlet的类方法。 |
Abstract void startApp() |
应用程序管理器调用MIDlet的这个方法,来告诉用户再次开启游戏。 |
Abstract void notifyDestroyed() |
如果游戏者决定退出游戏,可以调用这个方法来通知应用程序管理器。 |
续表2-2 MIDlet类
方 法 |
描 述 |
Abstract void notifyPausrd() |
调用这个方法来通知应用程序管理器游戏者已经暂停游戏。 |
Abstract void notifyRequest() |
调用这个方法来告诉应用程序管理器MIDlet要重新开始。 |
应用程序管理器(Application Manager ,AM)的作用就是管理MIDlet 。本质上来说,MIDlet应用程序只存在两种状态——暂停和运行。MIDlet被创建后默认为暂停状态,当应用程序管理器认为它准备完毕,它会调用startApp方法来通知MIDlet进入运行状态。
MIDP API 包括两个关于定时器的类——Java.util.Timer和Java.util.Timer Tast类 (如表2-3和2-4所示):
表2-3 Java.util .Timer类
方 法 |
描 述 |
Timer() |
构造一个新的Timer对象。 |
Void cancel() |
停止Timer。 |
Void schedule(TimeTask task,,Dare d) |
把一个任务定时在时间d运行。 |
Void schedule(TimeTask task, Data firstTime, long period ) |
让一个任务第一次在一个指定的时间运行,然后每隔period毫秒运行一次。 |
Void schedule(TimeTask task,long delay) |
指定一个任务在delay毫秒后运行一次。 |
Void schedule(TimeTask task,long delay,long period) |
指定一个任务从delay毫秒连续运行,然后每隔period毫秒运行一次。 |
Void scheduleAtFixedRate(timeTask task,Date firstTime,long period) |
指定一个任务从firstTime连续运行,然后以固定间隔period毫秒连续运行。 |
Void scheduleAtFixedRate(TimeTask task,long delay,long period) |
指定一个任务在delay毫秒后运行,然后以固定间隔period毫秒连续运行。 |
表2-4 Java.util.Timer Tast类
方 法 |
描 述 |
Timer Task() |
构造一个新的Timer Task对象。 |
Boolean cancel() |
结束该任务。 |
Abstract void run() |
这个方法必须被一个包含Timer时间执行的代码的方法重载。 |
Long scheduledExecution Time() |
返回任务上一次执行的确切时间。 |
我们可以通过不同schedule的方法来确定什么时候执行任务,包括在一个指定的时间执行一次和那个时间后按照一个固定的时间间隔连续执行。也可以在一段延迟(单位是毫秒)后执行任务,还可以选择以固定的时间间隔连续执行。
MIDP包含对CLDC中的GCF(Generic Connection Framework),即通用连接框架的支持,MIDP规范只是要区分必须实现HTTP的连接。如果想开发的是基于无线网络的高性能的多人联网游戏,这非常值得考虑应用。目前的情况是,无线网络的传输情况是高延迟以及高丢失率,所以响应时间在50ms的游戏很难实现。通用连接框架使用静态工厂类Connector去创建和返回一个连接。如图2-1
所示是所有类型的类层次图。
图2-1 通用连接框架包含丰富的多功能的通信类;但是MIDP只确保支持HttpConnection。
通用连接框架设计包括一个超级Connector的概念,这个Connector作为一个支持任何连接类型的工厂,基本上来说,调用Connector类的静态方法open即可,把需要连接的资源的名字作为参数传递过去,这个名字应该采用“协议: 地址: 参数”的格式。
在代码里将采用HttpConnection类,如表2-5所示:
表2-5 HttpConnection类
方 法 |
描 述 |
Static Connection open (String name) |
构造,打开和返回一个指向一个指定URL的连接。 |
续表2-5 HttpConnection类
Static Connection open(String name , int mode) |
构造,打开和返回一个连接,连接一个指定资源URL和打开的模式都需要设定。 |
Static Connection open(String name , int mode , Boolean timeouts) |
构造,打开和返回一个连接,连接一个指定资源URL和打开的模式都需要设定,同时也有一个参数指定是否需要超时异常。 |
static Connection openDataInputStream(String name) |
打开一个连接,然后构造和返回一个数据输入流。 |
static Connection openDataOutputStream(string name) |
打开一个连接,然后构造和返回一个数据输出流。 |
static Connection openinputStream(String name) |
打开一个连接,然后构造和返回一个输入流。 |
static Connection openOutStream(String name) |
打开一个连接,然后构造和返回一个输出流。 |
HttpConnection类是一个全功能的 HTTP客户端,实用于大多数的网络任务(低延迟)。对于游戏而言,可以用它来按需下载内容(如新的关卡),更新得分,或者实现游戏者之间的通信。表2-6 是所有HttpConnection类的可用方法。
表2-6 javx.microedition.io.HttpConnection类
方 法 |
描 述 |
long getData() |
获取头中的日期值。 |
Long getExpiration() |
获取头中的过期时间。 |
String getHeaderFieldkey(int n) |
根据索引获取头中的键名。 |
String getHeaderField(int n) |
根据索引获取头中的键值。 |
String getHeaderField(String name) |
获取指定的头文字段的值。 |
续表2-6 javx.microedition.io.HttpConnection类
long getHeaderFieldData(String name, long def) |
按照长日期类型返回指定字段的值,如果该字段不存在就返回def的值。 |
int getHeaderFieldInt(String name,int def) |
按照整数类型返回指定字段的值,如果该字段不存在就返回def的值。 |
Long getLastModified() |
返回最后一次更新的时间。 |
String getURL() |
返回URL。 |
String getFile() |
获取URL中的文件部分。 |
String getHost() |
获取URL中的主机部分。 |
int getPort() |
获取URL中的端口部分。 |
String getProtocol() |
获取URL中的协议部分。 |
String getQuery() |
获取URL中的查询部分。 |
String getRef() |
获取URL中的引用部分。 |
Int getResponseCode() |
返回HTTP响应状态码 |
String ResponseMessage() |
返回HTTP响应消息(如果存在的话)。 |
String getREquestMethod() |
获取连接请求的方法。 |
Void getRequestMethod(String method) |
设置URL请求的方法。可用的类型有GET,POST和HEAD。 |
String getRequestProperty(String key) |
获取与指定的键相关联的请求属性值。 |
Void setRequestProperty(String key, String value) |
设定与指定的键相关联的请求属性值。 |
开发游戏时,保存数据在J2ME里是用RMS(Record Management System,记录管理系统)来实现的,可以在Javax.microedition.rms包中找到它,表2-7就是这个包中所有的类的列表。RMS采用记录的方式来保存数据,然后使用唯一的记录号来应用这些数据,成组的数据就被保存在存储集中。
表2-7 RMS包(不包含异常)
类 |
描 述 |
类 |
|
RecordStore |
允许访问记录存储集功能。 |
接口 |
|
RecordComparator |
提供一个用来实现两个记录间比较的接口。 |
RecordComparation |
提供记录存储集的枚举器;可以和比较器和过滤器联合使用。 |
recordFilter |
对获取的数据进行过滤。 |
RecordListener |
提供一个用来“监听”RMS中发生的事件的接口,比如记录增加,修改和删除。 |
记录存储集即一个记录存储的机制,表2-8中展示了完整的API。
表2-8 记录存储API
方 法 |
描 述 |
存储集访问方法 |
|
Static RecordStore openRecordStroe(String record Name,blooean createIfNecessary) |
打开一个存储集或者在它不存在的时候创建一个存储集。 |
Void closeRecordStore() |
关闭一个存储集。 |
Static void deleteRecordStore(String recordStore Name) |
删除一个存储集。 |
Long getLastModified() |
获取存储集最后被修改的时间。 |
String getName() |
获取存储集的名称。 |
int getNumrecords() |
返回存储集当前记录的数量。 |
int getSize() |
返回存储集使用的总字节数。 |
int getSizeAvailable() |
获取空闲空间。 |
int getVersion() |
获取存储集的版本号。 |
续表2-8 记录存储API
Static String[] listRecordStores() |
获取MID中你可以访问的所有的记录存储集的字符串数组。 |
记录访问方法 |
|
int addRecord(byte,int offset,int numBytes) |
向存储集中加入一条新的记录。 |
byte[] getRecord(int recordId) |
用ID来获取一条记录。 |
int getRecord(int recorded,byte[] buffer,int offset) |
把一条记录读取到buffer中。 |
Void deletRecord(int recorded) |
删除与recordId相关的记录。 |
Void setRecord(int recorded,byte[] newData,int offset,intnumBytes) |
使用新的字节数组与recordId相关联的内容。 |
Int getNextRecordID() |
在插入后获取下一个记录的ID。 |
Int getRecord(int recorded) |
返回按字节计算的记录存储集当前的数据大小。 |
RecordEnumeration enumerate Records(RecordFilterfilter,RecordComparator,bool- -ean keepUpdataed) |
返回一个RecordEnumerator对象。它是用来在一个记录集合中枚举的(使用comparator参数)。 |
与时间有关的方法 |
|
Void addRecordListener(RecordListener listener) |
加入一个监听器对象,它可以在有这个记录存储集消息的时候被调用。 |
Void removeRecordLisrener(RecordListener listener) |
移除原来用addRecordListener方法加入的监听器对象。 |
记录存储集在与MIDlet包范围,也就是说同一个包的任何MIDlet都可以访问这个包中的记录存储集,其他包中的MIDlet甚至不能感知到别的包里记录存储集的存在。
一个记录就是一个字节数组,可以在里面写任何格式的数据。可以用DataInputStream、DataOutputStream往记录中写入数据,也可以用ByteArrayInputStream和ByteArrayOutputStream。
在记录存储集中记录是以一种类表的结构存储,如下图2-2所示:
每一个记录和它相关的字节数组都有一个整数主键唯一来标识,RMS会成为记录设定ID。头一个写入的ID是1,每次增加一条记录它的ID就增加1,上图展示了一个记录集的简单用法。在这个例子中,玩家的名字(字符串“John”)存储在记录1中,记录2保存最高分,记录3是先前从网络上下载的缓存的图象。
RMS支持使用Javax.microedition.rms.RecordEnumerator类来排序记录,如表2-9展示了它所有属于这个类中的全部的方法。
表2-9 Javax.microedition.rms.RecordEnumerator类
方 法 |
描 述 |
常用方法 |
|
Void destroy() |
销毁枚举器。 |
Boolean isKeptUpdated() |
指出在下面的记录存储集改变后该枚举器是否自动更新生成。 |
keepUpdated(Boolean keepUpdated) |
改变keepUpdated的状态。 |
Void rebuild() |
引起枚举器管理的索引重新生成。 |
续表2-9 Javax.microedition.rms.RecordEnumerator类
Void reset() |
把枚举器设置成刚刚创建后的状态。 |
访问 |
|
Boolean hasNextElement() |
测试在从前一个到最后一个的顺序中是否还有可以枚举的记录。 |
Boolean hasPreviousElement() |
测试在从最后一个到前一个的顺序中是否还有可以枚举的记录。 |
Byte[] nextRecord() |
获取存储集中的下一个记录。 |
Byte[] previousRecord() |
获取存储集中的前一个记录。 |
Int previousRecoed() |
获取前一天记录的ID。 |
Int nextRecord() |
获取下一个记录的ID。 |
Int numRecord() |
获取记录的数量,这在你使用过滤器的时候是很重要的。 |
可以用记录存储集来访问枚举器,也可以使用枚举器在记录中双向遍历。如果反向遍历只需要使用previousRecord。
RMS异常都是因为不正确的环境造成的,对于这些异常需要编写代码来处理问题(RecordStoreNotFoundException、RecondStoreNotOpenException、InvalidRecordIDExcepaion的情况),或者只能接受它。
在创建游戏时,MIDP允许我们使用两种截然不同的界面系统——高级UI和低级UI。
LCDUI的核心是screen的概念,它代表MID上的一个display,在任何一个时间点,只能有一个screen可见。
在LCDUI中有3种类型的screen:
高级UI提供了MID的一个抽象接口,通过它可以获得大量的功能。使用高级API首先创建组件把它们加入到屏幕,然后与它们相交互。高级UI一般划分为两大类:屏幕和组件。
screen是一个完整类组件,它管理整个屏幕。Form是一个特殊的screen,可以在Form中由少量几个组件来构造一个screen。
List是一个可以给用户显示一组备选项的组件。这个类实现了Javax.microedition.lcdui.Choice接口,ChoiceCroup item也实现了这个接口。
TestBox组件是微型世界的字处理器,它只能输入多行的文字。它可以让玩家输入多行文字、剪切、复制以及从剪切板粘帖、过滤输入的数据。
可以使用它来显示一个提示信息(因为它是一个screen,所以它接管整个屏幕)。
Form是一种可以包含一个或者多个下面这些从Item类派生出来的组件的screen——StringItem、ImageItem、TextField、ChoieGroup、DataField和Gauge。
运用StringItem类在Form上加入简单的文字消息。
低级UI提供了一个工具包来移动和绘制图形、显示文字、获取直接的按键事件等。
Canvas又称画布,是一个Displayable对象,所有绘图操作都画在它上面。
Graphics类工具在Canvas中承担基本的二维绘图。
drawLine采用4个参数——直线起点的x、y坐标值和直线终点x、y的坐标值,例如:
graphics.drawLine(50,0,100,0);
这行代码会从位置(50,0)到(100,0)绘制一条直线。
绘制一个矩形是一个类似的过程,不同的只是需要用起点加上宽度和高度的方式来指定这个对象,可以绘制透明的或者填充的矩形,甚至可以绘制圆角的矩形。4个绘制矩形的方法是:drawRect、drawRoundedRect、fillRect和fillRoundedRect。
弧是使用6 个参数来绘制的,前3个参数是弧所在的整个圆的外切矩形。剩下的两个参数是startAngle和arcAngle。Angle是度数,0为右侧(在三点的位置)的地方,180是左侧(在九点的位置)的位置。
可以使用方法drawChar、drawChars、drawString和drawSubstring在Canvas上面绘制文字。
裁剪让人可以把图象输出限制到显示设备的一个特定区域中,例如,如果将输出限制在一个从(10,10)开始到(50,50)的区域中,那么从那个时候起,没有图像会出现在显示设备上这个区域之外的任何地方。
实现一个或者多个按键事件响应方法:keyPressde、keyRleased和keyRepeated。
在贪吃蛇游戏中,玩家操作由小方块连接而成的蛇,去吃随机散落在画面内的小方块,每吃一块就增加一小方块长度,要是撞壁以及撞自己的尾,就属于失败,如无失败则直到通关为止。
屏幕的长度的行向为11单位,纵向为18单位。在这个范围内,玩家通过操作方向键来控制蛇的运动方向。该游戏的最大特色是屏幕自适应,无论各种手机,PDA的屏幕大小如何,该游戏总是能获得最佳的显示效果。
(1)开发的硬件环境:CPU C1.7HZ/Maxor 40G/DDR 256M/CD-ROW 40X
(2)开发软件:JDK1.3和J2MEWTK
本游戏的操作流程(如图3-1):用户在启动MIDlet后,即进入游戏主画面,屏幕开始显示为欢迎画面。用户按下[开始]按钮后,就可以开始玩游戏。当用户想暂停时,再次按一下[开始]按钮,游戏就暂停了,在暂停的情况下再按[开始]按钮,游戏继续运行。任何时候按[退出]按钮,游戏MIDlet都会终止。
(1)游戏地图代码设计
游戏地图是蛇的活动范围和食物随机散落的范围,游戏的容器为行向为11单位,纵向为18单位,如下代码:
private final int iX = 10; //地图的开始坐标
private final int iY = 10; //
private final int SWIDTH = 16; //图标的宽度
private final int iCells = 11; //地图的列数
private final int iRows = 18; //地图的行数
private final int iBoxW = SWIDTH*iCells; //地图的宽
采用二维绘图工具:二维绘图工具drawLine采用4个参数——直线起点的x、y坐标值和直线终点x、y的坐标值,例如:
graphics.drawLine(50,0,100,0);
这行代码会从位置(50,0)到(100,0)绘制一条直线。
绘制一个矩形是一个类似的过程,不同的只是需要用起点加上宽度和高度的方式来指定这个对象。可以绘制透明的或者填充的矩形,甚至可以绘制圆角的矩形。4个绘制矩形的方法是:drawRect、drawRoundedRect、fillRect和fillRoundedRect。
(2)贪吃蛇和食物的代码设计
屏幕的长度为行向为11单位,纵向为18单位;在这个范围内(如图3.3),玩家操作方向键控制蛇的运动方向。该游戏的最大特色是屏幕自适应,无论各种手机,PDA的屏幕大小如何,该游戏总是能获得最佳的显示效果。
贪吃蛇最初由3个小正方形组成,小正方形是蛇的身体和游戏容器的组成部分。食物也由一块小正方形组成,并且随机散落在游戏框图的区域内,每次只出现唯一的一个,待玩家操作游戏完成一个任务后面,再出现下个食物,小蛇每吃一个食物就增加一个长度。实现代码如下:
public int getCell(){
return iCell;
}
public int getRow(){
return iRow;
}
public void show(){
sLabel.setVisible(true);
}
public boolean isVisible(){
return sLabel.isVisible();
}
public void hide(){
sLabel.setVisible(false);
}
protected void setPosition(int row, int cell){
iCell = cell;
iRow = row;
sLabel.setBounds(cell*SWIDTH, row*SWIDTH, SWIDTH, SWIDTH);
}
public void moveUp(){
if(iRow>0)
setPosition(--iRow,iCell);
else
setTouch();
}
public void moveDown(){
if(iRow setPosition(++iRow,iCell); else setTouch(); } public void moveLeft(){ if(iCell>0) setPosition(iRow,--iCell); else setTouch(); } public void moveRight(){ if(iCell setPosition(iRow,++iCell); else setTouch(); } static boolean getTouch(){ return isTouch; } static void setTouch(){ isTouch = true; } static void setImTouch(){ isTouch = false; } public Snake clone(){ Snake tem = new Snake(); tem.setPosition(this.getRow(),this.getCell()); return tem; } (3)操作控制代码设计 MIDP的游戏设计,本质上就是用一个线程或者定时器产生重绘事件,用线程和用户输入改变游戏状态。这个游戏也不例外,启动MIDlet后,就立即生成一个重绘线程,该线程每隔50ms绘制一次屏幕。当然,重绘时有一些优化措施,并不是屏幕上所有的像素都需要重绘,而是有所选择,比如游戏画布上那些已经固定下来的就不需重绘。游戏画布是一个CommandListener,可以接受用户键盘命令,控制蛇的左移,右移,下移,上移。其代码如下: public void move(){ hide(); if(aoSnakes[0].getRow()==Food.getRow()&&aoSnakes[0].getCell()==Food.getCell()) eat(); Snake tempSnakes[] = new Snake[iLen-1]; for(int i=0;i tempSnakes[i] = aoSnakes[i].clone(); } switch(heading){ case 37: aoSnakes[0].moveLeft(); break; case 38: aoSnakes[0].moveUp(); break; case 39: aoSnakes[0].moveRight(); break; case 40: aoSnakes[0].moveDown(); break; default: break; 整个游戏的流程控制体现在游戏画布对象的paint()方法里。paint()根据当前的游戏状态,绘制出当时的游戏画面。欢迎画面和Game Over画面的绘制相当简单,游戏暂停画面的绘制也相当容易,就是设立标志,让paint()执行的时候无需真正执行重绘动作。 未完待续。。。。