【Unity俯視角射擊】我們來做一個(gè)《元?dú)怛T士》的完整Demo(二)

作者:Yumir
hi~我是Yumir。
今天要分享的內(nèi)容是進(jìn)階版的AI尋路,之前分享過使用A*Pathfinding官方提供的AI腳本進(jìn)行移動(dòng),但是要做一個(gè)完整的游戲使用該腳本有諸多不便,于是決定還是利用官方提供的尋路接口自己寫一個(gè)AI移動(dòng)腳本。
在線游戲鏈接:https://connect.unity.com/mg/other/untitled-9864
怪物設(shè)計(jì)
目前我用到的怪物角色有下圖三種,其中三號(hào)怪物放大一倍之后作為Boss使用于Boss房。

①野豬怪
一號(hào)怪物的狀態(tài)我用了待機(jī)、巡邏(實(shí)際上就是亂跑)還有死亡等三個(gè)狀態(tài),當(dāng)野豬怪在奔跑途中撞到玩家會(huì)對(duì)玩家造成傷害。
②吐泡泡的花
二號(hào)怪物是在出生點(diǎn)不會(huì)亂動(dòng)的怪物,所以只有待機(jī)、攻擊、死亡三個(gè)狀態(tài),當(dāng)玩家進(jìn)入二號(hào)怪物的攻擊范圍時(shí)二號(hào)怪物會(huì)向四周發(fā)射泡泡保護(hù)自己。
③戴面具的人形生物
三號(hào)怪是等級(jí)比較高的怪物,可以和玩家一樣使用武器,所以我給他配了一把和玩家一樣的玩具槍,狀態(tài)機(jī)相對(duì)也更復(fù)雜,有待機(jī)、巡邏、追逐、攻擊、死亡等五個(gè)狀態(tài)。
④Boss
關(guān)于四號(hào)怪其實(shí)我一開始不打算這樣設(shè)計(jì)的,后來發(fā)現(xiàn)元?dú)怛T士的新手教程也是把三號(hào)怪放大當(dāng)Boss用,而且在彈幕設(shè)計(jì)方面我確實(shí)并不擅長,所以暫時(shí)先這樣設(shè)計(jì)了,可以以后再優(yōu)化。
Boss使用的武器是權(quán)杖,由于本身傷害比較大范圍比較廣,還進(jìn)行追逐的話游戲體驗(yàn)就基本沒有了,所以Boss的狀態(tài)有:待機(jī)、巡邏、攻擊、死亡。在Boss進(jìn)行攻擊之后在“巡邏”和“攻擊”中隨機(jī)一個(gè)作為下一個(gè)狀態(tài)。
設(shè)置A*尋路
①設(shè)置尋路網(wǎng)格
之前的文章已經(jīng)寫過這部分內(nèi)容,簡略的說一下:
1.在官網(wǎng):https://arongranberg.com/astar/? ?
點(diǎn)擊Download選項(xiàng),在跳轉(zhuǎn)到的頁面上選擇下載”Free“版本,將下載下來的文件導(dǎo)入到unity中。
2.新建一個(gè)空物體,點(diǎn)擊”AddComponent“搜索”Pathfinder“添加該組件。添加組件之后面板顯示如下,點(diǎn)擊圖中框選按鈕添加Grid Graph(Graphs>Grid Graph)。

3.將”2D“和”Use 2D Physic“勾選,再在第三個(gè)紅框的位置選擇一個(gè)Layer,同時(shí)將場(chǎng)景中的障礙物的Layer都設(shè)置為對(duì)應(yīng)的Layer。

4.點(diǎn)擊Scan按鈕,生成尋路網(wǎng)格。

②編寫尋路腳本
之前的文章用了官方提供的尋路腳本,其實(shí)使用官方的提供的路徑計(jì)算接口自己編寫一個(gè)尋路腳本也是很簡單的。官方也提供了范例腳本,文末會(huì)貼上知乎大佬的翻譯貼。
尋路這件事其實(shí)可以拆解成兩個(gè)步驟,現(xiàn)代人準(zhǔn)備去一個(gè)不認(rèn)識(shí)路的目的地的情況下會(huì)怎么做?
首先,打開地圖進(jìn)行路線搜索,其實(shí)在沒有地圖app的時(shí)代問路的原理也是一樣,說白了就是規(guī)劃路線,然后就是沿著路線向目的地出發(fā)了。
AI尋路的道理也一樣,先計(jì)算路徑,再沿路徑移動(dòng):
1.計(jì)算路徑
在A*Pathfinding中只要調(diào)用Seeker組件里的StartPath方法就可以進(jìn)行路徑的計(jì)算。
由于需要使用Seeker組件所以需要先在需要尋路的角色(就是游戲中的怪物)身上添加Seeker組件,然后新建一個(gè)腳本AstarAI編寫如下代碼:
using Pathfinding;
?
public class AstarAI : MonoBehaviour {
??? public Transform targetPosition;
?
??? public void Start () {
??????? //獲取到A*Pathfinding提供的路徑計(jì)算接口所在的腳本。
??????? Seeker seeker = GetComponent<Seeker>();
??????? //調(diào)用路徑計(jì)算方法,這里需要一個(gè)回調(diào)方法,但是是測(cè)試代碼所以沒有寫內(nèi)容。
??????? seeker.StartPath(transform.position, targetPosition.position, OnPathComplete);
??? }
?
??? public void OnPathComplete (Path p) {
??????? Debug.Log("Yay, we got a path back. Did it have an error? " + p.error);
??? }
}
?
2.沿路徑移動(dòng)

當(dāng)尋路成功的時(shí)候OnPathComplete方法會(huì)被調(diào)用,我們將獲得一個(gè)Path,在將他裝到兜里之前需要先確認(rèn)下他是不是尋路成功了。
public void OnPathComplete (Path p) {
?if (!p.error) {
??????????? path = p;
??????????? currentWaypoint = 0;
?}
?
獲取到的Path中有一個(gè)Vector3類型的列表,這是我們需要的路徑點(diǎn)集合,接下來只需要讓角色從第一個(gè)路徑點(diǎn)開始不斷朝著下一個(gè)路徑點(diǎn)移動(dòng)就可以了,案例代碼比較長,直接把我最終的代碼放到下文。
由于上面的方法只是在Start中調(diào)用StartPath進(jìn)行一次尋路,目標(biāo)點(diǎn)是固定不變的。
而游戲中玩家不是站著等怪物找的,也就需要每隔一段時(shí)間更新目標(biāo)位置重新規(guī)劃路線,考慮到AI狀態(tài)機(jī)的需求,我將上面兩個(gè)操作分別封裝成了方法。
由于玩家可能動(dòng)也可能不動(dòng),所以添加了一個(gè)目標(biāo)點(diǎn)移動(dòng)距離超過1的條件,這樣就不會(huì)重復(fù)沒有必要的路徑計(jì)算。
public void OnPathComplete(Path p){
??? if (!p.error){
??????? path = p;
??????? currentWaypoint = 0;
??? }
}
public void UpdatePath(Vector2 targetPosition){
??? if (Vector2.Distance(targetPosition, targetLastPosition) > 1){
??????? targetLastPosition = targetPosition;
??????? seeker.StartPath(transform.position, targetPosition, OnPathComplete);
??? }
}
?
控制角色向下一個(gè)目標(biāo)點(diǎn)前進(jìn)的方法:
public void NextTarget(){
??? if (path == null){ return; }
??? reachedEndOfPath = false;//標(biāo)記是否已經(jīng)到達(dá)目標(biāo)點(diǎn)
??? float distanceToWaypoint;
??? while (true){
??????? distanceToWaypoint = Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]);
??????? if (distanceToWaypoint < nextWaypointDistance){
??????????? if (currentWaypoint + 1 < path.vectorPath.Count){ currentWaypoint++; }
??????????? else{ reachedEndOfPath = true; break; }
??????? }
??????? else{ break; }
??? }
??? var speedFactor = reachedEndOfPath ? Mathf.Sqrt(distanceToWaypoint / nextWaypointDistance) : 1f;
??? if (!reachedEndOfPath){
??????? nextTargetPosition = path.vectorPath[currentWaypoint];
??????? Vector3 dir = (nextTargetPosition - transform.position).normalized;
??????? Vector3 velocity = dir * speed * speedFactor;
??????? transform.position += velocity * Time.deltaTime;
??? }
??? else{ path = null; }
}
?
怪物AI邏輯實(shí)現(xiàn)
文章篇幅有限,所以只舉例三號(hào)怪物的實(shí)現(xiàn)過程,因?yàn)槠渌值腁I邏輯都相對(duì)簡單,相信看這個(gè)文章的朋友看完三號(hào)怪物的AI功能實(shí)現(xiàn)過程就可以自己做出其他的AI效果了。我在游戲設(shè)計(jì)上并沒有做的很好建議自己擴(kuò)展設(shè)計(jì)。

①待機(jī)
游戲中怪物是在玩家進(jìn)入怪物所在的房間才需要開始計(jì)算路徑的,所以在玩家進(jìn)入房間前需要一個(gè)待機(jī)狀態(tài),在該狀態(tài)下怪物的動(dòng)畫狀態(tài)機(jī)播放idle動(dòng)畫,不需要額外設(shè)置。
當(dāng)玩家進(jìn)入房間時(shí),系統(tǒng)將該房間所有的怪物的標(biāo)記位“isStart”設(shè)置為true,怪物由待機(jī)轉(zhuǎn)為巡邏。
void Idle()
{
??? if (isStart)
??? {
??????? monsterState = MonsterState.Stroll;
??????? animator.SetBool("run", true);
??? }
}
?
②巡邏
設(shè)置這個(gè)狀態(tài)主要是希望怪物有“視野”這個(gè)設(shè)定,如果沒有視野的話最后就會(huì)變成一堆怪追著玩家和玩家一起繞柱走的狀態(tài),游戲體驗(yàn)極差,可以說是最基本的AI需求了。
顯然我不可能每個(gè)怪物去設(shè)置巡邏點(diǎn),隨機(jī)尋路目標(biāo)位置有一個(gè)很簡單的算法——在圓里隨機(jī)一個(gè)點(diǎn)(然后計(jì)算新的路徑)。
public void RandomPath(){
??? var point = Random.insideUnitSphere * randomRadius;
??? point += transform.position;
??? UpdatePath(point);
}
?
同時(shí)需要檢測(cè)怪物是否可以看到玩家:
public void RaycastDetection(){
??? hit = Physics2D.Raycast(transform.position + Vector3.up, (targetPosition.position - (transform.position + Vector3.up)).normalized, trackingRange, layerMask);
??? if (hit.transform != null && hit.transform == targetPosition){
??????? seeTarget = true;
??????? Debug.DrawLine(transform.position + Vector3.up, hit.transform.position, Color.red);
??? }
??? else{ seeTarget = false; }
}
?
在巡邏方法中只需要調(diào)用控制角色向下一個(gè)目標(biāo)點(diǎn)前進(jìn)的方法,并且使怪物看向下一個(gè)目標(biāo)點(diǎn)。每隔一段時(shí)間隨機(jī)一個(gè)新的目標(biāo)點(diǎn)并計(jì)算路徑,當(dāng)怪物看到玩家(seeTarget==true)時(shí)切換到追逐狀態(tài)。
void Stroll()
{
??? RaycastDetection();
??? if (seeTarget){ monsterState = MonsterState.Tracking; }
??? UpdateLookAt(myAI.nextTargetPosition);
??? if (Time.time - strolltiming >= strollCD){
??????????? strolltiming = Time.time;
??????????? myAI.RandomPath();
??? }
??? myAI.NextTarget();
}
?

③追逐
追逐狀態(tài)下無非就是追到和沒追到,這個(gè)需要用距離和視線判斷,該狀態(tài)下需要一直調(diào)用計(jì)算路徑方法和往下一個(gè)路徑點(diǎn)移動(dòng)的方法,如果和玩家距離小于等于攻擊距離就進(jìn)入攻擊狀態(tài),如果脫離了視野就回到巡邏狀態(tài)。
void Tracking(){
??? myAI.UpdatePath(targetPosition.position);
??? myAI.NextTarget();
??? if (Vector2.Distance(transform.position, targetPosition.position) <= attackRange){
??????? monsterState = MonsterState.Attack;
??????? animator.SetBool("run", false);
??? }
??? RaycastDetection();
??? if (!seeTarget){ monsterState = MonsterState.Stroll; }
??? UpdateLookAt(myAI.nextTargetPosition);
}
?

④攻擊
我在攻擊狀態(tài)下設(shè)置了攻擊計(jì)時(shí)器,主要是因?yàn)楣治锕籼斓脑捨掖虿贿^,如果你打得過你可以不設(shè)(\doge)然后就是根據(jù)距離切換回追逐狀態(tài),和面向玩家,沒啥可說的。
void Attack(){
??? if (Vector2.Distance(transform.position, targetPosition.position) > attackRange){
??????? monsterState = MonsterState.Tracking;
??? }
??? UpdateLookAt(targetPosition.position);
??? if (Time.time - attacktiming >= attackCD){
??????? attacktiming = Time.time;
??????? weapon.ShootButtonDown();
??? }
}
?
⑤死亡
在任何狀態(tài)下,只要血量為0就會(huì)死亡,所以該狀態(tài)的入口是寫在被攻擊的方法里面的,三號(hào)怪物死亡時(shí)會(huì)向后彈飛一段距離,這個(gè)我也是用尋路實(shí)現(xiàn)了:
public override void BeAttack(float data){
??? base.BeAttack(data);
??? if (hp <= 0){
??????? myAI.UpdatePath((transform.position - targetPosition.position).normalized * 2 + transform.position);
??????? weapon.gameObject.SetActive(false);
??? }
}
void Die(){ myAI.NextTarget(); }
?
可以看到我的BeAttack是Override,因?yàn)檫@個(gè)血量為0的狀態(tài)轉(zhuǎn)換是所有的怪通用的,所以我將通用邏輯都寫在了Monster父類中(包括死亡動(dòng)畫以及生成怪物掉落獎(jiǎng)勵(lì)等功能),再在子類中分別處理特殊要求。
public virtual void BeAttack(float data){
??? hp -= data;
??? if (hp <= 0)
??? {
??????? monsterState = MonsterState.Die;
??????? GetComponent<Animator>().SetBool("die", true);
??????? GetComponent<Collider2D>().enabled = false;
??????? room.MonsterDie(this);
??????? for (int i = 0; i < coin; i++){//掉落金幣
??????????? Instantiate(GameManager.instance.coinPre, transform.position, Quaternion.identity);
??????? }
??????? for (int i = 0; i < magic; i++){//掉落魔晶石
??????????? Instantiate(GameManager.instance.mpPre, transform.position, Quaternion.identity);
??????? }
??? }
??? else{
??????? GetComponent<Animator>().Play("BeAttack");
??????? GameManager.instance.ShowAttack(data, Camera.main.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0)));
??? }
}
?
關(guān)于怪物AI我已經(jīng)傾囊相授啦~接下來是游戲的小地圖繪制分享,游戲項(xiàng)目以及資源我會(huì)在游戲完成之后整理(放在第四篇文章)需要的同學(xué)可以關(guān)注一下。
知乎上有大佬翻譯了官方教程,下面這篇是本文提到的AI移動(dòng)腳本,我認(rèn)為比起去官網(wǎng)翻看相對(duì)來說更方便一些,有興趣可以看看:
https://zhuanlan.zhihu.com/p/69703555

歡迎加入游戲開發(fā)群歡樂攪基:1082025059
對(duì)游戲開發(fā)感興趣的童鞋可戳這里進(jìn)一步了解:http://www.levelpp.com/