中北大学2010届毕业设计说明书
息(效果如图4.2),第三项则是重新显示与作品和作者相关的logo画面。 4.2 主游戏逻辑及其涉及到的若干类
BattleCanvas主管着所有类之间的协调,决定何时死亡,何时分配新的敌人,及控制敌人出现处的闪光图标、游戏结束后的动态Gameover字样。它运行在独立的线程中,以恒定的频率刷新画面。刷新速度需大于30/秒才能使画面显示因人眼的暂时停留效应流畅运行。本程序设置为20毫秒。其主逻辑如图4-4所示。
程序中建立了另外的两个类,分别表述了敌人坦克和玩家坦克的功能。它们分别为:EnemySprite和UserSprite。这两个类均在BattleCanvas中建立了对象,以便进行统一调度。BattleCanvas包括了LayerManager,这样所有静态和动态的图象都不需要手动刷新,只需要在LayerManager中加入所有的需控制的元素,再统一由LayerManager刷新即可。因此,有必要在其中创立一个LayerManager的对象。 其他,如Sprite类的gameover字样、记分统计画面也都需在此主逻辑中建立相应对象。还需保存的变量有,游戏开始时间、结束时间(用于统计分数)、敌人的总数、屏幕上敌人的数量、下一个敌人需要出现的位置(总共允许在三个不同的位置出现,分别位于屏幕的左、中、右方)、游戏是否已成功结束或是否已死亡。 构造函数中,需初始化地图。地图实际即为TiledLayer的一个对象,可调用setCell设置其具体的图象格内容。地图由外部文件读入。外部文件分别命名为level*.png,利用MIDP中唯一获取外部文件为程序内资源的getResourceAsStream()函数将地图文件读入程序。在创建了InputStream类的map对象后,使用read()函数可将流中的下一个字节读出,并返回此字节代表的整数。每个整数代表一种障碍物。用二维循环将读出的每个整数,通过setCell()将整幅地图画出即可。地图文件可用十六进制的文本编辑器生成,如本程序使用的Ultraedit。
绘出地图后,可用LayerManager的append()将地图放置在第一层。这是很有必要的。因为地图上的障碍物之一——草,在坦克运行中时是必须处于坦克的上层的,否则将失去真实性。为此,地图必须首先载入。
由于敌人将依次出现在屏幕上,同时出现的数量应当受到控制。本程序设置为6。所以在构造函数中,也应当分配6个EnemySprite对象的内存空间。构造坦克时,将把坦克的png图片作为参数传递给EnemySprite和UserSprite,BattleCanvas中创建坦克仅调用createEnemy()和createUser()实现。
第 16 页 共30 页
中北大学2010届毕业设计说明书
在构造函数自己调用了线程的start后,程序将开始循环运行,直至跳出while的循环。每次循环中将检测是否死亡,屏幕上坦克的数量,是否该过关统计分数,检测用户输入的按键、重绘整个屏幕及回收垃圾内存(Garbage Collection)。
当敌人坦克完全死亡时(enemyNum为0),需调用System类的currentTimeMillis()赋值给结果的时间。接着调用setCurrent()显示统计分数的画面,为了进入下一关,统计画面只是停留四秒,就重新转回BattleCanvas画面。当然,如果当前已是最后一关,就不会再转回。进入下一关时,许多变量需要重新被初始化,如地图的绘制、敌人出现位置的重置、敌人的数量、玩家坦克的当前位置。
如果游戏未结束,则需判断屏上坦克是否已小于还剩坦克的总数,如果是这样,就需要再提供一辆坦克。提供新坦克之前,在屏幕上设置了一个专用指示的闪光符号,它继承了Sprite,运行在单独的线程中。以在两秒钟内反复闪现两次为一个生命周期。当它闪光完毕后,敌人就会从闪光位置出现。这样可提示玩家具体敌人将在什么时刻出现,以便做好准备。闪光位置设置了三处坐标,由于敌人不能同时出现,便设置了enemyOutDelay的倒数计时,每次屏幕刷新会减少一次计数,直到为0时就准备一辆坦克。本程序设置的两次坦克出现的最小间隔为2秒。
如果玩家已经死亡,就需要使用LayerManager的insert()将gameover字样的图片覆盖到最上层,效果如图4.3所示。
在检测用户输入的input()函数中,当按方向键时,玩家坦克就将向不同的方向运行,这调用了UserSprite的go()函数;当开炮时,就调用其fire()函数,作出相应的行为。
第 17 页 共30 页
图4.3 游戏结束的画面
图4.4 装载中的画面
中北大学2010届毕业设计说明书
在出现正式画面前设置了一个loading state*字样的单独屏幕,调用了
loadinglevel()函数,并停滞了1500毫秒,提示用户做好准备。效果如图4.4所示。
在绘图的render()过程中,除了要重绘坦克、地图、子弹外,还会在右边空白处绘出一个生命统计栏。并反复使用Graphics的drawLine()、drawImage()绘画出一个三维的效果,增强视觉感。该三维栏的上方为白色,下方为黑色,就创造了立体感。在每次刷新绘图页面时,应使用GameCanvas的flushGraphics()将屏幕后台的缓冲区内的图象刷新到前台来。
在允许敌人出现前,需要检测给即将出现的敌人分配一个数组序号。在程序中调用了getNullEnemyIndex()进行测试,当返回为-1时说明没有序号可以分配,否则,将返回空的序号。
游戏的最终运行状态如图4.5所示。
4.3 坦克的共同行为
在TankSprite中定义了所有坦克(包括敌方坦克和玩家坦克)的共同行为和属性。EnemeySprite和UserSprite都继承了该类以简化结构。在transformDirection[]中定义了坦克四个方向分别应将原始图片旋转的角度,分别为
TRANS_NONE,TRANS_ROT90,TRANS_ROT180,TRANS_ROT270,以便在后来的setTransform()中将这些常量代入。构造函数中创建了每个坦克必须拥有的一颗子弹,这些子弹就将只跟随自己的坦克调动。
相关代码:private static int transformDirection[]= {
第 18 页 共30 页
图4.5 游戏运行中的模拟器画面
中北大学2010届毕业设计说明书
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 IOException{ super(image);
defineReferencePixel(1, 3); //center and bottom of the bullet }
为了能提前预测碰撞,调用了defineCollisionRectangle(0,-1,11,12)将碰撞矩形向前设置了一个象素,具体原理见第二章。
在setBulletDirection()中,将根据坦克当前的方向确定子弹出膛后的方向,其中setRefPixelPosition()将子弹的参考点设置在其未变形状态的底部,setXY()将其放置到炮口的位置,setTransform()将其图片方向转到需要使用的位置。
canPass()函数将返回坦克是否能够向前前进,考虑到的因素有边界、障碍物。它返回一个boolean值,提供给go()函数,做进一步的判断。getTileIndex()将检测传递来的象素处是什么类型的障碍物,它将象素除以8(即障碍物的象素宽度),取整,再通过getCell()得到。在得到障碍物属性后,判断其序号是否与草相同,或是否为空(序号为0)。因为所有的障碍物中只有草不会阻碍坦克的向前运行。 4.4 玩家坦克的功能属性
构造函数中需要将坦克方向设置为向上,因为刚出现时就是这样的状态。当开炮时,调用BulletSprite的setLayerManager()将子弹与layerManager联系起来。需要联系的还有自身坦克、地图。这些都由坦克传递给子弹。因为子弹是属于坦克的,它的属性需要跟当前的坦克保持一致。接着使用append()将子弹贴到layerManager上显示出来。最终调用其start()开始子弹自己的线程。子弹一旦开始运行,就脱离了当前坦克的控制,直到其生命周期终止。
无论子弹是属于敌人还是玩家的,它都必须记录自己的来源和攻击的对象。在玩家坦克发射的子弹中,就必须将攻击对象设置为所有的敌人。这样,它才能有扫描的目标。在setShootCheck()的参数中,传给子弹的是敌人的数组,子弹的对象就
第 19 页 共30 页
中北大学2010届毕业设计说明书
被确定了。
die()、resetPosition()、getLife()都是很简短的函数,但却提供必不可少的功能。他们可被外部调用,以取得生命值、死亡记数、重置位置。
在go()函数,每个方向在走前都须用if (canPass(UP)&&!collidesEnemy()) 检测是否可以行走。canPass()检测是否有障碍物及是否到边界。collidesEnenmy检测是否前方有坦克阻碍行动。当可以行走时,就在当前方向的坐标上增加或减少一个象素。
在collidesEnemy()函数中,将有一个for循环按照敌人数组的序号依次检测6次。有一点非常重要:在检测前,需要将敌人的检测矩形区域设置为与原来图片一样大小。否则,当玩家向上走,而有敌人从左方向右走,并且已经碰撞到玩家坦克时,玩家坦克会因为被判定已与敌人发生碰撞而不允许前进。事实上,敌人坦克此时并没有阻碍玩家前进。这样的判断必须排除在考虑范围外。当然,在设置完成后,必须将将检测区域设置回原先的状态,否则敌人在往后自己的检测中将发生错误。 4.5 敌人坦克的功能属性
由于和UserSrite同属于一个TankSprite的继承类,其功能就与UserSprite有很大的相似之处,但也有其自身的特别属性。其主要功能流程图见图4.6。
首先,EnemySprite继承了Runable接口。因为敌人的运行是自动的,需要有设定的程序让它可自己控制,而不像UserSprite完全通过每次输入的键盘信号来做出反映。因此,它可以运行在单独的线程中。
setEnemyShootCheck()函数与UserSprite中的一样,设置了攻击的对象,并且此函数将继续把参数传递给自身的子弹,以便子弹可以识别攻击对象。此函数由BattleCanvas调用。
getRandomDirection()以当前系统时间作为种子,调用了Random类的nextInt()产生一个随机的整数,此整数取除4的余数的绝对值作为随机的方向。
Random random=new Random(System.currentTimeMillis()); return Math.abs(random.nextInt())%4+1;
此时返回的值的范围就确定在1~4之间,正好对应四个方向。将他们代入需要使用方向的函数中就可以使用了。
getRandomStep()的原理类似:
第 20 页 共30 页