美文网首页unity3D技术分享Unity基础入门分享征服Unity3d
Unity官方教程 2D Roguelike(5):敌人移动

Unity官方教程 2D Roguelike(5):敌人移动

作者: 小巷里有只猫 | 来源:发表于2019-08-23 19:09 被阅读0次
2D Roguelike 最终效果

前言

Unity官方教程 2D Roguelike(4):角色移动中,我们完成了游戏最主要的功能——角色移动相关逻辑,接下来只要完成怪物的移动,整个游戏的底层架构就差不多完整了,除了UI和音乐音效等。这一节我们主要完成以下内容:

  • 怪物移动逻辑
  • 怪物动画设置

本节你将学会什么?

  • 无新知识点,强化前面所掌握的内容

一、编辑Enemy Script

2D Roguelike是个回合制游戏,我动敌静,敌动我静。接下来创建一个Script,命名为Enemy,双击打开编辑怪物敌动代码吧!

第1步:MoveEnemy()

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>()

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>()

代码简读:

  • 我们还记得父类里有个泛型方法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,增加以下代码。

enemies

代码简读:

  • 新建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的Update() MovingObject Script

然后我们在Player Script的AttempMove()方法内对playerTurn进行赋值改动。

playerTurn改成false
也就是说,人物开始移动之后立刻把playerTurn赋值为false,这样游戏管理器的Update()判断playerTurn和enemyMoving都是false的时候,它开始指挥怪物们进行移动。怪物移动完毕之后,把playerTurn赋值为true,Player的Update()即可执行后续代码让Player开始第二次移动。
当然,我们也不要忘了即使轮到怪物回合了,它也有选择不走的权利!太懒惰了,怪物利用skipMove这个开关,成功实现人物走两次,它才动一次。(和我一样懒( >﹏<。)~)

第4步:执行移动

保存脚本,切回到Unity编辑器。打开Prefabs文件夹,同时选中Enemy1和Enemy2预制件,再点击菜单栏的Component-Scripts-Enemy,把Enemy脚本挂载到这两个预制件上。

挂载Enemy脚本

在右侧Inspector内的Enemy组件里,Blocking Layer选择BlockingLayer层。

设置Layer层

单独选择Enemy1预制件,把它的Player Damage设置为10,而Enemy2的为20。(这里可以自由发挥设置伤害为多少,但是注意不要过高一招就把Player秒了……)

设置Player Damage数值

最后一步,开测!

胜利就在前方,大家冲鸭!

运行游戏,我们按键盘的方向键让小人走起来。


可以看到游戏正常耍起了:

  • 小人和怪物都可以正常移动
  • 小人走两次,怪物才走一次
  • 怪物之间移动有先后次序
  • 小人可以正常拾取地上的食物
  • 小人可以正常劈砍障碍墙直到消失开辟路径

但其实我们会发现这个游戏的怪物移动逻辑会有一个缺点:怪物如果被障碍墙挡住了,会一直卡着不动,直到小人移动变换左右或者上下,它才有可能再动起来。这个是由于Enemy脚本的MoveEnemy()方法里面获取移动方向的设定上不够灵活。感兴趣的童鞋可以想想如何优化这个怪物AI~

虽然看起来除了音乐和UI,其他逻辑都做完了。但是细心的小强同学却发现了:“老师,怪物攻击Player的时候没有动作展示!”

真聪明!

那接下来我们就把动画补上吧!

三、实现怪物动画添加

怪物动画的转换只有一种情况:怪物遇到Player并且进行攻击,这时候怪物的动画从idle切换到attack,并且在attack动画结束之后切换回idle。
其实在上一节我们已经介绍过角色动画的转换是如何设置的,而怪物的动画设置上基本上是一致的,所以很多细节和这样设置的原因是什么我就不再赘述了。

  1. 双击Enemy1动画控制机打开Animator面板,在Parameters里增加Trigger名为enemyAttack
动画触发器
  1. 通过右键的Make Transition在Enemy1Idle和Enemy1Attack之间创建连接。
动画切换关联
  1. 选中高亮从Enemy1Idle出发到Enemy1Attack的线,对动画转换进行设置。
Idle到Attack
  1. 选中高亮从Enemy1IAttack出发到Enemy1Idle的线,对动画转换进行设置。
Attack结束回到Idle

如此就完成了Enemy1Idle和Enemy1IAttack之间的互相转换的设置。由于Enemy2控制器是重写控制器,自动继承Enemy1的设置,所以不需要再去编辑Enemy2的两种动画状态之间的切换了。
怪物动画的切换设置完毕,我们需要在Enemy脚本里添加触发动画的代码。

编写Enemy动画触发代码

代码简析:

  • 新增一个私有成员animator,代表挂载在Enemy物体上的Animator组件,并且在Start()方法内进行初始化赋值。
  • 在OnCantMove()方法内,遇到Player进行攻击的时候,调用animator的SetTrigger()方法来激活enemyAttack触发器,这样就会播放对应的attack动画。

可以看到怪物攻击小人的时候有对应的攻击动画出来啦!
ヾ(゚∀゚ゞ)快接近尾声了。接下来只剩下音乐音效、UI、切换关卡处理等部分了!等我一篇搞定~

上一章传送门:角色移动
下一章传送门:音乐音效、UI

相关文章

网友评论

    本文标题:Unity官方教程 2D Roguelike(5):敌人移动

    本文链接:https://www.haomeiwen.com/subject/tfhgdqtx.html