前言
在Unity官方教程 2D Roguelike(4):角色移動(dòng)中,我們完成了游戲最主要的功能——角色移動(dòng)相關(guān)邏輯,接下來只要完成怪物的移動(dòng),整個(gè)游戲的底層架構(gòu)就差不多完整了,除了UI和音樂音效等。這一節(jié)我們主要完成以下內(nèi)容:
- 怪物移動(dòng)邏輯
- 怪物動(dòng)畫設(shè)置
本節(jié)你將學(xué)會(huì)什么?
- 無新知識(shí)點(diǎn),強(qiáng)化前面所掌握的內(nèi)容
一、編輯Enemy Script
2D Roguelike是個(gè)回合制游戲,我動(dòng)敵靜,敵動(dòng)我靜。接下來創(chuàng)建一個(gè)Script,命名為Enemy,雙擊打開編輯怪物敵動(dòng)代碼吧!
第1步:MoveEnemy()
代碼簡讀:
- Enemy類必須繼承MovingObject類,所以冒號(hào)后面記得修改。
- 新增私有成員變量target并在Start()進(jìn)行初始化賦值,代表Player的位置,怪物移動(dòng)是根據(jù)Player的位置來決定方向。
- 對(duì)Start()進(jìn)行了重寫,所以需要添加修飾關(guān)鍵詞override,然后通過base調(diào)用了父類的Start()方法。
- 新增公共方法MoveEnemy(),定義了int類型變量xDir、yDir初始化為0,代表怪物移動(dòng)方向向量。
當(dāng)Player和Enemy在同一個(gè)X坐標(biāo),則對(duì)兩個(gè)物體的Y坐標(biāo)進(jìn)行高低判斷,如果target(Player)的y值高,則移動(dòng)方向是向上移動(dòng),yDir為1,否則為-1向下移動(dòng)。
當(dāng)Player和Enemy不在同一個(gè)X坐標(biāo),則直接判斷X高低,target高則xDir為1向右移動(dòng),否則為-1向左移動(dòng)。
游戲管理器GameController會(huì)調(diào)用MoveEnemy()方法進(jìn)行指揮怪物隊(duì)列移動(dòng),因此關(guān)鍵詞為public。
Mathf.Abs指的是絕對(duì)值。float.Epsilon是最小浮點(diǎn)值,接近0。
條件?結(jié)果A:結(jié)果B,是三元運(yùn)算符的表達(dá)式,條件結(jié)果為true則是表達(dá)式結(jié)果是A,否則是B。
從上面的代碼解析可以看出,MoveEnemy()干的活就是根據(jù)Player坐標(biāo)確定怪物移動(dòng)方向,然后調(diào)用AttempMove<Player>()進(jìn)行真正移動(dòng)。
第2步:AttempMove<T>()
代碼簡讀:
- 這游戲并非是Player走一回合,怪物走一回合。而是Player走兩回合,怪物才能走一回合。帶著這個(gè)認(rèn)知去看這一段代碼就很好理解。
- 新增布爾值類型的私有成員變量skipMove,用它來控制怪物是否跳過這回合。
- 在AttempMove<T>(),判斷skipMove是否為true,如果是的話這回合怪物要跳過不進(jìn)行移動(dòng),就return跳出這個(gè)方法不執(zhí)行后續(xù)代碼。如果是false,則調(diào)用了父類MovingObject的AttempMove<T>()方法進(jìn)行移動(dòng),最后重新把skipMove賦值為true,保證下一回合怪物不能移動(dòng)。
從上面的代碼解析可以看出,AttempMove<T>()干的活就是接收到MoveEnemy()的信息通知往哪邊移動(dòng)的時(shí)候,判斷下這回合要不要跳過,然后再進(jìn)行移動(dòng)。
OK,怪物開始移動(dòng)了!噠噠噠,噠噠噠,誒?遇見Player了!好家伙,要打的就是你!
第3步:OnCantMove<T>()
代碼簡讀:
- 我們還記得父類里有個(gè)泛型方法OnCantMove<T>()吧?因?yàn)樗浅橄蠓椒ǎ枰宇惾ソo出具體實(shí)現(xiàn),因此我們?cè)诜椒ㄇ凹由狭?strong>override修飾符。
- 在父類MovingObject我們可以看到,OnCantMove<T>()這里的泛型參數(shù)T的類型是組件Component,而Player組件也是Component的一種,所以傳入的參數(shù)可以在方法內(nèi)轉(zhuǎn)化為Player類型,并且調(diào)用Player的LoseFood方法來扣除角色生命。playerDamage指的是怪物攻擊角色造成的傷害,也就是每次被打角色生命扣除數(shù)值。
二、編輯GameController
在上面的Enemy Script里,我們實(shí)現(xiàn)了怪物基本移動(dòng)邏輯(根據(jù)Player坐標(biāo)確定方向進(jìn)行移動(dòng),與Player碰撞的時(shí)候調(diào)用LoseFood()方法實(shí)現(xiàn)攻擊Player效果)。作為游戲管理器的GameController,則負(fù)責(zé)調(diào)控全局,指揮多個(gè)怪物在一定的條件下依次調(diào)用Enemy Script進(jìn)行移動(dòng)。
第1步:增加怪物隊(duì)列集合
打開GameController,增加以下代碼。
代碼簡讀:
- 新建List類型的私有變量成員enemies并在Awake()內(nèi)初始化,集合里面存放的是Enemy類型的數(shù)據(jù),也就是把關(guān)卡內(nèi)的所有怪物都放進(jìn)去。
- 添加AddEnemyToList()方法,通過Add()方法把傳入的Enemy類型的參數(shù)都添加到enemies。
- 因?yàn)橹匦律申P(guān)卡的時(shí)候,enemies數(shù)據(jù)會(huì)被保留,所以需要在InitGame()初始化關(guān)卡時(shí)用Clear()方法清除上一個(gè)關(guān)卡的敵人數(shù)組。
切回到Enemy Script,在Start()方法內(nèi)增加一句代碼。
這句代碼的意思是調(diào)用GameController的公有方法AddEnemyToList(),把自己(當(dāng)前實(shí)例)當(dāng)成參數(shù)傳入進(jìn)去,也就是把當(dāng)前怪物加進(jìn)集合enemies。正因?yàn)樾枰谶@里進(jìn)行調(diào)用,所以AddEnemyToList()方法的關(guān)鍵詞是public哦~
第2步:指揮怪物們依次移動(dòng)
關(guān)卡內(nèi)的怪物都被添加進(jìn)集合enemies內(nèi)了,接下來就是在恰當(dāng)?shù)臅r(shí)機(jī)指揮這些怪物一個(gè)個(gè)移動(dòng)啦!
代碼簡讀:
- 新增浮點(diǎn)值變量turnDelay并賦值,代表回合等待時(shí)間,單位為s。
- 新增布爾值類型變量enemyMoving,代表怪物們是不是正在移動(dòng)中,正在移動(dòng)則為true,其他情況則為false。
- Update()方法,判斷當(dāng)playerTrun和enemyMoving均為false的情況下(是怪物回合并且怪物沒有在移動(dòng)中),使用StartCoroutine函數(shù)開啟協(xié)同程序MoveEnemys()指揮怪物開始一個(gè)個(gè)進(jìn)行移動(dòng);
協(xié)程是分步驟執(zhí)行代碼的程序,遇到條件(yield return語句)會(huì)掛起暫停退出,直到條件滿足才會(huì)被喚醒繼續(xù)執(zhí)行后面的代碼。
- MoveEnemys()方法,把enemyMoving賦值為true確保Update()不再執(zhí)行開啟協(xié)程的代碼,等待turnDelay時(shí)長之后(為了讓Player走完),判斷如果沒有怪物的時(shí)候再等待turnDelay時(shí)長(讓回合感更明顯,Player不能一直不停地移動(dòng));如果有怪物的話,則開始for循環(huán)敵人數(shù)組enemies,調(diào)用MoveEnemy()方法指揮他們一個(gè)個(gè)移動(dòng)。為了實(shí)現(xiàn)依次的效果而不是同時(shí)移動(dòng),加了間隔時(shí)長moveTime。
- 所有敵人移動(dòng)完畢之后,把人物回合開關(guān)開起來(playerTurn為true),敵人移動(dòng)中開關(guān)關(guān)掉(enemyMoving為false),重新把回合權(quán)交給了Player。
第3步:填坑-playerTurn開關(guān)
在移動(dòng)邏輯章節(jié)的第三節(jié)內(nèi)的第3步,當(dāng)時(shí)為了修正按一次方向鍵而Player移動(dòng)了多次的問題,我們臨時(shí)增加了一些代碼。現(xiàn)在已完成怪物移動(dòng)代碼,可以正常地進(jìn)行人物怪物交換來回移動(dòng),因此我們需要把之前臨時(shí)增加的三句代碼刪除。
然后我們?cè)赑layer Script的AttempMove()方法內(nèi)對(duì)playerTurn進(jìn)行賦值改動(dòng)。
也就是說,人物開始移動(dòng)之后立刻把playerTurn賦值為false,這樣游戲管理器的Update()判斷playerTurn和enemyMoving都是false的時(shí)候,它開始指揮怪物們進(jìn)行移動(dòng)。怪物移動(dòng)完畢之后,把playerTurn賦值為true,Player的Update()即可執(zhí)行后續(xù)代碼讓Player開始第二次移動(dòng)。
當(dāng)然,我們也不要忘了即使輪到怪物回合了,它也有選擇不走的權(quán)利!太懶惰了,怪物利用skipMove這個(gè)開關(guān),成功實(shí)現(xiàn)人物走兩次,它才動(dòng)一次。(和我一樣懶( >﹏<。)~)
第4步:執(zhí)行移動(dòng)
保存腳本,切回到Unity編輯器。打開Prefabs文件夾,同時(shí)選中Enemy1和Enemy2預(yù)制件,再點(diǎn)擊菜單欄的Component-Scripts-Enemy,把Enemy腳本掛載到這兩個(gè)預(yù)制件上。
在右側(cè)Inspector內(nèi)的Enemy組件里,Blocking Layer選擇BlockingLayer層。
單獨(dú)選擇Enemy1預(yù)制件,把它的Player Damage設(shè)置為10,而Enemy2的為20。(這里可以自由發(fā)揮設(shè)置傷害為多少,但是注意不要過高一招就把Player秒了……)
最后一步,開測!
運(yùn)行游戲,我們按鍵盤的方向鍵讓小人走起來。
可以看到游戲正常耍起了:
- 小人和怪物都可以正常移動(dòng)
- 小人走兩次,怪物才走一次
- 怪物之間移動(dòng)有先后次序
- 小人可以正常拾取地上的食物
- 小人可以正常劈砍障礙墻直到消失開辟路徑
但其實(shí)我們會(huì)發(fā)現(xiàn)這個(gè)游戲的怪物移動(dòng)邏輯會(huì)有一個(gè)缺點(diǎn):怪物如果被障礙墻擋住了,會(huì)一直卡著不動(dòng),直到小人移動(dòng)變換左右或者上下,它才有可能再動(dòng)起來。這個(gè)是由于Enemy腳本的MoveEnemy()方法里面獲取移動(dòng)方向的設(shè)定上不夠靈活。感興趣的童鞋可以想想如何優(yōu)化這個(gè)怪物AI~
雖然看起來除了音樂和UI,其他邏輯都做完了。但是細(xì)心的小強(qiáng)同學(xué)卻發(fā)現(xiàn)了:“老師,怪物攻擊Player的時(shí)候沒有動(dòng)作展示!”
那接下來我們就把動(dòng)畫補(bǔ)上吧!
三、實(shí)現(xiàn)怪物動(dòng)畫添加
怪物動(dòng)畫的轉(zhuǎn)換只有一種情況:怪物遇到Player并且進(jìn)行攻擊,這時(shí)候怪物的動(dòng)畫從idle切換到attack,并且在attack動(dòng)畫結(jié)束之后切換回idle。
其實(shí)在上一節(jié)我們已經(jīng)介紹過角色動(dòng)畫的轉(zhuǎn)換是如何設(shè)置的,而怪物的動(dòng)畫設(shè)置上基本上是一致的,所以很多細(xì)節(jié)和這樣設(shè)置的原因是什么我就不再贅述了。
- 雙擊Enemy1動(dòng)畫控制機(jī)打開Animator面板,在Parameters里增加Trigger名為enemyAttack。
- 通過右鍵的Make Transition在Enemy1Idle和Enemy1Attack之間創(chuàng)建連接。
- 選中高亮從Enemy1Idle出發(fā)到Enemy1Attack的線,對(duì)動(dòng)畫轉(zhuǎn)換進(jìn)行設(shè)置。
- 選中高亮從Enemy1IAttack出發(fā)到Enemy1Idle的線,對(duì)動(dòng)畫轉(zhuǎn)換進(jìn)行設(shè)置。
如此就完成了Enemy1Idle和Enemy1IAttack之間的互相轉(zhuǎn)換的設(shè)置。由于Enemy2控制器是重寫控制器,自動(dòng)繼承Enemy1的設(shè)置,所以不需要再去編輯Enemy2的兩種動(dòng)畫狀態(tài)之間的切換了。
怪物動(dòng)畫的切換設(shè)置完畢,我們需要在Enemy腳本里添加觸發(fā)動(dòng)畫的代碼。
代碼簡析:
- 新增一個(gè)私有成員animator,代表掛載在Enemy物體上的Animator組件,并且在Start()方法內(nèi)進(jìn)行初始化賦值。
- 在OnCantMove()方法內(nèi),遇到Player進(jìn)行攻擊的時(shí)候,調(diào)用animator的SetTrigger()方法來激活enemyAttack觸發(fā)器,這樣就會(huì)播放對(duì)應(yīng)的attack動(dòng)畫。
可以看到怪物攻擊小人的時(shí)候有對(duì)應(yīng)的攻擊動(dòng)畫出來啦!
ヾ(???ゞ)快接近尾聲了。接下來只剩下音樂音效、UI、切換關(guān)卡處理等部分了!等我一篇搞定~