实验3 二维游戏动画合成

目录

一、实验目的与要求

二、实验内容与方法

三、实验步骤与过程

1. 完成游戏编译

2. 增加计分板功能和回合制

3. 增加英雄Defend动作

4. 修改游戏bug

1) 攻击键BUG

2) 英雄被攻击后不再接受玩家操作指令,smitten动作不执行,attack动作被打断BUG

3) 连续不断使用attack BUG

4) 角色在奇怪的地方被砍中,或砍不到角色BUG

5) 敌人大概率向左走,最后会撞左边的墙BUG

6) 云朵移动动画出现衔接不上的现象BUG

5. 游戏优化升级

1) 防御机制设计

2) 添加Label细节

3) 增加音乐以及音效

4) 增加Replay按钮

四、实验结论或心得体会


一、实验目的与要求

1.了解二维游戏动画合成原理。

2.熟悉Cocos2d-x中的用户交互、触摸事件、碰撞检测机制。

3.熟悉CocoStudio动画编辑器的使用,了解骨骼动画。

二、实验内容与方法

1.完成游戏编译(10分)

成功编译并运行教材P128“游戏动画实例-侠客行”。

2.增加计分板功能和回合制 10分)

记录成功/失败次数,增加计分板功能;将游戏改为回合制游戏。

3.增加英雄Defend动作 15分)

利用CocoStudio设计英雄Defend动作,并将此功能在游戏中加载。

4.修改游戏bug15分)

修改游戏中出现的明显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必须要保证两个前提:

  1. 状态m_state一旦被改变后,只有执行完了该动作(ATTACK和SMITTEN)后,才能再次改变状态m_state。
  2. 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动画编辑器,学习骨骼动画,并对二维游戏动画合成原理有所了解,并根据以上知识完成了实验。

这次实验完成时间比较长,用了比较多心机去完成,透过上网找资料实现了一些自己想要的功能,总的来说是一次不错的体验,期待未来能够做出更多有趣的游戏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值