前言
在Unity官方教程 2D Roguelike(4):角色移动中,我们完成了游戏最主要的功能——角色移动相关逻辑,接下来只要完成怪物的移动,整个游戏的底层架构就差不多完整了,除了UI和音乐音效等。这一节我们主要完成以下内容:
- 怪物移动逻辑
- 怪物动画设置
本节你将学会什么?
- 无新知识点,强化前面所掌握的内容
一、编辑Enemy Script
2D Roguelike是个回合制游戏,我动敌静,敌动我静。接下来创建一个Script,命名为Enemy,双击打开编辑怪物敌动代码吧!
第1步:MoveEnemy()
代码简读:
- Enemy类必须继承MovingObject类,所以冒号后面记得修改。
- 新增私有成员变量target并在Start()进行初始化赋值,代表Player的位置,怪物移动是根据Player的位置来决定方向。
- 对Start()进行了重写,所以需要添加修饰关键词override,然后通过base调用了父类的Start()方法。
- 新增公共方法MoveEnemy(),定义了int类型变量xDir、yDir初始化为0,代表怪物移动方向向量。
当Player和Enemy在同一个X坐标,则对两个物体的Y坐标进行高低判断,如果target(Player)的y值高,则移动方向是向上移动,yDir为1,否则为-1向下移动。
当Player和Enemy不在同一个X坐标,则直接判断X高低,target高则xDir为1向右移动,否则为-1向左移动。
游戏管理器GameController会调用MoveEnemy()方法进行指挥怪物队列移动,因此关键词为public。
Mathf.Abs指的是绝对值。float.Epsilon是最小浮点值,接近0。
条件?结果A:结果B,是三元运算符的表达式,条件结果为true则是表达式结果是A,否则是B。
从上面的代码解析可以看出,MoveEnemy()干的活就是根据Player坐标确定怪物移动方向,然后调用AttempMove<Player>()进行真正移动。
第2步:AttempMove<T>()
代码简读:
- 这游戏并非是Player走一回合,怪物走一回合。而是Player走两回合,怪物才能走一回合。带着这个认知去看这一段代码就很好理解。
- 新增布尔值类型的私有成员变量skipMove,用它来控制怪物是否跳过这回合。
- 在AttempMove<T>(),判断skipMove是否为true,如果是的话这回合怪物要跳过不进行移动,就return跳出这个方法不执行后续代码。如果是false,则调用了父类MovingObject的AttempMove<T>()方法进行移动,最后重新把skipMove赋值为true,保证下一回合怪物不能移动。
从上面的代码解析可以看出,AttempMove<T>()干的活就是接收到MoveEnemy()的信息通知往哪边移动的时候,判断下这回合要不要跳过,然后再进行移动。
OK,怪物开始移动了!哒哒哒,哒哒哒,诶?遇见Player了!好家伙,要打的就是你!
第3步:OnCantMove<T>()
代码简读:
- 我们还记得父类里有个泛型方法OnCantMove<T>()吧?因为它是抽象方法,需要子类去给出具体实现,因此我们在方法前加上了override修饰符。
- 在父类MovingObject我们可以看到,OnCantMove<T>()这里的泛型参数T的类型是组件Component,而Player组件也是Component的一种,所以传入的参数可以在方法内转化为Player类型,并且调用Player的LoseFood方法来扣除角色生命。playerDamage指的是怪物攻击角色造成的伤害,也就是每次被打角色生命扣除数值。
二、编辑GameController
在上面的Enemy Script里,我们实现了怪物基本移动逻辑(根据Player坐标确定方向进行移动,与Player碰撞的时候调用LoseFood()方法实现攻击Player效果)。作为游戏管理器的GameController,则负责调控全局,指挥多个怪物在一定的条件下依次调用Enemy Script进行移动。
第1步:增加怪物队列集合
打开GameController,增加以下代码。
代码简读:
- 新建List类型的私有变量成员enemies并在Awake()内初始化,集合里面存放的是Enemy类型的数据,也就是把关卡内的所有怪物都放进去。
- 添加AddEnemyToList()方法,通过Add()方法把传入的Enemy类型的参数都添加到enemies。
- 因为重新生成关卡的时候,enemies数据会被保留,所以需要在InitGame()初始化关卡时用Clear()方法清除上一个关卡的敌人数组。
切回到Enemy Script,在Start()方法内增加一句代码。
这句代码的意思是调用GameController的公有方法AddEnemyToList(),把自己(当前实例)当成参数传入进去,也就是把当前怪物加进集合enemies。正因为需要在这里进行调用,所以AddEnemyToList()方法的关键词是public哦~
第2步:指挥怪物们依次移动
关卡内的怪物都被添加进集合enemies内了,接下来就是在恰当的时机指挥这些怪物一个个移动啦!
代码简读:
- 新增浮点值变量turnDelay并赋值,代表回合等待时间,单位为s。
- 新增布尔值类型变量enemyMoving,代表怪物们是不是正在移动中,正在移动则为true,其他情况则为false。
- Update()方法,判断当playerTrun和enemyMoving均为false的情况下(是怪物回合并且怪物没有在移动中),使用StartCoroutine函数开启协同程序MoveEnemys()指挥怪物开始一个个进行移动;
协程是分步骤执行代码的程序,遇到条件(yield return语句)会挂起暂停退出,直到条件满足才会被唤醒继续执行后面的代码。
- MoveEnemys()方法,把enemyMoving赋值为true确保Update()不再执行开启协程的代码,等待turnDelay时长之后(为了让Player走完),判断如果没有怪物的时候再等待turnDelay时长(让回合感更明显,Player不能一直不停地移动);如果有怪物的话,则开始for循环敌人数组enemies,调用MoveEnemy()方法指挥他们一个个移动。为了实现依次的效果而不是同时移动,加了间隔时长moveTime。
- 所有敌人移动完毕之后,把人物回合开关开起来(playerTurn为true),敌人移动中开关关掉(enemyMoving为false),重新把回合权交给了Player。
第3步:填坑-playerTurn开关
在移动逻辑章节的第三节内的第3步,当时为了修正按一次方向键而Player移动了多次的问题,我们临时增加了一些代码。现在已完成怪物移动代码,可以正常地进行人物怪物交换来回移动,因此我们需要把之前临时增加的三句代码删除。
然后我们在Player Script的AttempMove()方法内对playerTurn进行赋值改动。
也就是说,人物开始移动之后立刻把playerTurn赋值为false,这样游戏管理器的Update()判断playerTurn和enemyMoving都是false的时候,它开始指挥怪物们进行移动。怪物移动完毕之后,把playerTurn赋值为true,Player的Update()即可执行后续代码让Player开始第二次移动。
当然,我们也不要忘了即使轮到怪物回合了,它也有选择不走的权利!太懒惰了,怪物利用skipMove这个开关,成功实现人物走两次,它才动一次。(和我一样懒( >﹏<。)~)
第4步:执行移动
保存脚本,切回到Unity编辑器。打开Prefabs文件夹,同时选中Enemy1和Enemy2预制件,再点击菜单栏的Component-Scripts-Enemy,把Enemy脚本挂载到这两个预制件上。
在右侧Inspector内的Enemy组件里,Blocking Layer选择BlockingLayer层。
单独选择Enemy1预制件,把它的Player Damage设置为10,而Enemy2的为20。(这里可以自由发挥设置伤害为多少,但是注意不要过高一招就把Player秒了……)
最后一步,开测!
运行游戏,我们按键盘的方向键让小人走起来。
可以看到游戏正常耍起了:
- 小人和怪物都可以正常移动
- 小人走两次,怪物才走一次
- 怪物之间移动有先后次序
- 小人可以正常拾取地上的食物
- 小人可以正常劈砍障碍墙直到消失开辟路径
但其实我们会发现这个游戏的怪物移动逻辑会有一个缺点:怪物如果被障碍墙挡住了,会一直卡着不动,直到小人移动变换左右或者上下,它才有可能再动起来。这个是由于Enemy脚本的MoveEnemy()方法里面获取移动方向的设定上不够灵活。感兴趣的童鞋可以想想如何优化这个怪物AI~
虽然看起来除了音乐和UI,其他逻辑都做完了。但是细心的小强同学却发现了:“老师,怪物攻击Player的时候没有动作展示!”
那接下来我们就把动画补上吧!
三、实现怪物动画添加
怪物动画的转换只有一种情况:怪物遇到Player并且进行攻击,这时候怪物的动画从idle切换到attack,并且在attack动画结束之后切换回idle。
其实在上一节我们已经介绍过角色动画的转换是如何设置的,而怪物的动画设置上基本上是一致的,所以很多细节和这样设置的原因是什么我就不再赘述了。
- 双击Enemy1动画控制机打开Animator面板,在Parameters里增加Trigger名为enemyAttack。
- 通过右键的Make Transition在Enemy1Idle和Enemy1Attack之间创建连接。
- 选中高亮从Enemy1Idle出发到Enemy1Attack的线,对动画转换进行设置。
- 选中高亮从Enemy1IAttack出发到Enemy1Idle的线,对动画转换进行设置。
如此就完成了Enemy1Idle和Enemy1IAttack之间的互相转换的设置。由于Enemy2控制器是重写控制器,自动继承Enemy1的设置,所以不需要再去编辑Enemy2的两种动画状态之间的切换了。
怪物动画的切换设置完毕,我们需要在Enemy脚本里添加触发动画的代码。
代码简析:
- 新增一个私有成员animator,代表挂载在Enemy物体上的Animator组件,并且在Start()方法内进行初始化赋值。
- 在OnCantMove()方法内,遇到Player进行攻击的时候,调用animator的SetTrigger()方法来激活enemyAttack触发器,这样就会播放对应的attack动画。
可以看到怪物攻击小人的时候有对应的攻击动画出来啦!
ヾ(゚∀゚ゞ)快接近尾声了。接下来只剩下音乐音效、UI、切换关卡处理等部分了!等我一篇搞定~