
用動畫狀態(tài)機(jī)當(dāng)AI狀態(tài)機(jī)寫ai邏輯基本上都需要狀態(tài)機(jī)。因?yàn)閼械檬执隊(duì)顟B(tài)機(jī)所以選擇直接用動畫狀態(tài)機(jī)當(dāng)邏輯狀態(tài)機(jī)用。AI的基本行為不停檢測視野范圍內(nèi)是否有敵人沒發(fā)現(xiàn)敵人時(shí)靜止、沿設(shè)定路線巡邏或隨意走動發(fā)現(xiàn)敵人后進(jìn)入警戒狀態(tài)并進(jìn)入攻擊狀態(tài)轉(zhuǎn)向敵人攻擊丟失目標(biāo)后保持警戒一段時(shí)間然后進(jìn)入安全狀態(tài)丟失目標(biāo)后可以選擇是否追擊追擊則移動到受攻擊后會進(jìn)入警戒狀態(tài)警戒狀態(tài)隨意走動指定一個附近目的地朝向它前進(jìn)同時(shí)不停檢測是否已經(jīng)足夠接近接近了就停止再指定目的地循環(huán)警戒狀態(tài)環(huán)顧四周攻擊狀態(tài)朝向目標(biāo)開啟瞄準(zhǔn)約束開槍一段時(shí)間?;鹨欢螘r(shí)間然后檢查沒子彈就換彈如此循環(huán)AI程序的特點(diǎn)是在一個檢測狀態(tài)的大循環(huán)之下各狀態(tài)有攻擊、環(huán)顧、走動等小循環(huán)。架構(gòu)設(shè)計(jì)因?yàn)閿橙说母?jié)點(diǎn)已經(jīng)有一個animator控制動畫只能增加一個子對象AI給它加一個animator指向邏輯“動畫”狀態(tài)機(jī)。還有一個腳本用來放一些檢測函數(shù)和動畫事件函數(shù)。狀態(tài)圖設(shè)計(jì)所有狀態(tài)都不停執(zhí)行檢測敵人的方法。動畫剪輯設(shè)置動畫剪輯添加一個無關(guān)緊要的屬性如Scale來卡時(shí)間。重要的是在特定的時(shí)間執(zhí)行動畫事件。如在Safe一段時(shí)間后開始巡邏public void StartPatrolling(){ enemyController.SetBool(patrolling,true); }攻擊狀態(tài)開槍幾秒停歇幾秒然后檢查是否該換彈狀態(tài)機(jī)行為腳本主要用于在特定狀態(tài)才每幀執(zhí)行的代碼。在狀態(tài)機(jī)行為腳本里滿足某些條件時(shí)執(zhí)行animator.SetXXX()改變動畫參數(shù)動畫參數(shù)改變又引起狀態(tài)轉(zhuǎn)換執(zhí)行新的狀態(tài)機(jī)行為腳本可以達(dá)到狀態(tài)機(jī)“自驅(qū)動”的效果。public class EnemyAlert : StateMachineBehaviour { Character1 myCharacter; MyNPCAI myAI; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){ myCharacteranimator.transform.parent.GetComponentCharacter1(); myCharacter.PutAwayGun(); myAIanimator.GetComponentMyNPCAI(); myCharacter.PlayRandomClip(myAI.findAudio); myCharacter.UseRifle(); myAI.StopLookingAround(); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){ if(myAI.DetectEnemy()){ animator.SetBool(MyNPCAI.foundEnemyPara,true); } } }但是每個狀態(tài)機(jī)行為腳本進(jìn)入時(shí)都要獲取一遍組件。這很惡心。慢慢地我發(fā)現(xiàn)用協(xié)程好像也能替代狀態(tài)機(jī)行為的功能只需要狀態(tài)機(jī)動畫事件協(xié)程就狀態(tài)里的小狀態(tài)Alert交替進(jìn)行環(huán)顧四周、隨機(jī)移動。環(huán)顧四周是一個過程可以指定目標(biāo)方向或旋轉(zhuǎn)時(shí)間移動也是類似的過程攻擊要先轉(zhuǎn)向目標(biāo)然后交替進(jìn)行射擊和停火也是個子狀態(tài)機(jī)其他狀態(tài)要想更細(xì)都會變成子狀態(tài)機(jī)要把狀態(tài)分成小狀態(tài)嗎狀態(tài)會多一點(diǎn)多出來的轉(zhuǎn)換就更多了。或者可以用協(xié)程做小狀態(tài)怎么用協(xié)程做個小狀態(tài)機(jī)可以在行為IEnumerator里開啟下一個行為的協(xié)程會不會有什么問題AI檢測敵人一開始我想讓AI能發(fā)現(xiàn)前方扇形區(qū)域的敵人沒有扇形碰撞體用代碼寫了一個。后來為了簡單直接在人物前方用球形范圍檢測了。除了范圍檢測還要用Physics.LineCast()檢測中間有沒有障礙物。bool DetectEnemy() { Vector3 center transform.position transform.forward * detectRadius; Collider[] colliders Physics.OverlapSphere(center, detectRadius, MyGameManager.Instance.aiDetectLayers); for (int i 0; i colliders.Length; i) { CharacterBase character; if (colliders[i].TryGetComponent(out character) !CheckBarrier(character) character.HP 0) { if (character.characterSide ! myCharacter.characterSide) { target character; targetLastPositioncharacter.transform.position; return true; } } } return false; }AI瞄準(zhǔn)敵人給AI的槍綁定對象加了AimConstraint使槍對準(zhǔn)敵人省去了寫復(fù)雜的瞄準(zhǔn)算法。為了使AI不每槍必中給AimVector加了隨機(jī)誤差。void AddAimError(){ gunAim.aimVectorVector3.forward UnityEngine.Random.Range(-aimErrorRange,aimErrorRange)*Vector3.up UnityEngine.Random.Range(-aimErrorRange,aimErrorRange)*Vector3.right; }這容易造成AI身體沒指向目標(biāo)時(shí)就打開瞄準(zhǔn)約束造成很不自然的樣子。AI攻擊流程進(jìn)入攻擊狀態(tài)后轉(zhuǎn)向目標(biāo)面向目標(biāo)后交替開火和?;稹9暨^程中要一直朝向敵人沒有朝向敵人不能開火。public void TurnTo(Vector3 target) { Vector3 aimVector target - myCharacter.transform.position; aimVector new Vector3(aimVector.x, 0, aimVector.z); if (aimVector Vector3.zero) { return; } Quaternion targetRotation Quaternion.LookRotation(aimVector); myCharacter.transform.rotationQuaternion.Lerp(myCharacter.transform.rotation, targetRotation,.2f); }AI環(huán)顧四周先得到一個隨機(jī)方向A然后一幀幀轉(zhuǎn)過去和方向A的偏差小于一個值后再得到隨機(jī)方向循環(huán)。效果演示Unity簡單敵人邏輯演示巡邏、發(fā)現(xiàn)、攻擊、追擊_演示總結(jié)使用狀態(tài)機(jī)寫AI一開始會想分成安全、警戒、攻擊等狀態(tài)但是很快會發(fā)現(xiàn)這樣粗略的狀態(tài)劃分不夠大狀態(tài)下必然有次級狀態(tài)規(guī)定人物的行為人物的行為不是執(zhí)行一個函數(shù)就完成都需要持續(xù)一段時(shí)間有的持續(xù)一段時(shí)間結(jié)束有的持續(xù)中還要檢測結(jié)束條件不用動畫狀態(tài)機(jī)寫ai首先還是狀態(tài)基類、狀態(tài)管理器狀態(tài)基類有進(jìn)入、更新、退出。然后狀態(tài)管理器持有狀態(tài)的方式可以用字段也可以用字典。我選擇直接讓管理器持有每種狀態(tài)作為字段因?yàn)闋顟B(tài)機(jī)擁有的狀態(tài)數(shù)量是寫死的不需要運(yùn)行中添加移出狀態(tài)反而需要一開始就保證所有需要的狀態(tài)都有了。記錄當(dāng)前狀態(tài)的方法可以直接維護(hù)一個狀態(tài)父類字段但是我們想在檢查器看到當(dāng)前狀態(tài)那么可以自定義檢查器打印當(dāng)前狀態(tài)的名字。如果用字典記錄狀態(tài)則可以弄狀態(tài)枚舉枚舉記錄當(dāng)前狀態(tài)檢查器顯示枚舉。那樣除了定義一堆狀態(tài)子類還要一一定義枚舉。然后更新狀態(tài)是用switch還是用多態(tài)方法。用switch狀態(tài)多了會變成巨量代碼。最終我們選定的方案組合是字段存儲狀態(tài)無動態(tài)增減狀態(tài)需求代碼比定義字典然后添加狀態(tài)少一些一個狀態(tài)父類字段表示當(dāng)前狀態(tài)用當(dāng)前狀態(tài)的Update多態(tài)更新狀態(tài)然后難搞的還是那幾個問題大狀態(tài)里的小狀態(tài)就拿人物射擊攻擊為例分為轉(zhuǎn)向、開槍、間歇。開槍和間歇循環(huán)開槍期結(jié)束后要檢查要不要換彈。這個攻擊狀態(tài)本身就是一個狀態(tài)機(jī)。時(shí)間控制。以人物開槍0.6秒為例動畫狀態(tài)機(jī)在動畫剪輯里很容易控制時(shí)間純程序要么計(jì)時(shí)器要么協(xié)程。我們需要手搓一個類似Animation時(shí)間軸配置動畫事件的界面。我們想到弄一個列表列表元素里記錄距上一個動作的時(shí)間以及這個動作的方法名字?;蛘呷绻麘械锰罘椒謩t用方法枚舉弄一個枚舉-方法映射字典。