第第 1144章章 单单屏屏游游戏戏的的设设计计与与实实现现
本章实现了一个单屏幕飞机游戏,讲解了 2D 游戏的策划和架构设计,这些设计侧重在
单屏手机游戏上。之后对程序设计以及游戏商用化进行了更深层次的讨论,包括游戏定制、
版权保护和游戏框架等内容。通过本章的学习希望读者领悟到:
• 2D游戏的基本设计流程
• 如何让游戏更具有吸引力
• 如何保护游戏版权以及可能有的方法
• 尽可能让玩家可以定制游戏
• 益智类游戏需要关注逻辑层和
现层的分离
14.1 游戏的策划以及架构
本节以一个小游戏为例,讲解了单屏幕游戏的策划、图片制作、类结构和流程设计。这
些步骤讲解得比较简单,但却基本能够代表了一个游戏在策划方面的所有工作。在实际设计
中,这些步骤还需要更加细致和具体化。例如图片的设计和优化、交互界面的人性化都是游
戏成功的重要因素。
14.1.1 单屏幕手机游戏概述
单屏幕游戏是指那些游戏画面不进行滚动(或者不进行大范围滚动)的游戏,它和滚屏
游戏相比照不需要游戏的滚屏技术,但是单屏游戏更加侧重于游戏的可玩性和简单性。
在此将单屏游戏分为两类:其中一类体现在操作简便,持续时间短,通常需要玩家具有
较好的操控能力和反应速度。这类游戏的代表例子为泡泡龙、俄罗斯方块、祖玛游戏等,此
外类似的还有单屏幕的射击游戏。这类游戏的设计比滚屏游戏要简单,但其难点是可能逻辑
判断更为复杂而且对美工的要求更高。如图 14-1 和图 14-2 分别是特别具有代表性的泡泡龙
游戏和俄罗斯方块游戏,虽然游戏非常简单,但非常容易让玩家痴迷其中。
J2ME手机游戏设计
·464·
图 14-1 一款泡泡龙游戏的游戏画面
图 14-2 两款方块游戏画面
另一类单屏幕游戏是属于益智类的,虽然游戏界面比较简单,也不需要很复杂的游戏特
效,但是其游戏逻辑通常使用到人工智能等算法,如何控制游戏难度和优化算法是该类游戏
的难点之一。这类游戏包括棋类游戏、牌类游戏和智力测试,例如五子棋、麻将、拼图和扫
雷游戏等。如图 14-3所示为益智类单屏游戏的画面。
图 14-3 益智类游戏
14.1.2 手机游戏策划概论
是什么使得一个手机游戏令人上瘾?为了使一个游戏令人上瘾,它必须有一个使人玩下
去的动力。以下是一些因素:想完成这个游戏,想战胜其他的对手,想掌握游戏的操作方法和
第 14章 单屏游戏的设计与实现
·465·
交互界面,想在游戏的世界中探险并且获得高的得分或等价物。
1.想完成手机游戏的动力
想完成这个游戏经常基于想看到游戏最终的结果或仅仅只是想完成它。在仅仅只是想完
成游戏的例子中,这些游戏只是被看作是一个挑战。去持续地玩一个明显很难的游戏并直到
完成为止,这可能是一种满足自信心的行为。然而,这并不能产生一种最好的结果,有些玩
家乐于寻找挑战并去战胜它,而其他的玩家可能会觉得它太难了以至于抛在一边。人们都喜
欢去赢得胜利,如果你能提供一个游戏,它挑战这些玩家,而且还最终让玩家赢得胜利,那
么这个游戏会和高兴的玩家一起愉快地结束。游戏的难度虽然只是一个设计上的选择,但作
为一个体贴玩家的设计者必需要充分地考虑到这一因素所带来的结果。
另一方面,一个玩家结束了一个游戏,来看游戏的结局是怎样的,这就是一个故事的推
动力,甚至在象超级玛丽奥兄弟《SuperMarioBrothers》这样简单故事的游戏中,也有一个结
局。许多玩家想知道在不断地战斗并最终救出公主后会发生什么。一旦他们完成了整个故事,
又看到了最终的结局,那么多半玩家会终止游戏的进行,不会再去碰它了。
2.竞争的动力
和其他人竞争是一个有力的因素并且能够保持游戏的活力,能够让人难以致信地在很长
一段时间内流行。一个两个人或更多玩家能够很好地互相竞争的游戏能够玩相当长的时间,
远远超过了它在手机上的期待生存期。竞争是游戏的基石之一。它允许人们在游戏
--
这一公共
的监督下互相交互,而且确实把游戏的主动权交到了每个玩家的手中而不是在
设计者的手中。如果你设计的游戏规则比较自由、能动,那就允许了玩家创立他们自己的游
戏方式,在游戏中移动和使用计谋,就像你以前从未考虑过的。
3.提高操作技巧的动力
游戏中的技巧或控制也是非常重要的。运动模拟游戏尤为突出地表现了这一点。因为这
类游戏的主要目地是模拟独特的运动控制。玩家经常重复地玩这类游戏来提高自已的操作技
巧。
举个例子,在赛车游戏中,在笔直向前的道路上做一个简单的拐弯很容易就能做到,但
是为了更好地完成这个动作以赢得时间,你必须能够感觉到道路的情况,以及当你的轮胎打
滑时所采取的相应动作。让玩家在你的游戏中学习如何控制的要点是在你给玩家的反馈中。
一个轮胎摩擦的声音提示你轮子开始打滑了,当你穿过一个急转弯时发动机的轰鸣声会发生
变化等。设计者应提供给玩家游戏操作手册或帮助功能,使玩家能够通过图片、文字、动画
来理解游戏的功能和如何操作,这将使玩家逐渐地熟悉这种方式,并且给他们一个理由来试
着掌握它。如果玩家不能够获得足够的反馈信息,那就可能没有更好的方式来掌握它。如果
一个初玩者和一个老练的玩家在做一些动作时只有相同的选择,那就没有理由尝试着成为一
个老练的玩家。
4.渴望探险的动力
在计算机游戏开始时探险就已经包含在其中了。事实上早期的一些游戏只包括探险。冒
J2ME手机游戏设计
·466·
险《Adventure》是一个文字类的游戏,在其中玩家可以在广阔的区域中游荡,查找并搜集有
趣的物品,使用它们来解开几个谜题,通过这些谜题会发现更广阔的区域需要去探险。当代
的探险游戏--冒险岛《Myst》也是用类似内容作为它的基础,这个游戏的流行并不说明许
多人在这块有趣地方探险的推动力是去发现神秘岛中奇怪的事件,但有好几个游戏是以这点
作为宣传的资本。
隐藏的内容也是许多游戏吸引人的因素之一,超级玛丽奥兄弟的一个吸引点就是去找出
隐藏的情节。ShigeruMiyamoto(任天堂的设计小组成员之一,曾制作过超级玛丽奥兄弟,
Zelda,Metroid,etc)认为视频游戏 40%的内容应该是隐藏的,对玛丽兄弟游戏来说这一点
做得非常好。
5.获得高得分的动力
渴望获得高得分的情况主要分成二类,一般来说在游戏中尝试获得高得分或其他等价物
的玩家希望在竞争中超过其他玩家的得分记录或想完全地掌握这个游戏。有许多游戏的目地,
只是简单地为了赢得一个较高的分数。它起源于古老的撞球游戏,在当前这个更先进的网络
时代,这一规律仍然生效,并且广受欢迎。
另一种情况已经超越了赢得游戏本身。在超级玛丽奥兄弟中,当你赢了之后,你可能会
在增加了难度后继续去玩。用一个硬币能通关几次或一条命能冲多少关,积多少分,这已经
变成了衡量玩家水平高低的标准。老玩家会因水平高而自豪,它甚至会引来其他玩家的尊敬。
设计你的游戏,使你的玩家甚至在游戏获胜后仍愿意继续玩下去。并通过难度的增加,
更具有挑战性来激励玩家玩下去。这是你能增加到你游戏中去的要素。
14.1.3 逃亡者游戏的策划
为了简化游戏的策划,选取了一款 PC小游戏作为范例移植到手机上,它的中文名为《是
男人就撑过 30秒》,相信你一定玩过原版本的这个游戏,虽然看似简单,但绝对有挑战性!
这是
了无数日本纵版飞行射击游戏中的武器而研究出的特殊训练软件,专门用来训练“战
斗机”飞行员。
在漆黑一片的宇宙中,停着一架小飞船,突然四面八方出现很多黄色的小点向小飞船聚
集过来,小飞船凭借速度优势和飞行技术从黄点之间的夹缝中飞过,之后又落入新的包围圈
中,直至被黄点击毁。坚持的时间越长,就说明水平越高(起码是控制键盘上方向键的水平)。
持续不同的时间会得到不同的评价。如图 14-4所示是它原来的游戏画面。
第 14章 单屏游戏的设计与实现
·467·
图 14-4 《特训:是男人就撑过 30秒》游戏画面
这游戏是日本人设计出来的,不得不承认,设计者对现代社会的本质认识得很深刻,将
激烈的社会竞争微缩在一个小小的游戏上。从中体会到三点人生启示:第一,玩这个游戏的
窍门——不停地绕圈子,人活在这个世界上也是一样,必须始终保持活力,跟上时代的脚步,
静止、停步不前就意味着死亡;第二,成功的关键在于坚持、在于坚定的意志,面对挑战和
困难,比别人坚持的时间更长,成功的机率就越大,受到来自社会的赞誉就越高;第三,要
想成功,仅仅有坚定的意志仍然不够,四面八方都是敌人,如何在运动的物体中找到一条安
全的出路,这还需要具备冷静敏锐的头脑。
这个游戏的目的是提供一个指导范例,而不是完全的商业化游戏产品。希望读者能开发
出具有更好的图形、音响效果和更具可玩性的游戏。
14.1.4 逃亡者游戏的准备工作
游戏的准备工作主要是准备游戏所需要的图片和声音,并且说明它们在何处用到。这里
用到了 7个图片文件,如图 14-5所示。它们的大小和用途如表 14-1所示。
注意:对于游戏中使用的大图,最好用 FireWork 或 PhotShop 等图像软件进行优化,例如在优化
之前闪屏图片为 32k,优化之后大小变为 6k,缩小为原来的五分之一。
图 14-5 游戏中使用到的图片
表 14-1 图片清单
图片名 大小(字节) 象素(宽×高) 用途
back_water.png 557 32×32 背景图片
bullet.png 230 6×6 子弹图片
Escapee.png 504 69×21 飞机图片
explosion.png 1131 128×32 爆炸图片
gameover.png 447 84×13 游戏结束图片
splash.png 6,158 146×156 闪屏图片
logo.png 474 13×13 游戏标志
对游戏来说,最有用的功能是播放短的声音文件产成音响效果。MIDP 2.0 Media API没
J2ME手机游戏设计
·468·
有规定必须支持的声音文件格式,但对WAV (8 kHz mono PCM) 和MIDI文件格式的支持
是合理的期望。游戏中用到的声音如表 14-2所示。
表 14-2 声音清单
声音文件名 大小(字节) 声音格式 用途
Blast.wav 10544 wav 子弹和飞机碰撞时发出
gameover.mid 103 mid 游戏结束,不是最好成绩时发出
highscore.mid 115 mid 游戏结束,是最好成绩时发出
通过MIDlet JAR文件中的资源文件播放声音样本的代码如下所示:
try {
InputStream is = getClass().getResourceAsStream(“/dog.wav”); //以输入流的形式获取声音资源
Player p = Manager.createPlayer(is, “audio/x-wav”); //创建 Player对象
p.prefetch(); //声音的预读去
p.start(); //播放声音
} catch (IOException ex)
{
//捕获 IO异常
}
catch (MediaException ex)
{
//捕获声音文件异常
}
可通过调用 start重复使用同一个播放器。但当播放器已经在进行播放时,这样做不起作
用,因此可以使用如下代码来确保播放器从头开始播放:
p.stop(); //停止声音
p.setMediaTime(0L);
p.start(); //从头从新开始播放
游戏中的类 SoundEffects 使用了这种方法。此外,MIDP2.0 在类 Display 中加入了方法
vibrate(int),这使得游戏可以激活电话的振动功能(如果电话具有该功能)。其参数为用毫秒
表示的振动持续时间——没有办法控制振动频率。当产生新的游戏纪录时,游戏使用该方法
产生振动。
在类 Display中加入了方法 flashBacklight(int)之后,游戏可以使电话的背景灯闪烁(如果
电话支持该功能)。其参数为用毫秒表示的闪烁持续时间——没有办法控制闪烁频率。当第一
次显示游戏结束屏幕时,游戏使用该方法(由 GameOverScreen调用)使背景灯闪烁。
14.1.5 程序的类结构
程序中一个有 10个类,其中MIDlet主类负责各个屏幕的切换,它们是闪屏屏幕、菜单、
介绍屏幕、高分屏幕、游戏屏幕,游戏结束屏幕。游戏中使用到的类为 SoundEffects(音效)、
Bullets(子弹)、Escapee(逃亡小飞机)。程序的类结构如图 14-6所示。
第 14章 单屏游戏的设计与实现
·469·
图 14-6 类结构
14.1.6 游戏的流程图
在进入游戏之前先显示闪屏图片,当用户按下键盘或等待 3秒后,进入游戏菜单。初始
情况下,游戏菜单有三个选项,它们分别是开始游戏,游戏说明和高分记录。选择开始新游
戏则进入游戏,在游戏中如果按下非游戏键盘则中断游戏返回菜单,此时菜单中增加了一个
继续游戏的选项,可以返回游戏也可以重新开始新的游戏。当游戏结束时则进入游戏结束屏
幕,屏幕上显示了玩家的成绩和等级,以及游戏的最好成绩,如果当前成绩是最好成绩,则
手机震动并播放音乐庆祝成功。在菜单中选择游戏说明或者高分纪录,则进入相应的屏幕,
它们都能用“后退”软键返回菜单。菜单中的退出选项用于退出程序。游戏的流程如图 14-7
所示。
图 14-7 游戏的流程图
J2ME手机游戏设计
·470·
14.2 游戏的实现
逃亡者游戏一共实现了几个类包括用于关于游戏外部的闪屏类、菜单类、高分屏幕类、
简介屏幕类、结束屏幕类,以及用于游戏本身的游戏画布类、子弹类、逃亡飞机类和声音效
果类。
14.2.1 主类 escapeeMIDlet的实现
MIDlet被用作一个状态机来管理各种屏幕以及它们之间的转换。例如,当显示 splash屏
幕时,该类和类 SplashScreen(通过方法 splashScreenPainted 和 splashScreenDone)共同在方
法 init 中完成背景初始化。而使用方法 readRecordStore 和 writeRecordStore 在一个名为
“BESTTIME”的记录存储区中保存最高得分。下面将讲解它的具体实现。
1.实现闪屏
游戏的最开始将会出现一幅和游戏相关的图片,它会停留一小段时间,然后才进入游戏
菜单。游戏闪屏使用得当将会增加整个游戏的视觉效果,而且虽然画面在此期间停留不动,
后台程序却是忙碌的,一般在此期间完成基本的初始化工作。
在 startApp 方法中,将游戏的显示权交给闪屏。注意前面说过,startApp 方法可能会在
程序受到外界中断的情况下多次调用,所以还应当看是否处于这种情况,如果中断之前的显
示内容为游戏画布,则重新启动画布的线程,显示权仍然交给画布。
public void startApp()
{
Displayable current = Display.getDisplay(this).getCurrent(); //获得关于显示的设备上下文
if (current == null)
{
Display.getDisplay(this).setCurrent(new SplashScreen(this)); //第一次将显示闪屏
}
else
{
if (current == myCanvas)
{
myCanvas.start(); //重新开始游戏线程
}
Display.getDisplay(this).setCurrent(current); //显示游戏画布内容
}
}
当闪屏绘制完毕时,将会调用 splashScreenPainted 方法,表示程序可以利用剩余的间歇
来完成一些工作,这里启动了MIDlet实现的一个线程,它会自动去调用 run方法。在 run方
法中将会进行了一些初始化工作,这包括读取游戏记录、初始化游戏声音、初始化游戏菜单、
初始化游戏画布,当所有工作都完成时,将一个 initDone 标志位设置为 true,表明已经初始
化完毕(用来避免重复初始化)。当闪屏结束后,将会调用 splashScreenDone方法,在这个方
法里将检测初始化是否完毕,如果还没有则继续初始化,如果已经完毕则显示游戏菜单。关
于闪屏实现的代码如下:
第 14章 单屏游戏的设计与实现
·471·
void splashScreenPainted()
{
new Thread(this).start(); //启动幕后的初始化线程
}
public void run()
{
init(); //调用初始化方法
}
private synchronized void init()
{
if (!initDone) //如果还没有初始化完毕
{
readRecordStore(); //读取游戏记录
SoundEffects.getInstance(); //获得声音效果类的唯一实例
menuList = new MenuList(this); //初始化菜单
myCanvas = new escapeeCanvas(this); //初始化游戏画布
initDone = true; //标志位为真表示初始化完毕
}
}
void splashScreenDone()
{
init(); //检测是否初始化完毕,如果没有继续初始化
Display.getDisplay(this).setCurrent(menuList); //在屏幕上显示游戏菜单
}
2.屏幕切换
前面说过 MIDlet 主类起到一个状态机的作用,负责各个屏幕之间的切换。它所实现的
切换功能如下。
从菜单中选择开始游戏,首先初始化 Canvas 的游戏数据,显示游戏画布,然后启动游
戏线程:
void menuListNewGame()
{
myCanvas.init(); //初始化游戏数据
Display.getDisplay(this).setCurrent(myCanvas); //显示游戏画布
myCanvas.start(); //启动游戏线程
}
从菜单中选择显示高分纪录,注意每次都动态生成一个高分纪录屏幕,使用完后就会销
毁以避免在不使用的时候占用堆内存:
void menuListHighScore()
{
Display.getDisplay(this).setCurrent(new HighScoreScreen(this)); //显示高分纪录屏幕
}
从菜单中选择游戏说明,和选择显示高分纪录类似:
void menuListInstructions()
{
Display.getDisplay(this).setCurrent(new InstructionsScreen(this)); //显示游戏介绍屏幕
}
游戏如果中断,从菜单中选择继续游戏,显示游戏画布并重启游戏线程:
J2ME手机游戏设计
·472·
void menuListContinue()
{
Display.getDisplay(this).setCurrent(myCanvas); //显示游戏画面
myCanvas.start(); //启动游戏线程
}
从菜单中选择退出,调用 quit方法:
void menuListQuit()
{
quit(); //退出游戏
}
从游戏介绍退回菜单,将显示权交还游戏菜单:
void instructionsBack()
{
Display.getDisplay(this).setCurrent(menuList); //显示游戏菜单
}
从高分榜退回菜单,将显示权交还游戏菜单:
void highScoreBack()
{
Display.getDisplay(this).setCurrent(menuList); //显示游戏菜单
}
从游戏中切换到菜单,需要中止游戏线程并且告知菜单游戏正在进行中,以便让菜单添
加继续游戏的选项:
void GameCanvasMenu()
{
myCanvas.stop(); //暂停游戏线程
menuList.setGameActive(true); //告知菜单游戏已经开始(暂停状态)
Display.getDisplay(this).setCurrent(menuList); //显示游戏菜单
}
显示游戏结束画面,需要告诉 GameOverScreen 类当前的最好成绩以及子弹的数目,同
时告诉菜单游戏已经结束:
void GameCanvasGameOver(long time,int BULLETS_NUM)
{
myCanvas.stop(); //中止游戏线程
menuList.setGameActive(false); //告知游戏已经结束
Display.getDisplay(this).setCurrent(new GameOverScreen(this, time, BULLETS_NUM)); //结束画面
}
从游戏结束画面返回,在屏幕上显示菜单:
void gameOverDone()
{
Display.getDisplay(this).setCurrent(menuList); //显示游戏菜单
}
3.最佳纪录
读取存储游戏纪录,以及检查是否存在最佳纪录的工作也是在这里进行的。读取游戏存
储记录的功能在 readRecordStore方法中实现。在 MIDlet类的开始指定了 rms记录的名称,
用静态字符串常量表示:
private static final String RS_NAME = "BESTTIME";
第 14章 单屏游戏的设计与实现
·473·
读取方法的代码如下,hasBestTime 标志位用来表明当前是否有最佳成绩。注意如果该
记录不存在也不用创建它,而是在写入记录的时候加以创建,此外 rms的数据记录是从第一
条开始的:
private void readRecordStore()
{
hasBestTime = false; //读取记录还没有获得最好成绩
RecordStore rs = null;
ByteArrayInputStream bais = null;
DataInputStream dis = null;
try
{
rs = RecordStore.openRecordStore(RS_NAME, false); //记录名为 RS_NAME的字符串
byte[] data = rs.getRecord(1); //读取第一条记录
bais = new ByteArrayInputStream(data);
dis = new DataInputStream(bais);
bestTime = dis.readLong(); //读取长整型值
hasBestTime = true; //以及从记录中获得最佳成绩
}
catch (IOException ex)
{
// 捕获 IO异常
}
catch (RecordStoreException ex)
{
// 捕获读取存储记录异常
}
finally
{
if (dis != null)
{
try
{
dis.close(); //关闭流
}
catch (IOException ex)
{
// 捕获 IO异常
}
}
if (bais != null)
{
try
{
bais.close(); //关闭流
}
catch (IOException ex)
{
//捕获 IO异常
}
}
J2ME手机游戏设计
·474·
if (rs != null)
{
try
{
rs.closeRecordStore(); //关闭存储
}
catch (RecordStoreException ex)
{
//捕获存储异常
}
}
}
}
除了读取存储之外,如果游戏产生了一个最新成绩,还需要保存到数据存储中去。保存
方法如下:
rivate void writeRecordStore()
{
RecordStore rs = null;
ByteArrayOutputStream baos = null;
DataOutputStream dos = null;
try
{
rs = RecordStore.openRecordStore(RS_NAME, true);//带开数据存储存如果不存在则创建
baos = new ByteArrayOutputStream(); //输出字节流
dos = new DataOutputStream(baos); //输出数据流
dos.writeLong(bestTime); //写入最佳成绩
byte[] data = baos.toByteArray();
if (rs.getNumRecords() == 0) //如果里面没有记录
{
rs.addRecord(data, 0, data.length); //添加记录
}
else
{
rs.setRecord(1, data, 0, data.length);//如果存在记录,则将第一条设置为当前最佳成绩
}
}
catch (IOException ex)
{
// 捕获 IO异常
}
catch (RecordStoreException ex)
{
// 捕获数据存储异常
}
finally
{
if (dos != null)
{
try
{
第 14章 单屏游戏的设计与实现
·475·
dos.close(); //关闭流
}
catch (IOException ex)
{
//捕获 IO异常
}
}
if (baos != null)
{
try
{
baos.close(); //关闭流
}
catch (IOException ex)
{
//捕获 IO异常
}
}
if (rs != null)
{
try
{
rs.closeRecordStore(); //关闭数据存储
}
catch (RecordStoreException ex)
{
//捕获数据存储异常
}
}
}
}
在读取游戏或者从玩家前面的游戏记录中获取到成绩之后,立刻写入 rms存储。通过比
较获得最佳成绩并保存的实现代码如下:
boolean checkBestTime(long time)
{
if (!hasBestTime || (time > bestTime)) //如果还没有最好成绩或者当前成绩比最好成绩长的时候
{
hasBestTime = true; //设置标志位为真,表示有最好成绩
bestTime = time; //上面两种情况下,把最好成绩 bestTime替换成当前成绩 time
writeRecordStore(); //保存最好成绩
return true; //返回 true表明对最好成绩进行了更新操作
}
else
{
return false; //返回 false表明没有最新的成绩纪录产生
}
}
在生成最好成绩之后(无论是读取存储或者玩家在游戏中动态产生的),就需要让游戏
结束屏幕或者高分榜知道这个最好成绩:
long getBestTime()
J2ME手机游戏设计
·476·
{
return hasBestTime ? bestTime : -1;
}
4.停止、销毁、退出程序
一般来说,游戏程序的停止、销毁、退出工作主要集中在对游戏画布的处理上,暂停程
序如果当前显示内容为游戏画布(表明游戏进行中)则应当调用 Canvas 的 stop 方法暂停游
戏线程。注意 Canvas的暂停需要对当前的游戏状态做一个简单记录,以便将来恢复,Canvas
的暂停方法在后面会讲到。游戏的暂停、退出代码如下:
public void pauseApp()
{
Displayable current = Display.getDisplay(this).getCurrent(); //获得当前设备的显示上下文
if (current == myCanvas) //如果屏幕上的显示内容为游戏画布
{
myCanvas.stop(); //中止游戏线程
}
}
public void destroyApp(boolean unconditional)
{
if (myCanvas != null)
{
myCanvas.stop(); //在销毁程序之前先停止游戏线程
}
}
private void quit()
{
destroyApp(false);
notifyDestroyed(); //告知系统已经销毁程序,可以完全退出
}
5.加载图片资源
MIDlet中实现了根据字符串类型的文件名来加载图片资源的通用方法,其他各个类都可
以使用这个方法来加载图片,也可以不这样做,但是这样使得程序更加模块化和便于理解:
static Image createImage(String filename)
{
Image image = null;
try
{
image = Image.createImage(filename); //根据文件名创建 Image对象
}
catch (java.io.IOException ex)
{
//捕获 IO异常
}
return image; //返回图片对象 image
}
第 14章 单屏游戏的设计与实现
·477·
6.游戏特效
因为只有MIDlet类能够访问 Display对象,因此通常将震动、背景光等特效放置在这个
类里面,其代码如下,mills代表持续的时间。
void vibrate(int millis)
{
Display.getDisplay(this).vibrate(millis); //手机震动,持续时间为millis
}
void flashBacklight(int millis)
{
Display.getDisplay(this).flashBacklight(millis); //手机背景光闪烁,持续时间为millis
}
14.2.2 游戏闪屏 SplashScreen类的实现
Splash屏幕在屏幕中央显示图像,如图 14-8所示,画面为一架战斗机,以及游戏的名称
和版本信息。在图像边缘显示有围绕的红线(这样在所有屏幕大小中都有较好视觉效果)。在
第一次绘制屏幕后,它将图像释放作为垃圾回收(把图片设为 null)并回调MIDlet进行初始
化工作。三秒钟后或第一次按键后,它再次回调MIDlet显示游戏菜单。通过这种方法,MIDlet
可在显示 splash屏幕的同时进行初始化。
图 14-8 游戏闪屏画面
1.构造函数
SplashScreen类是一个 Canvas类的子类,它的构造函数先获得一个对主类的引用,以便
于回调主类的方法,然后加载闪屏图片 splash.png,并启动闪屏线程。
SplashScreen( escapeeMIDlet midlet)
{
this.midlet = midlet;
setFullScreenMode(true);
splashImage = escapeeMIDlet.createImage("/splash.png");
new Thread(this).start();
}
J2ME手机游戏设计
·478·
2.绘制屏幕
闪屏屏幕的绘制是在 paint方法中完成,当显示闪屏时会自动调用 paint方法。注意下面
是绘制具有轮廓的文本的有用诀窍:首先用轮廓颜色把它绘制四次,分别向上、下、左、右
偏移,然后在它的正常位置用文本颜色对其绘制。在游戏主屏幕中使用该方法要小心,因为
文本绘制可能很慢,而该方法使速度减慢为原来的五分之一。
public void paint(Graphics g)
{
int CanvasWidth = getWidth(); //获得画布宽度
int CanvasHeight = getHeight(); //获得画布高度
g.setColor(0x00FFFFFF); //画笔颜色设置为白色
g.fillRect(0, 0, CanvasWidth, CanvasHeight); //填充整个屏幕
g.setColor(0x00FF0000); //画笔颜色设置为红色
g.drawRect(1, 1, CanvasWidth-3, CanvasHeight-3); //在屏幕边缘绘制一个矩形边框
if (splashImage != null)
{
g.drawImage(splashImage, CanvasWidth/2, CanvasHeight/2,
Graphics.VCENTER | Graphics.HCENTER); //在屏幕中央绘制闪屏图片
splashImage = null; //将图片对象设置为 null,以便垃圾回收
}
g.setFont(Font.getFont(Font.FACE_PROPORTIONAL,Font.STYLE_BOLD, Font.SIZE_LARGE));
int centerX = CanvasWidth / 2;
int centerY = CanvasHeight / 2+60;
g.setColor(0x00FFFFFF); //将画笔颜色设置为白色
drawText(g, centerX, centerY - 1); //分别绘制四次,相差 1个象素,呈现文本带有背景的感觉
drawText(g, centerX, centerY + 1);
drawText(g, centerX - 1, centerY);
drawText(g, centerX + 1, centerY);
g.setColor(0x00000000); //将画笔颜色设置为黑色
drawText(g, centerX, centerY); //绘制文本
midlet.splashScreenPainted(); //回调midlet的方法,通知程序在幕后做初始化
}
private void drawText(Graphics g, int centerX, int centerY)
{
int fontHeight = g.getFont().getHeight();
int textHeight = 2 * fontHeight; //文本总的高度
int topY = centerY - textHeight / 2; //topY用于定位文本的合适位置
g.drawString("逃亡者游戏",centerX, topY, Graphics.HCENTER | Graphics.TOP);
g.drawString("版本: " + midlet.getAppProperty("MIDlet-Version"),
centerX,topY + fontHeight,Graphics.HCENTER | Graphics.TOP);
}
3.闪屏线程
闪屏线程的主要工作为接受玩家键盘事件退出或者等待 3秒退出,代码如下:
public void run()
{
synchronized(this)
{
try
第 14章 单屏游戏的设计与实现
·479·
{
wait(3000L); //等待 3秒
}
catch (InterruptedException e)
{
//捕获线程中断异常
}
dismiss();
}
}
4.结束线程
有两种可能会结束闪屏线程,一为在 run方法中线程等待 3秒后调用,二为玩家按下键
盘,触发键盘事件,调用 keyPressed方法,在这个方法中结束闪屏线程。
public synchronized void keyPressed(int keyCode)
{
dismiss();
}
dimiss方法如下,它将回调midlet的 splashScreenDone方法,显示游戏菜单:
private void dismiss()
{
if (!dismissed)
{
dismissed = true;
midlet.splashScreenDone();
}
}
14.2.3 游戏菜单MenuList类的实现
菜单列表是在 splash 屏幕之后,或者游戏结束时,或者游戏中用户按下一个非游戏键时
(从而暂停了游戏)显示的屏幕。如果游戏被暂停,在菜单列表的顶部还有一个额外的菜单项
“继续游戏”。如图 14-9所示。
图 14-9 游戏菜单
J2ME手机游戏设计
·480·
1.构造函数
MenuList类是 List类的子类,并且实现了 CommandListener接口,它的构造函数为,首
先它实现 List类的构造函数,然后添加了三个基本选项以及 Command:
MenuList(escapeeMIDlet midlet)
{
super("逃亡者", List.IMPLICIT); //实现父类 List的构造方法,指定 List名和选择方式
this.midlet = midlet;
append("开始新游戏", null);//添加新游戏选项
append("高分记录", null); //添加高分纪录选项
append("游戏说明", null); //添加游戏说明选项
exitCommand = new Command("退出", Command.EXIT, 1); //定义并添加退出软键
addCommand(exitCommand);
setCommandListener(this); //绑定侦听器侦听选项以及软键
}
2.继续游戏选项
当游戏中间被玩家中止时,游戏菜单中将会多一个“继续游戏”的选项,这个选项添加
与否取决于外部传入的布尔值和 gameActive值,前者为外部通知菜单游戏是否运行,后者和
继续游戏选项是否存在对应(初始情况下为 false)。
void setGameActive(boolean active)
{
if (active && !gameActive) //如果游戏正在运行,且不存在“继续游戏”选项
{
gameActive = true;
insert(0, "继续游戏", null); //在选项顶部插入继续游戏选项
}
else if (!active && gameActive) //如果游戏不在运行,但存在“继续游戏”选项
{
gameActive = false;
delete(0); //删除继续游戏选项
}
}
3.处理事件
游戏菜单的事件处理包括两种,一种是 Command 触发的事件,这里为退出程序,另一
种是菜单选项的处理,例如开始游戏、显示高分纪录等,处理方式根据选择不同回调 midlet
的相应方法,让midlet来负责屏幕的切换。
public void commandAction(Command c, Displayable d)
{
if (c == List.SELECT_COMMAND)
{
int index = getSelectedIndex(); //获得选择项的序号
if (index != -1) // should never be -1
{
if (!gameActive)//如果不存在继续游戏选项,则序号往后移位,以便和 case选项对应
{
第 14章 单屏游戏的设计与实现
·481·
index++;
}
switch (index)
{
case 0: //继续游戏
midlet.menuListContinue();
break;
case 1: //新游戏
midlet.menuListNewGame();
break;
case 2: //高分纪录
midlet.menuListHighScore();
break;
case 3: //游戏介绍
midlet.menuListInstructions();
break;
default:
//按照逻辑不可能出现这种情况