1、手机版疯狂坦克游戏设计与开发摘 要Java 良好的跨平台特性在移动平台的开发中显示出了巨大的威力。Java 语言面向对象的优势也使得开发游戏变得非常容易。随着手机的日益普及、Java 功能在移动设备上的实现, Java 应用程序产生的手机增值服务逐渐体现出其影响力,对丰富人们的生活内容、提供快捷的资讯起着不可忽视的作用。本论文着眼于 J2ME 技术的应用,开发一款可商用的手机游戏程序坦克大战。本程序的界面和运作方式继承于日本任天堂公司在 20 世纪 80 年代开发的 Battle City 游戏,将老少皆宜的经典作品移植到手机上来,为更流行的硬件平台提供应用软件。本论文介绍了任天堂红白机的软硬
2、件特性、J2ME 的相关技术及本程序的结构分析和具体功能的实现。关键词:J2ME,手机游戏,Java,坦克大战目录1 绪论 31.1 开发背景 31.2 开发工具 31.3 开发意义 32 需求分析 62.1 功能需求分析 .62.2 界面需求分析 .63 总体设计 73.1 主要功能 .73.2 程序流程 84 详细设计和具体实现 94.1 游戏进入前的选择 .94.2 主游戏逻辑及其涉及到的若干类 .104.3 坦克的共同行为 .134.4 玩家坦克的功能属性 .144.5 敌人坦克的功能属性 .154.6 子弹的运行和控制 .194.7 记分系统 215.测试与实现 .226 程序的总结
3、和展望 .231 绪论1.1 开发背景在信息社会中,手机及其他无线设备越来越多的走进普通百姓的工作和生活,随着信息网络化的不断进展,手机及其他无线设备上网络势在必行。但是传统手机存在以下弊端:1. 传统手机出厂时均由硬件厂商固化程序,程序不能增加、删除,有了错误也不能更新、修改,若要增加新功能必须另换一部手机。2. 传统手机访问互联网是通过 WAP(Wireless Application Protocal),所有网络资源必须接通网络才能在线访问,非常耗时、费用亦很高。而 Java 技术在无线应用方面的优势非常明显:1. 应用程序可按需下载,而不是购买由硬件商提供的套件,可升级空间大。2. J
4、ava 技术提供了一个类库,它使的应用开发商可以创建更为直觉、丰富的用户界面(GUI);3. Java 技术使网络带宽的应用更为有效,因为应用程序可以下载到器件上,并在本地运行,仅仅是在连接到服务器时才会占用网络带宽。基于以上分析,Java 手机将是未来手机的发展方向,是业界的热点。1.2 开发工具操作系统:Microsoft Windows XP程序语言:Java 2开发包: Java(TM) 2 Standard Edition (build 1.4.1_02-b06) Sun Micro. J2ME Wireless Tool Kit 2.1IDE: Borland eclipse1.3
5、 开发意义图 1.1 Java 平台的结构虽然 Java 已经被用到许多企业级软体上,可是其实骨子里面还是非常适合用在嵌入式系统之中。Java 平台演进到 Java2 后, Java 平台分别针对不同领域的需求被分成四个版本,亦即 J2EE、J2SE、J2ME 以及 JavaCard(其结构示意图见图 1.1)。其中 J2ME 定位在消费性电子产品的应用上。这个版本针对资源有限的电子消费产品的需求精简核心类库,并提供了模块化的架构让不同类型产品能够随时增加支持的能力。这个版本的应用层面相当广泛,会是未来 Java平台发展的重点项目。J2ME 在设计其规格的时候,遵循着 “对各种不同的装置而造出
6、一个单一的开发系统是没有意义的事”这个基本原则。于是 J2ME 先将所有的嵌入式装置大体上区分为两种:一种是运算功能有限、电力供应也有限的嵌入式装置( 比方说 PDA 、手机);另外一种则是运算能力相对较佳、在电力供应上相对比较充足的嵌入式装置 (比方说冷气机、电冰箱、电视机上盒 (set-top box)。因为这两种型态的嵌入式装置,所以 Java 引入了一个叫做Configuration 的概念,把上述运算功能有限、电力有限的嵌入式装置定义在Connected Limited Device Configuration(CLDC)规格之中;而另外一种装置则规范为 Connected Devi
7、ce Configuration(CDC)规格。也就是说, J2ME 先把所有的嵌入式装置利用 Configuration 的概念区隔成两种抽象的型态。J2ME 平台被认为是最杰出的手机游戏平台,它为开发者、设备制造商、网络通信公司和消费者广泛接受。它有一些非常重要的特征对所有组织都有益。因为 J2ME 应用在不同设备上都是便携式的,他们常常可在网络上下载和执行。如果没有正确的防范,它则为用户和无线通信公司冒着无数个安全的风险。幸运的是,Java被设计成一种安全的语言。所有字节码应用在执行之前都要校验;JVM 在执行过程中监督应用的安全性和存储违反问题。MIDP v2 运行时间包括一个完全特征
8、化的、基于域的安全管理员,它在应用的数字签名者鉴别的基础上赋予应用API 级许可。手机游戏的盈利主要是由于它们的涉及面很广。手机已经与现代生活方式紧紧地结合在一起。他们是最普遍携带的个人用品中仅次于钥匙和钱包的东西。传统的台式机游戏将目标锁定在低级趣味的人和青少年身上,而手机游戏则每个人都可以访问的到随时,随地。尽管每个手机游戏都不贵,但是巨大的使用量(如:每人每星期一个新游戏)将使得这个市场商机无限。但是,对于开发者来说,将控制台游戏迁移到手机游戏工程很大。因为他们所面向的对象、生活方式和分布式模型都有着极大的区别。一个成功的手机游戏大多具有以下特征: 易于学习: 既然手机游戏面向的是普通消
9、费者而不是计算机专家,那么他们不可能深入的学习游戏技巧。消费者不会花几个小时去研究一个 3 元的手动操作的游戏。保持游戏的简单是最基本的要求。 可中断性: 多任务处理是手机生活方式的基本特征。手机用户常常在任务(如等一个电子邮件或者等车)之间有一小段时间。而游戏、日历管理、通讯和工作数据访问使用的是同一个设备。所以一个好的手机游戏应该提供短时间的娱乐功能,并且允许用户在游戏和工作模式之间顺利切换。 基于订阅:手机游戏的盈利成功取决于他们巨大的使用量。一开始开发和设计每个游戏都是昂贵的。如果一个手机游戏开发者要赢利的话,重要的是:同一个游戏引擎,多个标题,基本的故事情节类似。基于订阅的游戏是不断
10、产生收入的最好方法。 丰富的社会交互: 不管一个游戏设计得多好,只要玩家找到了它的根本模式或者玩完了所有的游戏路径很快就会厌烦这个游戏。对于一个基于订阅的游戏,重要的是与别的玩家合作以增强所玩游戏的智力和随机性。在今天纷繁复杂的多玩家游戏中具有丰富社会交互的游戏证明是成功的。 利用手机技术的优点: 巨额的手机技术研发费用都花在提高设备和网络的可用性和可靠性上面。因此,手机设备硬件和网络协议与桌面/控制台世界(如全球定位系统(GPS)扩展、条形码扫描仪、和短消息服务 (SMS)/多媒体信息服务(MMS)通讯)有着非常大的差别。好的手机游戏应该利用那些更新的设备特征和网络基础设备的优点。 无违法内
11、容:既然所有年龄/性别的人群都玩手机游戏并且常常在公共 /工作场合,就应该避免明显的暴力或者色情内容。2 需求分析J2ME 非常适合开发手机上的应用程序,尤其是手机游戏开发。手机坦克大战游戏就是个非常有趣的手机应用。涉及到动画显示、按键检测、数据读写、Sprite、图层管理、多线程和双缓冲等多项 J2ME 关键技术,充分发挥了 J2ME用于嵌入式开发的大部分功能和特性,并引入了人工智能控制。2.1 功能需求分析游戏支持 2 名玩家同时进行战斗,并附加有任务编辑器,可以任意创建关卡。每关需要在复杂的地形上摧毁 20 辆敌人坦克车辆才能通过,如果玩家的坦克被摧毁多次或己方基地被毁即算任务失败。游戏
12、中可以获取有多种功能的宝物,敌人种类则包括装甲车、轻型坦克、反坦克炮、重型坦克 4 种,且存在炮弹互相抵消和友军火力误伤的设定。2.2 界面需求分析游戏中为了美观,适用性强,可能需要采用外部文件引入的图片贴图。坦克大战(Battle City)地形包括砖墙、海水、钢板、森林、地板 5 种。游戏的结束、开始、动态信息画面作为构成一个完美程序都是必不可少的重要部分。良好的用户界面更是吸引用户的硬指标,相关的美术构图也需要有一定的考虑。3 总体设计3.1 主要功能GameCanvas 中提供了与以往 MIDP1.0 不同的键盘采样功能。Canvas 类中采取响应键盘事件的方法,每次执行周期时会读取
13、keyPressed 函数中需执行的代码。这样的机制并不适合某些游戏场合。在某些不支持 keyRepeat 功能的设备上,反复执行的按键,比如发射子弹,将不能因为长时间按压而自动重复,这样就需要用户高频率的手动击键,这在操纵空间非常有限的移动设备上是非常困难的。同时,事件的执行周期也并不一定适合游戏的场合,也许需要更高频率执行的按键却只能在指定的周期内规律的响应。对此,针对游戏的开发,Game 包提供的键盘状态功能将显得十分有效。GameCanvas 提供 getKeyStates 函数可获取当前键盘上的信息。将以位的形式返回键盘上所有键的按与释放的状态,当 bit 为 1 时,键就是被按下的
14、状态,为 0 时则为释放状态。只需要此一个函数的返回值就可以返回所有键的状态。这保证了快速的按键和释放也会被循环所捕捉。同时,这样的机制也可检测到几个键同时按下的状态,从而提供斜向运行等相应功能。敌方按照规则不能和用户坦克重合,则它每行走一步就需要把用户坦克扫描一次,判断其是否碰撞到了用户的坦克。Sprite 类中提供了 collidesWith 函数,用于判断是否与某个 TiledLayer、Sprite、Image 的对象有图象上的重合(即游戏中的碰撞) 。然而不能仅仅将用户坦克作为其 Sprite 参数传递给敌人的类进行判断。因为如果发生碰撞,collidesWith 成立,则两辆坦克已
15、经发生了图象重合,违反了规则,甚至若再进行 collidesWith 判断的话,其结果将永为真。为了提前预知碰撞,可以将所有坦克的碰撞范围设定为一个比坦克图片稍大一些的矩形,此矩形仅在坦克前方比坦克图形多出一个象素。在多出的 11 个象素中,按照每个象素依次检查此象素是否于外界发生碰撞,如果不是按照象素检查,则当坦克与障碍物错位并同时与两种物体接触时将有可能忽略检测其中的一样物体。这样,就可以提前一步判断。如果发生碰撞,则坦克应当选择掉转方向,此时,两辆碰撞的坦克又因为其矩形区域不重合而不符合collidesWith 的条件,就可以继续正常运行了。敌方坦克由于需要具有一定的智能性,以便对玩家攻
16、击,使之具有一定的可玩性。敌人可以自动行走,但是应当在以下适当的情况下转向:首先是是否超出界面的边界,其次是是否与地图障碍物发生了碰撞,再次是是否与用户坦克发生了碰撞。需要指出的是,当发生阻碍不能在不变方向的情况下继续行走时,并不一定立即需要采取转向的对策。如果一定发生转向,试想,当敌方碰到玩家时,如果它立即转向,将不会对玩家发射射向他的子弹,就不构成任何威胁,当然,也不能永远不转向。本程序设置为:当碰撞到障碍物或边界时立即转向,但碰到玩家坦克时需要有一个等待的时间,这个时间由碰撞前随机取得的在某方向上的持续行走步数决定,当发生坦克间碰撞时,此随机数将在下一次循环前减少为原来的 2/3,这样就
17、实现了加快转向的时间,避免死锁在一个方向上静止的停留过长的时间。另外,坦克的发炮间隔和转后的具体方向都由随机数决定。坦克之间由以上道理也不会发生重叠,但当某坦克正从上方生成而正巧有另一辆阻碍在其生成点处,这将导致不可避免的重合。这是允许的,但需要对他们标注状态,即当坦克刚出现时暂时允许重合,一旦在某个时间他们脱离了重合状态,就不能在允许重合,如果不设置这样的判断,刚出现的坦克将会因为受到阻塞而永远不能前进,坦克将混成一团。本程序中并未使用过多复杂的人工智能算法,如有时间,将可能再此方面加以完善。3.2 程序流程本程序采用面向对象的设计模式,对游戏中的所有物体赋予对象的概念和属性。用户控制的坦克
18、运行在主线程中,随屏幕刷新的频率而步进。敌方坦克将在游戏开始时逐渐新增线程,每增加一个敌方对象就新增加一条线程,一旦线程数满到最大值(本程序暂设置为 6) ,就不允许敌人再继续出现。用户坦克自诞生之时起将拥有一Logo 画面选项画面主程序屏幕绘图本关记分统计显示GameOver显示历史记分表About开始敌方需要出坦克时,生成坦克初始化参数死亡时符合结束条件时图 3.2 本程序的主流程图发子弹,子弹虽然开在单独的线程中,但运行结束后(比如撞到相关物体或敌方坦克时)并不结束子弹对象,只是将其线程终止。用户再次发射子弹时只是将终止的线程再次激活。在屏幕重绘的主程序中,将在每次的循环中判断若干事件。
19、如:用户坦克的生命是否已完全用尽,敌方坦克数是否已经为零,屏幕上的坦克数量是否少于仍剩下的坦克数量等。以便程序进入相关的分支执行相关的反应代码,结束游戏或统计分数等。主程序流程如图 3.2 所示:程序为需要完成独立功能的需显示的模块设置了单独的类。TankMain 类是继承自 MIDlet 的控制主程序启动的首先被载入系统的部分。载入程序后首先启动的是程序介绍的信息画面。闪过后载入 StartChoice 类,为用户提供可选择的选项。在选择开始后,将运行 BattleCanvas 类中的总流程控制。它决定了游戏何时该结束,何时分配敌人数量,GameOver 字样的闪现规则,地图的绘制及整个游戏
20、的调度。4 详细设计和具体实现4.1 游戏进入前的选择每个 MIDlet 程序都必须有一个主类,该类必须继承自 MIDlet。它控制着整个程序的运行,并且可以通过相应函数从程序描述文件中获取相关的信息。该类中拥有可以管理程序的创建、开始、暂停(手机中很可能有正在运行程序却突然来电的情况,这时应进入暂停状态。结束的函数。进入时,首先载入画面的不是游戏运行状态,而是提供选项,当再次选择Start Game 时才正式运行。运行画面如图 4.1 所示。因此,在 TankMain 的构造函数中分配了 StartChoice 类,即选项画面的内存空间。在 startApp()函数中,随即调用了 Displ
21、able 的 setCurrent()函数将当前屏幕设置为 startChoice。图 4.1 游戏前的选项画面 图 4.2 使用说明画面startChoice 继承了接口 commandListener,这样,就可以使用高级界面的Command 按钮。继承了 commandListener 的类必须拥有 commandAction(),以决定对按键采取什么样的行为。即按钮事件触发后需执行的函数。在设置好commandlistener 后,需要调用 setCommandListener()以将按钮事件激活。键盘事件中,可用 getCommandType()返回的 Command 类型来确定选择
22、的是什么按钮。startChoice 继承了 List 类,用于显示列表选项,使用其 append()函数可将选项加入到列表中。getSelectIndex()可检测到选择的项目的序号,序号从 0 开始递增。其中,当选择第一项时将载入正式游戏画面 BattleCanvas 类,第二项将显示帮助信息( 效果如图 4.2),第三项则是重新显示与作品和作者相关的 logo 画面。4.2 主游戏逻辑及其涉及到的若干类BattleCanvas 主管着所有类之间的协调,决定何时死亡,何时分配新的敌人,及控制敌人出现处的闪光图标、游戏结束后的动态 Gameover 字样。它运行在独立的线程中,以恒定的频率刷
23、新画面。刷新速度需大于 30/秒才能使画面显示因人眼的暂时停留效应流畅运行。本程序设置为 20 毫秒。其主逻辑如图 4-4 所示。程序中建立了另外的两个类,分别表述了敌人坦克和玩家坦克的功能。它们分别为:EnemySprite 和 UserSprite。这两个类均在 BattleCanvas 中建立了对象,以便进行统一调度。BattleCanvas 包括了 LayerManager,这样所有静态和动态的图象都不需要手动刷新,只需要在 LayerManager 中加入所有的需控制的元素,再统一由 LayerManager 刷新即可。因此,有必要在其中创立一个LayerManager 的对象。其他
24、,如 Sprite 类的 gameover 字样、记分统计画面也都需在此主逻辑中建立相应对象。还需保存的变量有,游戏开始时间、结束时间(用于统计分数) 、敌人的总数、屏幕上敌人的数量、下一个敌人需要出现的位置(总共允许在三个不同的位置出现,分别位于屏幕的左、中、右方) 、游戏是否已成功结束或是否已死亡。构造函数中,需初始化地图。地图实际即为 TiledLayer 的一个对象,可调用 setCell 设置其具体的图象格内容。地图由外部文件读入。外部文件分别命名为 level*.png,利用 MIDP 中唯一获取外部文件为程序内资源的getResourceAsStream()函数将地图文件读入程序
25、。在创建了 InputStream 类的map 对象后,使用 read()函数可将流中的下一个字节读出,并返回此字节代表的整数。每个整数代表一种障碍物。用二维循环将读出的每个整数,通过setCell()将整幅地图画出即可。地图文件可用十六进制的文本编辑器生成,如本程序使用的 Ultraedit。绘出地图后,可用 LayerManager 的 append()将地图放置在第一层。这是很有必要的。因为地图上的障碍物之一草,在坦克运行中时是必须处于坦克的上层的,否则将失去真实性。为此,地图必须首先载入。由于敌人将依次出现在屏幕上,同时出现的数量应当受到控制。本程序设置为 6。所以在构造函数中,也应当
26、分配 6 个 EnemySprite 对象的内存空间。构造坦克时,将把坦克的 png 图片作为参数传递给 EnemySprite 和UserSprite,BattleCanvas 中创建坦克仅调用 createEnemy()和 createUser()实现。在构造函数自己调用了线程的 start 后,程序将开始循环运行,直至跳出while 的循环。每次循环中将检测是否死亡,屏幕上坦克的数量,是否该过关统计分数,检测用户输入的按键、重绘整个屏幕及回收垃圾内存(Garbage Collection)。当敌人坦克完全死亡时(enemyNum 为 0),需调用 System 类的currentTime
27、Millis()赋值给结果的时间。接着调用 setCurrent()显示统计分数的画面,为了进入下一关,统计画面只是停留四秒,就重新转回 BattleCanvas 画面。当然,如果当前已是最后一关,就不会再转回。进入下一关时,许多变量需要重新被初始化,如地图的绘制、敌人出现位置的重置、敌人的数量、玩家坦克的当前位置。如果游戏未结束,则需判断屏上坦克是否已小于还剩坦克的总数,如果是这样,就需要再提供一辆坦克。提供新坦克之前,在屏幕上设置了一个专用指图 4.3 游戏结束的画面示的闪光符号,它继承了 Sprite,运行在单独的线程中。以在两秒钟内反复闪现两次为一个生命周期。当它闪光完毕后,敌人就会从
28、闪光位置出现。这样可提示玩家具体敌人将在什么时刻出现,以便做好准备。闪光位置设置了三处坐标,由于敌人不能同时出现,便设置了 enemyOutDelay 的倒数计时,每次屏幕刷新会减少一次计数,直到为 0 时就准备一辆坦克。本程序设置的两次坦克出现的最小间隔为 2 秒。如果玩家已经死亡,就需要使用 LayerManager 的 insert()将 gameover 字样的图片覆盖到最上层,效果如图 4.3 所示。在检测用户输入的 input()函数中,当按方向键时,玩家坦克就将向不同的方向运行,这调用了 UserSprite 的 go()函数;当开炮时,就调用其 fire()函数,作出相应的行为
29、。在出现正式画面前设置了一个 loading state*字样的单独屏幕,调用了loadinglevel()函数,并停滞了 1500 毫秒,提示用户做好准备。效果如图 4.4 所示。在绘图的 render()过程中,除了要重绘坦克、地图、子弹外,还会在右边空白处绘出一个生命统计栏。并反复使用 Graphics 的 drawLine()、drawImage()绘画出一个三维的效果,增强视觉感。该三维栏的上方为白色,下方为黑色,就创造了立体感。在每次刷新绘图页面时,应使用 GameCanvas 的 flushGraphics()将屏幕后台的缓冲区内的图象刷新到前台来。在允许敌人出现前,需要检测给即
30、将出现的敌人分配一个数组序号。在程图 4.4 装载中的画面序中调用了 getNullEnemyIndex()进行测试,当返回为-1 时说明没有序号可以分配,否则,将返回空的序号。游戏的最终运行状态如图 4.5 所示。4.3 坦克的共同行为在 TankSprite 中定义了所有坦克( 包括敌方坦克和玩家坦克 )的共同行为和属性。EnemeySprite 和 UserSprite 都继承了该类以简化结构。在transformDirection中定义了坦克四个方向分别应将原始图片旋转的角度,分别为 TRANS_NONE,TRANS_ROT90,TRANS_ROT180,TRANS_ROT270,以便
31、在后来的 setTransform()中将这些常量代入。构造函数中创建了每个坦克必须拥有的一颗子弹,这些子弹就将只跟随自己的坦克调动。相关代码:private static int transformDirection=TRANS_NONE,TRANS_ROT90,TRANS_ROT180,TRANS_ROT270;private int currentDirection=0; /if no direction defined,it will/stay where it is.public BulletSprite(Image image) throws IOExceptionsuper(im
32、age);defineReferencePixel(1, 3); /center and bottom of the bullet图 4.5 游戏运行中的模拟器画面为了能提前预测碰撞,调用了 defineCollisionRectangle(0,-1,11,12)将碰撞矩形向前设置了一个象素,具体原理见第二章。在 setBulletDirection()中,将根据坦克当前的方向确定子弹出膛后的方向,其中 setRefPixelPosition()将子弹的参考点设置在其未变形状态的底部,setXY()将其放置到炮口的位置,setTransform() 将其图片方向转到需要使用的位置。canPas
33、s()函数将返回坦克是否能够向前前进,考虑到的因素有边界、障碍物。它返回一个 boolean 值,提供给 go()函数,做进一步的判断。getTileIndex()将检测传递来的象素处是什么类型的障碍物,它将象素除以 8(即障碍物的象素宽度) ,取整,再通过 getCell()得到。在得到障碍物属性后,判断其序号是否与草相同,或是否为空(序号为 0)。因为所有的障碍物中只有草不会阻碍坦克的向前运行。4.4 玩家坦克的功能属性构造函数中需要将坦克方向设置为向上,因为刚出现时就是这样的状态。当开炮时,调用 BulletSprite 的 setLayerManager()将子弹与 layerMana
34、ger 联系起来。需要联系的还有自身坦克、地图。这些都由坦克传递给子弹。因为子弹是属于坦克的,它的属性需要跟当前的坦克保持一致。接着使用 append()将子弹贴到 layerManager 上显示出来。最终调用其 start()开始子弹自己的线程。子弹一旦开始运行,就脱离了当前坦克的控制,直到其生命周期终止。无论子弹是属于敌人还是玩家的,它都必须记录自己的来源和攻击的对象。在玩家坦克发射的子弹中,就必须将攻击对象设置为所有的敌人。这样,它才能有扫描的目标。在 setShootCheck()的参数中,传给子弹的是敌人的数组,子弹的对象就被确定了。die()、resetPosition()、ge
35、tLife()都是很简短的函数,但却提供必不可少的功能。他们可被外部调用,以取得生命值、死亡记数、重置位置。在 go()函数,每个方向在走前都须用 if (canPass(UP)return Math.abs(random.nextInt()%4+1;此时返回的值的范围就确定在 14 之间,正好对应四个方向。将他们代入需要使用方向的函数中就可以使用了。getRandomStep()的原理类似:Random random=new Random(System.currentTimeMillis();return (Math.abs(random.nextInt()%4)*50;只是需要乘以每秒会刷
36、新的屏幕的次数。这样就相当于允许在某一个方向运行03 秒钟的时间。每个敌人还需要拥有一个内部的所有敌人的数组元素。这样,它们才可以自动检测自己是否与同伴发生了碰撞,以便采取躲避、转向等行动。 collidesWithOtherTank()将检测是否与其他坦克(包括敌人和玩家) 。一个循开始是否刚出现前进Y可否开炮开炮可否前进换向、取随机移动步数、随机开炮倒数记步数结束是否已死亡是否碰撞NYNYNYNN图 4.6 敌方坦克运行流程图开始是否出界是否击中物体将可以消除的障碍物消除玩家的子弹吗是否与任何敌人的子弹碰撞是否击中任何敌人敌人的子弹吗?是否与玩家的子弹碰撞是否击中玩家结束NYNYYN 子弹
37、抵消Y消除敌人YNNNNN 消除玩家子弹抵消图 4.7 子弹运行的主要功能流程图YYY环将依据敌人的序号查找 5 次。if(i=number)break ; 语句将避免检测到自己,永远返回真。collidesInOtherTank()虽与上面的函数很相似,但仍有一些细微不同,那就是不需要在检测前设置被检测方的矩形区域。因为不需要进行预先检测。此函数用来检测是否在刚出现时就与其他坦克发生碰撞的。如果一出现,出口就被堵死,显然,不能永远不出现,那就应采取其他的办法,否则两辆坦克将因为都处在碰撞状态中而无法移出。在运行的线程中,需在每前进的一步骤中循环做下列事件:如果坦克已死亡,立刻退出。 (由 b
38、oolean 值 destroyed 决定) 。如果不是刚出现(由 isBeginner 决定),判断是否与将其他坦克发生碰撞,就向当前方向前进一步骤,否则,将需要循环检测的当前随机步数减少为原先的2/3(为了加速离开的时间) 。如果刚出现,就直接走一步,具体如何行走将在go()函数中决定,并且此 go()与 UserSprite 中的有所区别。当随机发炮数减少到 0 时,就进行发炮的动作。发炮后应立即重新赋值给随机发炮数,以便重新倒数计算。当所有的步骤走完后,因为需要转动方向,于是,调用一次随机取得方向的函数再次获值。其他的随机值也应当重置。在 go()函数中首先检测是否正处于碰撞状态中,如
39、果不是,就需要取消Beginner 的状态,因为不需要 Beginner 这样的特殊身份,让别的坦克不检测了。在运行在某个方向上,当确定为 canPass 时,应再检测是否为 Beginner。如果是,就不应该受到其他坦克的影响而直接改变坐标,但若不是,就应当远地不动。相关代码如下:public void go(int direction) /method for tank to go forwardif(!collidesInOtherTank()isBeginner=false;switch (direction)case UP:if (currentDirection = UP)if (
40、canPass(UP)if(isBeginner)y-;else if (!collidesWithOtherTank()y-;else setTransform(transformDirection0);currentDirection = UP;break ;case DOWN:if (currentDirection = DOWN)if (canPass(DOWN)if(isBeginner)y+;else if (!collidesWithOtherTank()y+;else setTransform(transformDirection2);currentDirection = DO
41、WN;break ;case RIGHT:if (currentDirection = RIGHT)if (canPass(RIGHT)if(isBeginner)x+;else if (!collidesWithOtherTank()x+;else setTransform(transformDirection1);currentDirection = RIGHT;break ;case LEFT:if (currentDirection = LEFT)if (canPass(LEFT)if(isBeginner)x-;else if (!collidesWithOtherTank()x-;
42、else setTransform(transformDirection3);currentDirection = LEFT;break ;setRefPixelPosition(x, y);4.6 子弹的运行和控制子弹继承了 Runnable,运行在独立的线程中。它拥有一个很重要的变量,isFromEnemy。它标识了该子弹是属于玩家的,还是敌人的,这样可以控制子弹在脱离坦克管束后的运行状态中的行为。其主要功能流程图见图 4.7。checkHit(int x,int y)调用了 getTileIndex(x,y)获取当前子弹击中的是什么障碍物,如果返回了 false 就表示没有击中任何东西。
43、当击中了需要作出反映的物体时,就分别采取措施:击中草时,由于没有定义相关函数,就不会有任何反映,会重合在草上正常通过;击中砖块时,将产生爆炸,调用 setCell 将当前块置为空,并产生爆炸效果。爆炸效果由 tileExplode(x,y)根据需要爆炸的坐标点生成,其中将一个 Sprite 图片在界面上闪现 150 毫秒。爆炸效果需要将图片 insert 进第 0 层,这样才不至于被其他景物所覆盖,爆炸结束后 layerManager 会自动相应调整。击中钢筋时,将只产生爆炸效果。setShootCheck(EnemySprite enemySprite);setUserSprite(User
44、Sprite userSprite);setEnemySprite(EnemySprite enemySprite)都是将相关的坦克传入到子弹类里来,以便确认来源或攻击目标。相关代码如下:private boolean checkHit(int x,int y) /check what tile the bullet had hit and what to doboolean hit=false;int index=getTileIndex(x,y);if(index=2) /if it is wall,eliminate it.tiledLayer.setCell(x / 8, y / 8,
45、 0);tileExplode(x, y);hit=true; / if the bullet hit a wall, it does hit something.else if(index=5) /if it is steel,ignore it but explode.tileExplode(x,y);hit=true; / if the bullet hit a steel pan,it does hit something,too. / hit any other things will be considered hit nothing(eg.grass,river);else if
46、(index=3)|(index=4)|(index=7)|(index=8)layerManager.remove(this); / the bullet should disappear immediatelyisEnd=true;while(userSprite.getLife()0)userSprite.die();/die many times until no lives left.return hit;子弹运行中,将用 collidesWith(tiledLayer,true)测试是否碰撞上了地图。如果为真,就继续检测碰撞上了什么样的物体。这将针对四个不同的方向分别以象素级检测。
47、如果击中了某样物体,那么 checkHit 自然会处理,子弹的生命周期结束,以 break 退出循环。如果没有击中物体,就继续检测是否击中了某辆坦克。这根据子弹的来源分为两种情况。当来自玩家时,将首先检测所有的敌人发出的子弹,当发生子弹间的碰撞时,用户的子弹将被移除,虽然按照道理敌人的子弹同时也应被移除,但敌人子弹是运行在另一线程中的,应当由它自己来控制为好,用户的子弹只需要管理好自己的状态就可以了。如果没有和子弹发生碰撞,就检测是否与敌人碰撞,发生碰撞时,将敌人从 layerManager 中移除,并置为 null,产生爆炸效果,敌人数量减少一位,敌人屏幕上数量减少一位。如果是来自敌人的子弹
48、,将同样检测与玩家子弹的碰撞,及与玩家坦克的碰撞如有碰撞,玩家生命数减少一位,位置重置。如果玩家生命已死亡殆尽,就需要在进行以上操作的同时将玩家坦克的位置放置到屏幕外的部分。因为layerManager 的 remove 函数并不会真正将层移除。只是用户看不见而已。如果不放置到屏外,敌人坦克仍会被阻挡,子弹仍会再次击中用户坦克。这将会是很荒唐的场面。为了能控制一辆坦克在同一时间只能发射一发子弹,在子弹生命运行结束时候,将调用 userSprite.enableShoot()恢复坦克继续发炮的能力。因为在发炮期间,坦克的再次发炮的功能是被锁定的。4.7 记分系统记分系统的功能指定在 ScoreS
49、creen 类内。采用的是手机专用的一种简化的数据库 RMS。在 BattleCanvas 请求调入分数统计的屏幕时,将首先调用 initRs()初始化一些环境变量。RecordStore.openRecordStore(“score“,true)创建一个名为“score”的数据库。随后将分别建立 ByteArrayOutputStream 和 DataOutputStream以便实现多字段的记录。首先将循环调用 dos.writeUTF(“NoName“) 和dos.writeInt(0),将记录中的条目的用户名置为 NoName,所有的分数置为0。score=baos.toByteArray() 将所得的被写入的数据流输送到 score 的字节数组中。Score 将被循环调用 10 次,将记录中的 10 个最高记录初始化。getScore()函数得到所需要序号的分数。其中 dis.readUTF() ,score=dis.readInt()将分别读出用户名和分数,然而前者读出后并不加以利用,因为在比较分