课程教材《计算机游戏程序设计》(基础篇)(第3版) 提供示例代码,而课程实验在示例代码的基础上提出更高的实验要求。除此之外,本人也会额外加入些个人创意,希望同学们在参考之余也能加入自己的想法。
(这次实验对应的书本示例代码bug超级多,所以报告里发的牢骚和说的废话也多了……)
示例代码的迷之bug贼多,怪物动作也只有固定模式
1.了解二维游戏动画合成原理。
2.熟悉Cocos2d-x中的用户交互、触摸事件、碰撞检测机制。
3.熟悉CocoStudio动画编辑器的使用,了解骨骼动画。
1.完成游戏编译(70分)
成功编译并运行教材P128“游戏动画实例-侠客行”。
2.完成方案一 (15分)
修改游戏代码,实现方案一,即利用cocostudio修改人物骨骼,并将修改结果在游戏中读取,从而改变人物外形,动作等,实现自定义人物骨骼动画效果。。
3.完成方案二 (15分)
修改游戏代码,实现方案二,即增加英雄defend动作,记录成功/失败次数,增加计分板功能。
1.完成游戏编译(70分)
老套路运行游戏例程,运行结果如下:
图 1
2.完成方案一 (15分)
修改游戏代码,实现方案一,即利用cocostudio修改人物骨骼,并将修改结果在游戏中读取,从而改变人物外形,动作等,实现自定义人物骨骼动画效果。
CocoStudio Animation导入新的部位的图片,如下图的帽子和斧子:
图 2
图 3
创建新骨骼并对图片进行绑定,再绑定到相应父骨骼上,得到新的角色外形:
图 4
之后更新英雄角色每个动作的角色外貌,如下图奔跑动画:
图 5
添加自制防御动画,如下图所示:
图 6
最后给每个动作的最后一帧中的某个部位层添加一个“动作名_end”的帧事件。如下图添加攻击动作的帧事件:
图 7
导出文件后替换掉源代码中英雄文件,运行程序,发现英雄外貌改变了:
图 8
3.完成方案二 (15分)
修改游戏代码,实现方案二,即增加英雄defend动作,记录成功/失败次数,增加计分板功能。
如第2部分“完成方案一”中已制作了“防御”defend的动画,接下来只要模仿攻击按钮及动作相关代码写一遍就行。如下为AnimationScene.cpp文件中按钮代码:
图 9
图 10
为区分攻击和防御键,另其分别使用两张不同的按钮照片。其中,攻击按钮为红色,防御按钮为蓝色:
图 11
枚举类型中添加了防御DEFEND后,英雄类文件Hero中,简单模仿攻击方式的代码写一个防御方式,使点击防御按钮后实现防御功能,如下图所示:
图 12
关于防御功能的代码及其相关完善,我将在后文的第6部分再详细说明。
增加计分板:
重新新建一个文件SaveScore (.h和.cpp):
SavaScore.h
图 13
SavaScore.cpp
图 14
把SaveScore.h头文件导入AnimationScene.h文件中被其引用。 SaveScore文件的功能为存储英雄和怪物的比分,heroScore为英雄得分,enemyScore为敌人得分,刚开始都为0。这种做法好处在于,可在AnimationScene文件中直接对heroScore和enemyScore进行操作,在AnimationScene场景被刷新后,计分值也不会改变,直接调用即可。
计分板代码:
图 15
update函数中不断调用judge函数,根据角色的血量来判断输赢,代码为:
图 16
图 17
当每轮游戏结束时(还没到最终输赢),需要刷新当前场景,这里用scheduleOnce的方式调用了restart函数,该函数里执行的代码可刷新场景。由于一轮游戏结束后场景不会被马上刷新,而是在等待几秒中后才刷新,所以这里的scheduleOnce对选择器选择的函数的执行是在3秒后。
restart代码:
图 18
其中,场景过渡使用了部落格特效,持续1.2s 。
4.云朵移动
寻找或自行扣掉一张没有背景的云朵的png图片,替换掉资源原有的cloud.png图片 。
图 19
AnimationScene.cpp初始化时创建3朵云,位置设置为不同,代码较简单,不显示。
再在update函数中不断修正其x轴方向位置,每朵云的位移距离不同。当云朵移动到镜头的一端看不见时,在修正其位置到屏幕另一端,这样就能得到循环播放的3朵云。
以1号云朵的位移代码为例,其余类似:
图 20
运行效果图:
图 21
5.BUG修正
这个游戏Demo有许许多多的bug,主要原因是代码逻辑写得不好,这一部分花了大量的时间去Debug来修正。终于解决了游戏中存在的目前能找到的所有Bug,令游戏能正确且流畅的运行起来。
由于游戏bug太多,下面根据解决方法的不同,总结为4类Bug:
下面修正上述bug:
注释掉或删掉AnimationScene.cpp文件中的attackCallback函数中的红框中代码即可:
图 22
这个bug为该程序中最主要的bug,以英雄Hero为例,问题主要出现在下面三个地方:
图 23
图 24
图 25
图 26
分析bug原因:
可见,1)根据摇杆的操作情况,调用2)的Hero中play函数,而3)能检测Hero的状态,执行动作。
因为1)在update函数中;2)被1)调用;3)是update函数。 所以理论上1)2)3)都是一直在运作中的,没有固定的先后顺序。因此,由于不能确定运作顺序(除了 2)会在1)后运行),导致程序容易出错。
举例:
当hero被攻击,Hero的play函数会被传入枚举类型SMITTEN作为参数,根据play代码可知,此时受伤状态变量m_ishurt会变为true,当前角色状态m_state会被赋值为SMITTEN。
理论上来说,下一步该执行Hero中的update函数,判断并执行SMITTEN动作了才对。 然而,这里也有可能在执行Hero的update函数之前,先执行了AnimationScene的update函数。
如果先执行了AnimationScene的update函数,那么1)的“控制角色移动”的代码段就先被执行了,如果此时没有动摇杆,那么枚举类型STAND将作为Hero的play函数的参数传进去,之后就会重新赋值给m_state,SMITTEN状态就会被STAND状态给覆盖掉了。
此时,m_state的值为STAND,而m_ishurt的状态依然是true(因为SMITTEN动作没有被执行)。 再进入Hero的update函数时,由于m_ishurt也是各个动作是否该执行的判断依据,当m_ishurt == true时,这些动作都不会被执行。
因此,当hero被攻击后,hero的操作都将失效。
根据上面分析,想要解决这个bug必须要保证两个前提(暂不讨论DEFEND防御):
解决上述1)和2):
1)在Hero.h中声明新的布尔类型私有变量actionFlag,其作用为 当m_state被赋值为ATTACK或SMITTEN时,actionFlag被赋值为true,当其为true时,m_state不能被再改变,只有在ATTACK和SMITTEN动画运行到最后一帧时,actionFlag变为false,此时m_state允许被赋值。
修改相关代码:
图 27
图 28
2)研究源代码中attack相关函数,发现Hero中的m_isAttack的作用为判断hero是否“正在攻击”,这里指的是“动作”而不是“状态”。通过这一变量,在动作执行时(动画播放时)才赋值为true,在最后一帧播放完了再赋值为false。把该变量作为动作执行的判断依据,能有效地控制并防止动作的被打断以及持续进行(譬如一直点击攻击键,攻击动作不断被打断并重新执行,只播放前几帧);
模仿m_isAttack变量,把m_ishurt变量的意义从原来的“受伤状态”更改为“受伤动作”。
同时,在监听帧事件的函数中,要在动作执行完后加入play(STAND)的代码,防止其带着原来的m_state先执行Hero的update函数又引起什么奇怪的操作。
模仿着更改代码:
图 29
图 30
同理修改Enemy的代码即可。
查看碰撞检测文件MyContactListener.cpp,查看其update函数:
图 31
以敌人攻击英雄为例,关键代码部分放大↓:
图 32
分析:
由上面代码可知,当其他条件满足的前提下,Enemy的m_isAttack变量为true时,表示此时敌人正在执行攻击动作,if满足条件,执行Hero的hurt函数,hero受伤掉血。然后Enemy执行setAttack(false)把其m_isAttack置为false。
然而,当m_isAttack置为false后,在Enemy中,会把其视为攻击动作已经结束,在m_state还是ATTACK时,会把m_isAttack==false作为再次执行攻击动作的判断依据。而检测碰撞文件的update函数又会很快的被再次执行,m_enemy->isAttack()又会被视为true……如此地连续执行,可能会造成角色的连续多次掉血,或者角色一旦攻击到另一角色时,会不断地执行攻击动作。
解决:
通过上述分析,我们了解到,解决问题的关键点在于不能在检测碰撞中执行m_enemy->setAttack(false)来改变破坏Enemy的攻击动作。
综上,我们保留其思想,但是不改变m_isAttack的值,为角色引入一个新的私有变量attackHurtFlag,表示被攻击伤到伤害的标志,增加set和get方法。以enemy攻击hero为例,关键代码为:
图 33
Hero的update函数中:
图 34
观察碰撞检测文件MyContactListener.cpp,查看其update函数中enemy攻击hero部分:
图 35
分析:
发现其碰撞检测的基本原理为:为enemy的ax层(即enemy的斧子部件)添加2个检测点,再根据hero的位置创建一个矩形。当enemy为攻击状态,并且其斧子的2个检测点在hero的矩形范围内时,即为实现碰撞。
因此,这里该如何创建矩形成为关键。
分析Rect方法的参数,其第1个参数为矩形左下角的x坐标,第2个参数为矩形左下角的y坐标,第3个参数为矩形的宽,第4个参数为矩形的高。
结合游戏运行图来分析:
假设在使用cocoStudio Animation时,角色的中心点在身体的中心点,随意创建一个矩形,则有:
图 36
假设还是同一程序,当hero转身后,其矩形不会根据角色的转身而左右颠倒,如下图所示:
图 37
由上面两张图可知道,创建矩形时,宽(即x轴)的中间位置的x坐标最好落在角色中心点的x坐标上。只有这样,hero无论转身与否,其前后的被攻击的判定范围都是一样的,这样才不会出现奇怪的“有时能砍到,有时又砍不到”的奇怪现象。
最后只要不断调整矩形的宽度即可(即调整第1个参数和第3个参数)。而矩形的高度只要足以涵盖住角色即可(即调整第2个参数和第4个参数)。
最终矩形参数修改为:
图 38
图 39
6.防御机制
防御机制设定为:
值显示
根据以上设定,编写代码:
Hero中的update:
图 40
并为每个动作的执行加上m_isDefend=false,以STAND站立动作为例:
图 41
Hero的showBloodTips函数 “减少暴击数和暴击伤害”以及“防御状态下伤害值为蓝色”:
图 42
图 43
运行图:
图 44
7.AI设计
观察AI文件AIManager的原代码,发现怪物AI仅仅是根据一套固有的动作反复执行而已,关键代码为:
图 45
分析上面代码,可知这个AI并不智能。并且根据上面的执行结果,可知moveLeft的动作持续最久,因此游戏中的后半段,敌人emeny会一直往左边界“推墙”,moveRight的持续时间太短,因此无法往右半边回来。
因此,重新编写一个AI代码文件,使其能够根据hero的位置,实现自动跟踪,在适宜的位置进行攻击的功能。
编写后的关键代码为(以hero在enemy的左方为例):
图 46
hero在enemy的右方时也同理可得,而当hero和enemy位置相同时,enemy直接攻击即可。
由上述得到了敌人AI的最佳方案,但是如果直接把bestAI函数放到update函数不断调用的话,会发现游戏会变得非常困难,几乎没有赢的可能性。并且,敌人enemy的行动模式不够随机也反而显得不是那么的“智能”。因此,在此基础上减少bestAI的执行次数,插入随机行动模式,并适当地减少攻击频率,让游戏变得更简单,令AI变得更随机些。
修改后的代码为:
图 47
bestAI()中以hero在enemy的左方为例:
图 48
AI的随机方案:
图 49
通过上述操作后,敌人AI能够保持在最佳行动方案的基础上,也进行些许随机行动了。
8.其余Label显示细节
① 每轮游戏开始时都会出现“Round X”,X表示游戏的第几轮,1.5s后消失(移除)
图 50
② 每一小轮游戏结束后,都会在对应角色的血条下方显示“win”字样
图 51
③ 场景有部落格特效过渡
④ 游戏结束后会显示玩家的输赢,赢了显示“YOU WIN!”;输了显示“YOU LOSE!”
图 52