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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容