目录
2) 英雄被攻击后不再接受玩家操作指令,smitten动作不执行,attack动作被打断BUG
一、实验目的与要求
1.了解二维游戏动画合成原理。
2.熟悉Cocos2d-x中的用户交互、触摸事件、碰撞检测机制。
3.熟悉CocoStudio动画编辑器的使用,了解骨骼动画。
二、实验内容与方法
1.完成游戏编译(10分)
成功编译并运行教材P128“游戏动画实例-侠客行”。
2.增加计分板功能和回合制 (10分)
记录成功/失败次数,增加计分板功能;将游戏改为回合制游戏。
3.增加英雄Defend动作 (15分)
利用CocoStudio设计英雄Defend动作,并将此功能在游戏中加载。
4.修改游戏bug(15分)
修改游戏中出现的明显bug。
5. 游戏优化升级(10分)
自行发挥想象力,优化游戏功能。
6.完成实验报告 (40分)
截图记录关键步骤,分析实验结果,撰写心得体会。
三、实验步骤与过程
1. 完成游戏编译
首先我们使用 cocos new MyGame -p com.2020152011.edu -l cpp -d Experiment_3 命令创建新项目。
(图1.1 - PowerShell窗口)
创建结果如下:
(图1.2 - 创建结果截图)
接着我们把本次实验的代码内容复制替换到生成的项目中的"Classes"文件夹内。
(图1.3 - "Classes"文件夹)
下面同样我们把实验内容中的图片、地图、字体资源复制到生成的项目中的"Resources"文件夹内。
(图1.4 - "Resources "文件夹)
接下来打开Visual Studio,添加、删除现有项(类)。
(图1.5 - Visual Studio窗口)
然后我们来修改游戏窗口大小,修改在AppDelegate.cpp中的designResolutionSize参数。
(图1.6 - AppDelegate.cpp - 修改后的designResolutionSize参数)
最后点击调试器按钮,稍等片刻,即可成功运行本次实验游戏。
(图1.7 - 调试器按钮)
(图1.8 - 游戏运行成功初始画面)
这里,我们顺便也把游戏显示名称进行修改,在applicationDidFinishLaunching函数中,对于中文的部分调用转换函数,之后点击调试器按钮,可以看到如图1.10,中文正常显示。
略
(图1.9 - AppDelegate.cpp - applicationDidFinishLaunching函数)
略
(图1.10 - 修改后的运行窗口)
2. 增加计分板功能和回合制
为了增加计分板和回合制功能,我们来编写新的文件ScoreBoard,它们的内容如下图2.1和图2.2。
(图2.1 - ScoreBoard.h)
(图2.2 - ScoreBoard.cpp)
ScoreBoard文件的功能为存储英雄和敌人的比分,heroScore为英雄得分,enemyScore为敌人得分,刚开始都为0。
把ScoreBoard.h头文件导入AnimationScene.h文件中被其引用。这种做法好处在于,可在AnimationScene文件中直接对heroScore和enemyScore进行操作,在AnimationScene场景被刷新后,计分值也不会改变,直接调用即可。
下面我们在AnimationScene.cpp的init函数中加入计分板的代码。
(图2.3 - AnimationScene.cpp - init函数新增部分)
因为需要判断输赢以及更新分数,我们编写一个updateBoard函数用来完成以上的任务,该函数会放在update函数中一直执行。代码内容如下图2.4,图2.5:
我们调用getLife函数获取角色的血量,并根据血量来判断输赢,当某一方血量到达0以下则为输家,赢的一方加1分,当某一方先达到2分为最终胜利者,游戏也随即结束,当每轮游戏结束时(还没到最终输赢),需要刷新当前场景,这里用scheduleOnce的方式调用了restart函数(图2.6),该函数里执行的代码可刷新场景。由于一轮游戏结束后场景不会被马上刷新,而是在等待几秒中后才刷新,所以这里的scheduleOnce对选择器选择的函数的执行是在3秒后。
(图2.4 - AnimationScene.cpp - updateBoard函数1/2)
(图2.5 - AnimationScene.cpp - updateBoard函数2/2)
(图2.6 - AnimationScene.cpp - restart函数)
最终计分板效果如下图所示:
(图2.7 - 计分板效果图)
3. 增加英雄Defend动作
首先在cocostudio中的动画编辑器制作“防御”defend的动画。
(图3.1 - cocostudio - 动画编辑器窗口)
接着把做好的项目进行导出,导出完毕后,把导出的文件夹拷贝到Resource文件夹下,就能够在项目中使用了。
(图3.2 -"Resources "文件夹)
因为接下来要添加防御这个动作,我们先在config_set.h中的枚举类型State中添加防御DEFEND。
(图3.3 - config_set.h - 枚举类型State)
然后我们在AnimationScene.cpp文件中编写按钮代码,如下图3.3:
(图3.4 - AnimationScene.cpp - init函数按钮代码)
还要添加对应的按钮回调函数。
(图3.5 - AnimationScene.cpp - defendCallback函数)
这里为了区分攻击按钮和防御按钮,分别使用两张不同的按钮照片。其中,攻击按钮为红色,防御按钮为蓝色,效果如下图3.6:
(图3.6 - 攻击按钮和防御按钮)
最后在游戏中点击防御按钮后,可以看到英雄做出我们设计好的Defend动作,效果如下图:
(图3.7 - 英雄Defend动作)
因为目前游戏依然存在BUG,关于防御功能的代码及其相关完善,我将在修改完游戏BUG的后文的第5部分游戏优化升级再详细说明。
4. 修改游戏bug
1) 攻击键BUG
注意到点击攻击按钮的时候,英雄和敌人会有同时响应攻击操作的情况。
通过观察代码发现,在AnimationScene.cpp文件中的攻击按钮回调函数attackCallback中,英雄/敌人会同时响应攻击动作,解决方法就是删除掉敌人对于攻击按钮的响应。
(图4.1 - AnimationScene.cpp - attackCallback函数)
2) 英雄被攻击后不再接受玩家操作指令,smitten动作不执行,attack动作被打断BUG
此BUG为该程序中最主要的BUG,通过观察代码后,以英雄Hero为例,问题主要出现在下面三个地方:
(图4.2 - AnimationScene.cpp - update函数)
(图4.3 - Hero.cpp - play函数)
(图4.4 - Hero.cpp - update函数)
以下分析造成BUG的原因。
可见,图4.2根据摇杆的操作情况,调用图4.3的Hero中play函数,而图4.4能检测Hero的状态,执行动作。
因为图4.2在update函数中;图4.3被图4.2调用;图4.4是update函数。 所以理论上图4.2、图4.3、图4.4都是一直在运作中的,没有固定的先后顺序。因此,由于不能确定运作顺序(除了图4.3会在图4.2后运行),导致程序容易出错。
以下举一个例子:
当Hero被攻击,Hero的play函数会被传入枚举类型SMITTEN作为参数,根据play代码可知,此时受伤状态变量m_ishurt会变为true,当前角色状态m_state会被赋值为SMITTEN。
理论上来说,下一步该执行Hero中的update函数,判断并执行SMITTEN动作了才对。 然而,这里也有可能在执行Hero的update函数之前,先执行了AnimationScene的update函数。
如果先执行了AnimationScene的update函数,那么图4.2的“控制角色移动”的代码段就先被执行了,如果此时没有动摇杆,那么枚举类型STAND将作为Hero的play函数的参数传进去,之后就会重新赋值给m_state,SMITTEN状态就会被STAND状态给覆盖掉了。
此时,m_state的值为STAND,而m_ishurt的状态依然是true(因为SMITTEN动作没有被执行)。 再进入Hero的update函数时,由于m_ishurt也是各个动作是否该执行的判断依据,当m_ishurt == true时,这些动作都不会被执行。
因此,当hero被攻击后,hero的操作都将失效。
根据上面分析,想要解决这个bug必须要保证两个前提:
- 状态m_state一旦被改变后,只有执行完了该动作(ATTACK和SMITTEN)后,才能再次改变状态m_state。
- ATTACK和SMITTEN对应动作在执行中时,一定要运行到最后一帧,不能被打断。
根据以上思路,我们在Hero.h中声明新的布尔类型私有变量isDoingAction,其作用为当m_state被赋值为ATTACK或SMITTEN时,isDoingAction被赋值为true,当其为true时,m_state不能被再改变,只有在ATTACK和SMITTEN动画运行到最后一帧时,isDoingAction变为false,此时m_state允许被赋值。
(图4.5 - Hero.cpp - 修改后play函数)
(图4.6 - Hero.cpp - 修改后onFrameEvent函数)
通过观察源代码中attack相关函数,发现Hero中的m_isAttack的作用为判断hero是否“正在攻击”,这里指的是“动作”而不是“状态”。通过这一变量,在动作执行时(动画播放时)才赋值为true,在最后一帧播放完了再赋值为false。把该变量作为动作执行的判断依据,能有效地控制并防止动作的被打断以及持续进行(譬如一直点击攻击键,攻击动作不断被打断并重新执行,只播放前几帧);
下面模仿m_isAttack变量,把m_ishurt变量的意义从原来的“受伤状态”更改为“受伤动作”。
同时,在监听帧事件的函数中,要在动作执行完后加入play(STAND)的代码,防止其带着原来的m_state先执行Hero的update函数引起奇怪的操作。
(图4.7 - Hero.cpp - 修改后update函数)
(图4.8 - Hero.cpp - 修改后onFrameEvent函数)
以上内容对于Enemy的代码同理,进行一样的修改即可。
3) 连续不断使用attack BUG
在解决 2) 的BUG后,发现当角色砍中另一角色时,会继续不断使用attack。我们查看碰撞检测文件MyContactListener.cpp,查看其update函数,以Hero攻击Enemy为例:
(图4.9 - MyContactListener.cpp - 修改前update函数)
由上面代码可知,当其他条件满足的前提下,Hero的m_isAttack变量为true时,表示此时英雄正在执行攻击动作,if满足条件,执行Enemy的hurt函数,enemy受伤掉血。然后Hero执行setAttack(false)把其m_isAttack置为false。
然而,当m_isAttack置为false后,在Hero中,会把其视为攻击动作已经结束,在m_state还是ATTACK时,会把m_isAttack==false作为再次执行攻击动作的判断依据。而检测碰撞文件的update函数又会很快的被再次执行,m_hero->isAttack()又会被视为true,如此地连续执行,可能会造成角色的连续多次掉血,或者角色一旦攻击到另一角色时,会不断地执行攻击动作。
通过上述分析,我们了解到,解决问题的关键点在于不能在检测碰撞中执行m_hero->setAttack(false)来改变破坏Hero的攻击动作。
综上,我们保留其思想,但是不改变m_isAttack的值,为角色引入一个新的变量attackHurtFlag,表示被攻击伤到的标志,增加对应的set和get方法。以Hero攻击Enemy为例,关键代码为:
(图4.10 - MyContactListener.cpp - 修改后update函数)
(图4.11 - Enemy.cpp - 修改后update函数)
以上内容对于Enemy攻击Hero的代码同理,进行一样的修改即可。
4) 角色在奇怪的地方被砍中,或砍不到角色BUG
我们观察碰撞检测文件MyContactListener.cpp,查看其update函数中enemy攻击hero部分:
(图4.12 - MyContactListener.cpp - update函数enemy攻击hero部分)
发现其碰撞检测的基本原理为:为enemy的ax层(即enemy的斧子部件)添加2个检测点,再根据hero的位置创建一个矩形。当enemy为攻击状态,并且其斧子的2个检测点在hero的矩形范围内时,即为实现碰撞。
因此,这里该如何创建矩形成为关键。分析Rect方法的参数,其第1个参数为矩形左下角的x坐标,第2个参数为矩形左下角的y坐标,第3个参数为矩形的宽,第4个参数为矩形的高。
结合游戏运行图来分析:
假设在使用cocoStudio Animation时,角色的中心点在身体的中心点,随意创建一个矩形,则有:
(图4.13 - 游戏运行示意图1)
假设还是同一程序,当hero转身后,其矩形不会根据角色的转身而左右颠倒,如下图所示:
(图4.14 - 游戏运行示意图2)
由上面两张图可知道,创建矩形时,宽(即x轴)的中间位置的x坐标最好落在角色中心点的x坐标上。只有这样,hero无论转身与否,其前后的被攻击的判定范围都是一样的,这样才不会出现奇怪的“有时能砍到,有时又砍不到”的奇怪现象。
最后只要不断调整矩形的宽度即可(即调整第1个参数和第3个参数)。而矩形的高度只要足以涵盖住角色即可(即调整第2个参数和第4个参数)。
最终矩形参数修改为如下图4.15。
(图4.15 - MyContactListener.cpp - update函数创建矩形部分)
5) 敌人大概率向左走,最后会撞左边的墙BUG
通过观察AI文件AIManager的原代码,发现怪物AI仅仅是根据一套固有的动作反复执行而已,关键代码为:
(图4.16 - AIManager.cpp - setAI函数)
分析上面代码,可知这个AI并不智能。并且根据上面的执行结果,可知moveLeft的动作持续最久,因此游戏中的后半段,敌人emeny会一直往左边界“推墙”,moveRight的持续时间太短,因此无法往右半边回来。
因此,重新编写一个AI代码文件,使其能够根据hero的位置,实现自动跟踪,在适宜的位置进行攻击的功能。
首先是Hero在Enemy的左方的情況。
(图4.17 - AIManager.cpp - DemonAI函数1/2)
然後是Hero在Enemy的右方以及位置相同的情況。
(图4.18 - AIManager.cpp - DemonAI函数2/2)
由上述得到了敌人AI的最佳方案,但是如果直接把DemonAI函数放到update函数不断调用的话,会发现游戏会变得非常困难,几乎没有赢的可能性。并且,敌人enemy的行动模式不够随机也反而显得不是那么的“智能”。因此,在此基础上减少DemonAI的执行次数,插入随机行动模式,并适当地减少攻击频率,让游戏变得更简单,令AI变得更随机些。
首先我们编写AI的随机方案RandomAI。
(图4.19 - AIManager.cpp - RandomAI函数)
接着修改DemonAI函数,修改内容如下图4.20,通过随机数,减少执行攻击的概率。
(图4.20 - AIManager.cpp - DemonAI函数)
最后修改setAI函数,设定在不同时段交替执行AI最佳方案与AI随机方案。
(图4.21 - AIManager.cpp - setAI函数)
通过上述操作后,敌人AI能够保持在最佳行动方案的基础上,也进行些许随机行动了。
6) 云朵移动动画出现衔接不上的现象BUG
我们观察AnimationScene.cpp中,update函数中的背景云朵动画部分,发现该动画是由两个云朵精灵同时向左移动实现的,当第一个云朵精灵到达x轴某一个位置时,则重置云朵精灵的位置,而第二个云朵精灵的位置又依赖于第一个云朵精灵的位置,所以重置时会一起重置。
(图4.22 - AnimationScene.cpp - update函数背景云朵动画部分)
通过分析,会出现云朵移动动画衔接不上的现象是因为重置的时机不对,显然解决方法就是修改要重置时的位置,经过不断尝试,最终得到合适的重置位置如下图所示:
(图4.23 - AnimationScene.cpp - 修改后update函数背景云朵动画部分)
5. 游戏优化升级
1) 防御机制设计
我们设定防御的机制为,点击蓝色按钮就会进入防御状态,而在防御状态下,角色最后会保持防御动画的最后一帧,如果在防御状态下,操作摇杆,点击攻击按钮,能打断防御状态,并执行其他相应动作,而如果在防御状态下,再点击一次防御按钮就可以取消防御状态。
另外在防御状态下能减少受到的一段伤害及造成的伤害,并且受到的伤害值以粉色表示,不同于正常伤害的红色。如果本来只会造成一段伤害(没有暴击),在防御状态下会把这次攻击抵挡掉,英雄不会受到伤害,并会显示出defended字样代替原来的伤害值,字样以蓝色表示。
根据以上设定,我们编写代码:
(图5.1 - Hero.cpp - update函数DEFEND部分)
为每个动作的执行加上m_isDefend = false,以ATTACK动作为例:
(图5.2 - Hero.cpp - update函数ATTACK部分)
根据新的防御机制,修改showBloodTips函数。
(图5.3 - Hero.cpp - showBloodTips函数1/2)
(图5.4 - Hero.cpp - showBloodTips函数2/2)
运行后的效果图如下:
(图5.5 - 减少受到的一段伤害及造成的伤害)
(图5.6 - 攻击被挡掉)
2) 添加Label细节
每轮游戏开始时都会出现“Round X”,X表示游戏的第几轮,并且会在3秒后消失。其中roundx代表回合数在ScoreBoard.h中定义,初始为1,每结束一回合加1。
实现关键代码如下图:
(图5.7 - AnimationScene.cpp - init函数新增部分)
(图5.8 - AnimationScene.cpp - removeRound函数)
(图5.9 - ROUND 1字样)
每一小轮游戏结束后,都会在对应角色的血条下方显示“win”字样。
(图5.10 - win字样)
游戏结束后会显示玩家的输赢,玩家赢了就会显示“YOU WIN!”;输了就会显示“YOU LOSE!”
(图5.11 – YOU WIN!字样)
3) 增加音乐以及音效
我为场景中的交互都添加了对应的音响效果,比如背景音乐、攻击音效、胜利音效等等,当然也包括上述添加的roundX音效,具体效果见游戏视频。
以下展示一部分添加的音响代码
(图5.12 - 添加背景音乐代码)
(图5.13 - 添加攻击音效代码)
(图5.14 - 添加回合失败音效代码)
4) 增加Replay按钮
我在游戏结束时添加Replay按钮,实现了重新开始游戏(Replay)功能。
实现关键代码如下图:
(图5.15 - 添加重玩按钮代码)
(图5.16 - 重玩按钮回调函数)
(图5.17 - Replay按钮)
四、实验结论或心得体会
根据实验指引顺利的完成了实验,通过这次实验,我熟悉了Cocos2d-x中的用户交互、触摸事件、碰撞检测机制,以及练习使用了CocoStudio动画编辑器,学习骨骼动画,并对二维游戏动画合成原理有所了解,并根据以上知识完成了实验。
这次实验完成时间比较长,用了比较多心机去完成,透过上网找资料实现了一些自己想要的功能,总的来说是一次不错的体验,期待未来能够做出更多有趣的游戏。