本文仅供个人记录和复习,不用于其他用途
致谢
http://blog.csdn.net/column/details/jackyairplane.html
游戏效果展示
游戏流程设计
我们的游戏有加载场景、游戏场景、结束场景这三个。加载场景不必多说,我们主要来说说游戏场景。我们操控的飞机就是英雄,它会不断地发射子弹,击毁迎面而来的敌人。当然,不同的敌人有不同的生命值,越大的生命值越多。游戏的过程中还会出现两种补给,一种可以帮我们清理所有的敌人,一种可以让我们的飞机发射双排子弹。至于结束场景,就是显示当前得分和历史最佳得分,当然还要加上再来一次的按钮。
介绍了整个流程之后,相必整个游戏的结构已经很清楚了。借助上一章的设计模式,我们要将各种各样的元素分为不同的层,最后综合到游戏场景中。
WelcomeScene
加载场景,用于完成相应的初始化工作。
WelcomeScene.h
|
|
WelcomeScene.cpp
|
|
场景的工作很简单,加载WelcomeLayer
和音乐,同时播放背景音乐。
WelcomeLayer
用于显示开始界面。
WelcomLayer.h
|
|
WelcomeLayer.cpp
|
|
代码看似很多,其实也就做了几件事。加载背景图片以及动画就不多说了,loadingDone
和repeat
编在了一个序列,为的就是在加载完毕后立即跳转到游戏场景。这里有一个getHighestHistorySorce()
,其实就是获取历史最高成绩,使用了数据存储的功能。注意,这里的转换场景用到了特殊的效果,大家可以自己进行尝试,选择喜欢的效果。
GameScene
游戏主场景,加载游戏层。
GameScene.h
|
|
GameScene.cpp
|
|
工作非常简单,就是加载游戏层GameLayer
。
PlaneLayer
英雄层,也就是我们操纵的飞机。
PlaneLayer.h
|
|
稍微讲解一下这里的成员。MoveTo()
主要用于飞机的触摸移动,Blowup()
是飞机爆炸的方法,RemovePlane()
则是移除飞机并转换到游戏结束的场景。isAlive
表示飞机是否存活,score
自然就是玩家的得分。
PlaneLayer.cpp
|
|
init()
方法制作了两个动作,一个是飞机出场时闪三下,还有一个就是循环播放的动画,通过切换两张图片来达到飞机喷气的效果。
MoveTo()
接受的是触摸点的坐标,逻辑判断的主要目的就是为了让飞机保持在屏幕中,而不是被我们操控着飞出屏幕。Blowup()
其实也好理解,如果isAlive
为true
,那么就把它改为false
,代表着英雄死亡,后面的只不过是加载飞机的爆炸动画,在播放了动画之后移除飞机。不过要注意的是,我们应该先暂停飞机之前所有的动作,然后再执行动作序列,这样会使得飞机显得自然一点。
BulletLayer
子弹层,专门用于生成子弹。
BulletLayer.h
|
|
BulletLayer.cpp
|
|
首先,我们没有使用Vector
,也就意味着我们需要手动为Array
数组调用retain()
。原因很简单,Array
数组使用的是autorelease()
,那么Array
数组就会在一帧之后被释放掉,这显然不是我们想要看到的。删除数组也很简单,为它调用release()
即可。
StartShoot()
和StopShoot()
就是开关定时器,不断地调用AddBullet()
添加子弹。其实查看过源代码后,我们会发现子弹其实就是生成在飞机的头部位置,然后使用MoveTo
动作不断往前飞。这里的速度已经规定为320,所以飞出屏幕用的时间自然就是飞行长度除以时间。
bulletMoveFinished()
是有参回调函数,参数就是调用这个函数的对象,然后将子弹从渲染树和数组中删除。另外,这里还有一个RemoveBullet()
,其实这个是提供给外部使用的函数,在子弹与敌人碰撞时被调用,删除子弹。
MutiBulletsLayer
这个层是双排子弹层,为了方便还是单独做了一个层。
MutiBulletsLayer.h
|
|
MutiBulletsLayer.cpp
|
|
基本逻辑和单排子弹层一样,只是改变了一下子弹的位置。
Enemy
我们把所有敌人共有的特性归纳到一起,方便我们生成不同样式的敌机。
Enemy.h
|
|
Enemy.cpp
|
|
Enemy
继承自Node
节点类,为的是能让Sprite
接收Enemy
的信息。定义了bindSprite()
、getSprite()
、getLife()
、loseLife()
、getBoundingBox()
这五种方法。
bindSprite()
主要用于添加敌机的图片,初始化敌机的生命值,也就是相当于init()
方法。
getSprite()
、getLife()
、loseLife()
不用多说,这里只讲一讲getBoundingBox()
。为什么将Enemy
的坐标转换成世界坐标呢?很简单,Enemy
只是节点,我们的GameLayer
才是主场景,addChild()
只是把精灵加入了Enemy
。
EnemyLayer
用于管理敌机的层。
EnemyLayer.h
|
|
看起来很多,其实功能都是重复的,这里我就拿Enemy3
作为例子,其他的两个可以以此类推。addEnemy3()
用于添加大飞机;enemy3MoveFinished()
是在大飞机飞出屏幕后移除它;enemy3Blowup()
是执行爆炸动画,同时调用removeEnemy3()
来移除飞机;removeAllEnemy3()
和removeAllEnemy()
自然就不用多说了。
EnemyLayer.cpp
|
|
首先先来讲一讲init()
干了什么。其实很简单,加入了三个飞机的精灵帧(大飞机有两张图片,用来做动态效果),然后加载了三个飞机的爆炸动画。因为我们要频繁使用这些动画,所以要把它们加入精灵帧缓存,之后加入到动画缓存中(记得为动画命名)。三个飞机的添加方法是作为定时器的回调事件,我们可以根据需要调整定时器的时间。一般来说,大飞机的添加间隔要长一些。
有人可能奇怪,为什么要有removeEnemy3()
?因为我们必须要等爆炸动画播放完之后,再执行移除操作。另外,我们在GameLayer
中还有敌机的碰撞检测,单独写一个销毁方法也是必要的。
removeAllEnemy3()
自然是为removeAllEnemy()
提供的方法,释放了炸弹之后需要将所有的飞机全部移除。
UFOLayer
用来管理补给品的添加和运动。
UFOLayer.h
|
|
方法看起来很多,其实大部分都是类似的。这里我们就拿双排子弹的补给品作为样例。
UFOLayer.cpp
|
|
构造函数和析构函数就是给Array
数组添加计数,init()
方法开启了两个定时器,不断地加入两种补给品。我们先看AddMutiBullets()
,它前半部分的代码是修改精灵的属性,和敌机的添加其实是一样的。至于move1
、move2
、move3
这几个动作,就是先往下再往上再往下的运动,可以根据自己的喜好设定不同的动作,让玩家更难或者更容易拿到补给品。
mutiBulletsMoveFinished()
就是补给品飞出屏幕后,对精灵进行的删除操作,之前已经讲过类似的。
RemoveMutiBullets()
用于碰撞检测,英雄捡到补给品后删除这个精灵。
ControlLayer
游戏的控制层,用于添加暂停按钮以及炸弹图片的显示。
ControlLayer.h
|
|
ControlLayer.cpp
|
|
这里用到了一个类:MenuItemSprite
。这个是按键特有的精灵,有正常、点中、禁用三种状态,主要也是为了增加按钮的动态效果。当然,这里我们用不到禁用状态。
pPauseItem
进行了初始化等基本操作后,我们就需要把它添加到Menu
中。Menu
绑定的回调函数有一个判断,那就是游戏未暂停时,调用导演类单例暂停游戏,并且将暂停按钮的图片改为继续按钮的图片。如果游戏处于暂停状态,那么就继续游戏,并且将继续按钮改回暂停按钮。
scoreItem
其实是用于记录分数,显示在暂停按钮的旁边。至于updateScore
则是用于更新分数,我们击毁敌机时就要调用这个方法。这里我们没有使用.ttf
文件创建Label
,因为标签要经常改变,所以最好使用.font
文件创建它
注意,我们这里有一个noTouchLayer
。这个层是用于暂停游戏时,阻止玩家进行触摸。如果游戏继续,那么就把这个层移除掉即可。
NoTouchLayer
暂停游戏时,停止触摸。
NoTouchLayer.h
|
|
NoTouchLayer.cpp
|
|
我们可以看到,这个层虽然添加了监听器,但是并没有对单点触摸事件作出任何的实现,所以无论我们怎么点击,都不会有任何反应。那么有人可能会问,如果
添加了一个不可触摸的层,那么我们还怎么点击继续按钮呢?倒回去看ControlLayer
的代码,我们会发现menuPause
的zOrder
被设置为101
,也就是说继续按钮是在不可触摸层的上面。
GameLayer
游戏主场景,管理游戏所有的元素。
GameLayer.h
|
|
GameLayer.cpp
|
|
到目前为止,游戏的基本元素我们都介绍完了,现在是到了要把它们综合在一起的时候了。由于代码较多,接下来只讲各种方法的功能,不会针对语句进行分析。
GameLayer的各种逻辑实现
无限长的背景图
我们开启了一个自定义定时器,用于移动背景图片。由于我们的背景图是有限长的(也不可能无限长),所以我们创建了两张背景图,将它们上下连接起来(高度-2是为了连接紧密),然后让它们同时往下移动,当第一张图片完全移动出屏幕时,我们将这两张图片恢复原位,开始新一轮的移动。具体效果如下:
我们查看backgroundMove()
会发现,第二张图片始终以第一张图片的位置为准,所以只要第一张图片回归原位,第二张图片也会跟着回归,这也是一种比较巧妙的方法。
英雄的触摸移动
我们首先要获取到英雄,这里就通过已经定好的Tag
值进行获取(也可以把英雄做成单例)。随后就是调整Rect
的大小,将飞机的触摸体积适当增大,方便玩家拖动飞机。在判断中我们进行了坐标转换,不懂的请翻看学习笔记7。
getPreviousLocation()
是获取上一次的触摸坐标,但其实我们可以使用getDelta()
,它能够获取前后两次移动的偏移量,更加方便。我们将当前触摸坐标减去之前的坐标,就是偏移量。然后我们将飞机的坐标加上偏移量,就是飞机的位置。注意,这里改变飞机的位置是使用类方法MoveTo()
,为的是限制飞机的移动范围。
碰撞检测
一般来说,游戏的碰撞检测每帧检测一次就足够了,因为游戏的帧率通常能够达到60帧。随着游戏时间变长,游戏的难度也应该增加。具体的表现就是修改敌机的移动速度,让它们飞得更快。之前在讲EnemyLayer
时,敌机接受一个难度系数,修改敌机飞行速度的上限,从而出现更多飞行速度快的敌机。这里是通过判断玩家获得的分数,不断地增加难度。
关于碰撞检测有几种情况:
- 敌机和子弹碰撞
- 敌机和英雄碰撞
- 补给品和英雄碰撞
这里我们拿大飞机作为例子,其他的两种飞机可以类推。我们首先要创建两个个数组,一个用于存储所有需要被删除的子弹,另一个用于存储需要被删除的大飞机。然后循环检测所有大飞机,是否被子弹击中。当被击中时,如果大飞机血量大于一,那么就扣一滴血,并且将子弹添加入数组。如果只有一滴血,那么就将大飞机和子弹加入数组,并且修改得分,同时调用updateScore()
更新分数。
请注意我们的循环顺序,我们先检查一颗子弹,判断他们有没有和敌机碰撞。当一颗子弹被检查完后,删除被击中的大飞机。当所有子弹被检查完后,删除掉发生碰撞的子弹。至于补给品和英雄、敌机和英雄,其实都是类似的,这里就不再赘述。
更新炸弹数量
当玩家捡到炸弹时,我们要在右下角显示炸弹的图片,并且修改炸弹数量。如果没有炸弹,我们就将炸弹图片移除,如果炸弹数量为一,我们就要先判断是捡到炸弹还是用掉了炸弹。当然,你觉得这样很复杂的话,我们可以直接让炸弹图片一直显示,只修改数量即可。
炸弹效果
当我们点击炸弹后,将炸弹数量减一,并且根据enemyLayer
中的数组,获取当前屏幕上的飞机数量,从而修改得分,并且销毁所有飞机。
GameOverScene
GameOverScene.h
|
|
GameOverScene.cpp
|
|
GameOverLayer
游戏结束画面,显示玩家的得分和再来一次的按钮。
GameOverLayer.h
|
|
GameOverLayer.cpp
|
|
代码看起来很多,但其实都是添加各种元素并修改属性。实现的方式并不复杂,我就不再多说了。
总结
微信飞机大战是一个正式的游戏,我们之前的项目实战只能算是练习作品。虽然玩法很简单,但我们能够看到,真正做一款游戏是比较复杂的,哪怕这个游戏很小,也需要严谨的逻辑。这个游戏的代码最具代表性的地方,就是将不同的元素分隔到一个个层中,这么做不仅逻辑清晰,还十分利于分工合作,所以做游戏的时候尽量学习这种方式。