基於 Unity 引擎的遊戲開發進階之 敵人AI

殭屍 AI

殭屍 AI 能根據自身情況(生命值,中槍與否)和外部條件(周圍是否存在玩家),採取合理的行動。

AI 基礎

  • 基本行為操控(靠近,遠離,追逐,逃避...)
  • 尋路能力(從遊戲場景中一個位置移動到另一個位置,最短路徑
  • 感知能力(自身狀態如血量,聽覺和視覺等感知能力)
  • 自主決策能力(根據自身狀態和外部條件作出合理的反應)

操控與尋路

使用 Unity 的 Navigation 導航系統,可以方便的操控殭屍的基本行為,使殭屍在運動過程中能夠自動尋路。

感知遊戲世界

模擬殭屍的視覺和聽覺的方法:

  • 觸發器(Trigger)
  • 向量計算(Vector)

這兩個方法實現難度差不多,向量計算的方法效率更高。

有限狀態機(Finite State Machine)

殭屍簡單的狀態機

稍微複雜的殭屍邏輯

  • 殭屍的感知範圍內沒有玩家時->殭屍隨機遊盪
  • 殭屍受到攻擊->殭屍向開槍時所在的方向進行搜索
  • 殭屍感知到玩家->殭屍追蹤玩家
  • 玩家進入殭屍攻擊範圍->殭屍攻擊玩家
  • 玩家死亡或脫離殭屍感知範圍->殭屍回到隨機遊盪
  • 殭屍生命值變為0->殭屍進入死亡狀態

這裡使用了教程中的截圖。

實現殭屍的感知能力

  • 外部世界(聽覺,視覺)
  • 自身情況(生命值,是否中槍)

使用觸發器 Triggle 感知的缺點

  • 我們需要使用三維建模軟體,製作出錐體網格來模擬可視區域。這種方式較為麻煩,且不易維護;
  • 物理計算,尤其是 MeshCollider 網格碰撞題的碰撞計算,開銷較大,會降低遊戲性能。

所以我們使用向量計算的方式來進行感知。

聽覺的模擬

殭屍的聽覺範圍可以視為一個球體,該球體的球心是殭屍。只要玩家位於該球體內部,我們就認為殭屍可以感知到玩家。

//計算玩家與殭屍之間的距離nfloat dist = Vector3.Distance (player.transform.position, zombieTransform.position);nn//如果玩家與殭屍的距離小於殭屍的聽覺距離,說明殭屍可以感覺到玩家nif (dist < SoundRange) {nt//緩存這個玩家ntnearbyPlayer = player.transform;n}n

視覺模擬

殭屍的視覺範圍使用殭屍眼睛正前方的一個圓錐體來模擬,只要玩家位於這個圓錐內部,且不被遮擋,我們就認為殭屍看見了玩家。

//如果玩家與殭屍的距離小於殭屍的視覺距離nif (dist < SightRange) {nt//計算玩家是否在殭屍的視角內ntVector3 direction = player.transform.position - zombieTransform.position;ntfloat degree = Vector3.Angle (direction, zombieTransform.forward);nntif (degree < SightAngle / 2 && degree > -SightAngle / 2) {ntRay ray = new Ray();tntray.origin = zombieEye.position;ttntray.direction = direction;ttntRaycastHit hitInfo;ttnt//判斷玩家和殭屍之間是否存在遮擋物ntif (Physics.Raycast (ray, out hitInfo, SightRange)) {nttif (hitInfo.transform == player.transform) {nttt//如果殭屍能夠看到玩家就緩存這個玩家ntttnearbyPlayer = player.transform;ntt}nt}n }n}n

  1. 先判斷殭屍與玩家之間的距離是否小於殭屍的視覺距離
  2. 計算從殭屍位置到玩家位置的向量和殭屍的 forward 向量之間的夾角,是否小於殭屍視覺圓錐夾角的一半
  3. 判斷從殭屍所在位置到玩家所在位置之間是否有物體阻擋

獲取感知信息

如果感知到玩家,那麼將玩家對象的引用保存在 nearbyPlayer 欄位中,否則,將 nearbyPlayer 欄位設置為 null。

//計算玩家與殭屍之間的距離nfloat dist = Vector3.Distance (player.transform.position, zombieTransform.position);nn//如果玩家與殭屍的距離小於殭屍的聽覺距離nif (dist < SoundRange) {nt//緩存這個玩家ntnearbyPlayer = player.transform;n} else {ntnearbyPlayer = null;n}n

持續感知

void FixedUpdate()nt{ntt//以一定的時間間隔,進行感知nttif (senseTimer >= SensorInterval) {ntttsenseTimer = 0;ntttSenseNearbyPlayer ();ntt}nttsenseTimer += Time.deltaTime;nnt}n

//獲得當前緩存的附近玩家對象,如果附近沒有玩家則返回nullntpublic Transform getNearbyPlayer()nt{nttreturn nearbyPlayer;nt}n

生命值感知和中槍感知

在 ZombieHealth 腳本的 TakeDamage 函數中,更新生命值,並記錄殭屍中槍時玩家所在的方向。

public void TakeDamage(int damage, Vector3 shootPosition){nttif (!IsAlive)ntttreturn;ntt//更新殭屍生命值nttcurrentHP -= damage;nttif (currentHP <= 0 ) currentHP = 0;nttif (IsAlive) {ttnttt//記錄殭屍的中槍狀態ntttgetDamaged = true;nttt//記錄殭屍受到攻擊時,玩家所在的方向ntttdamageDirection = shootPosition - transform.position;ntttdamageDirection.Normalize ();ttntt} n}n

實現殭屍的 AI

我們根據上面的 AI 狀態機圖編寫腳本。

殭屍狀態的定義

public enum FSMStatent{nttWander,tt//隨機遊盪狀態nttSeek,tt//搜索狀態nttChase,tt//追蹤狀態nttAttack,tt//攻擊狀態nttDead,tt//死亡狀態nt}n

更新狀態機

//定期更新殭屍狀態機的狀態,不斷更新狀態機ntvoid FixedUpdate()nt{nttFSMUpdate ();nt}nnt//殭屍狀態機更新函數ntvoid FSMUpdate()nt{ntt//根據殭屍當前的狀態調用相應的狀態處理函數nttswitch (currentState)ntt{nttcase FSMState.Wander: ntttUpdateWanderState();ntttbreak;nttcase FSMState.Seek:ntttUpdateSeekState();ntttbreak;nttcase FSMState.Chase:ntttUpdateChaseState();ntttbreak;nttcase FSMState.Attack:ntttUpdateAttackState();ntttbreak;nttcase FSMState.Dead:ntttUpdateDeadState ();ntttbreak;ntt}nt}n

遊盪狀態函數處理(UpdateWanderState)

//感知到周圍有活著的玩家,進入追蹤狀態ntttargetPlayer = zombieSensor.getNearbyPlayer ();nttif ( targetPlayer != null) {ntttcurrentState = FSMState.Chase;ntttagent.ResetPath ();ntttreturn;ntt}nntt//如果受到傷害,那麼進入搜索狀態nttif (zombieHealth.getDamaged) {ntttcurrentState = FSMState.Seek;ntttagent.ResetPath ();ntttreturn;ntt}n

如果既沒感知到,也沒有收到傷害,那麼就繼續進行遊盪。這裡是以殭屍為中心,wanderScope 為邊長的正方形區域內,隨機選擇一個位置,作為新的隨機遊盪目的地。

//如果沒有目標位置,那麼隨機選擇一個目標位置nttif (AgentDone () ) { //判斷殭屍是否到達上一次隨機遊盪的目的地ntttVector3 randomRange = new Vector3 ( (Random.value - 0.5f) * 2 * wanderScope, nttttttttttt0, (Random.value - 0.5f) * 2 * wanderScope);ntttVector3 nextDestination = zombieTransform.position + randomRange;ntttagent.destination = nextDestination;ntt} ntt//限制遊盪的速度nttsetMaxAgentSpeed(wanderSpeed);n

使用 AgentDone() 判斷導航是否結束:

//判斷殭屍是否在一次導航中到達了目的地ntprotected bool AgentDone()nt{n //有兩個條件,一個是導航代理的 pathPending 屬性變為 false。另一個條件是導航代理與目的地的剩餘距離 remainingDistance 小於其停止距離 stoppingDistance。nttreturn !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance;nt}n

//統計殭屍在當前位置附近的停留時間nttcaculateStopTime();nntt// 如果在一個地方停留太久(各種原因導致殭屍卡住)ntt// 那麼選擇殭屍背後的一個位置當做下一個目標nttif(stopTime > 1.0f)ntt{ntttVector3 nextDestination = zombieTransform.position ntttt- zombieTransform.forward * (Random.value) * wanderScope;ntttagent.destination = nextDestination;ntt}n

當殭屍被擊中後,殭屍進行搜索狀態,向著玩家射擊所在方向進行搜索。搜索狀態下發現玩家會進入追蹤狀態,到達最大搜索距離會回到隨機遊盪狀態。

搜索狀態處理函數(UpdateSeekState)

//如果殭屍感知範圍內有玩家,進入追蹤狀態ntttargetPlayer = zombieSensor.getNearbyPlayer ();nttif ( targetPlayer != null) {ntttcurrentState = FSMState.Chase;ntttagent.ResetPath ();ntttreturn;ntt}ntt//如果殭屍受到攻擊,那麼向著玩家開槍時所在的方向進行搜索nttif (zombieHealth.getDamaged) {ntttVector3 seekDirection = zombieHealth.damageDirection;ntttagent.destination = zombieTransform.position ntttt+ seekDirection * seekDistance;nttt//將getDamaged設置為false,表示已經處理了這次攻擊ntttzombieHealth.getDamaged = false;tntt}nntt//如果到達搜索目標,或者卡在某個地方無法到達目標位置,那麼回到遊盪狀態nttif (AgentDone () || stopTime > 1.0f ) {ntttcurrentState = FSMState.Wander;ntttagent.ResetPath ();ntttreturn;ntt} ntt//減速度限制為奔跑速度nttsetMaxAgentSpeed(runSpeed);n

追蹤狀態下,殭屍持續向玩家方向移動。如果殭屍和玩家的距離小於殭屍的攻擊距離,殭屍進入攻擊狀態。如果殭屍在追蹤狀態下丟失了玩家(玩家死亡,或者脫離了殭屍的感知範圍),那麼會進入隨機遊盪狀態。

追蹤狀態處理函數(UpdateChaseState)

//追蹤狀態處理函數ntvoid UpdateChaseState()nt{ntt//如果殭屍感知範圍內沒有玩家,進入遊盪狀態ntttargetPlayer = zombieSensor.getNearbyPlayer ();nttif (targetPlayer == null) {ntttcurrentState = FSMState.Wander;ntttagent.ResetPath ();ntttreturn;ntt}ntt//如果玩家與殭屍的距離,小於殭屍的攻擊距離,那麼進入攻擊狀態nttif (Vector3.Distance(targetPlayer.position, zombieTransform.position)<=attackRange) {ntttcurrentState = FSMState.Attack;ntttagent.ResetPath ();ntttreturn;ntt}nntt//設置移動目標為玩家nttagent.SetDestination (targetPlayer.position);n }n

攻擊狀態下,殭屍會攻擊玩家。如果殭屍在攻擊狀態下丟失了玩家,那麼進入隨機遊盪狀態。如果玩家脫離了攻擊範圍但是沒有脫離殭屍的感知範圍,那麼會進入追蹤狀態。

攻擊狀態處理函數(UpdateAttackState)

void UpdateAttackState()nt{ntt//如果殭屍感知範圍內沒有玩家,進入遊盪狀態ntttargetPlayer = zombieSensor.getNearbyPlayer ();nttif (targetPlayer == null) {ntttcurrentState = FSMState.Wander;ntttagent.ResetPath ();ntttanimator.SetBool ("isAttack", false);ntttreturn;ntt}ntt//如果玩家與殭屍的距離,大於殭屍的攻擊距離,那麼進入追蹤狀態nttif (Vector3.Distance(targetPlayer.position, zombieTransform.position)>attackRange) {ntttcurrentState = FSMState.Chase;ntttagent.ResetPath ();ntttanimator.SetBool ("isAttack", false);ntttreturn;ntt}n

//計算殭屍的正前方和玩家的夾角,只有玩家在殭屍前方才能攻擊ntttVector3 direction = targetPlayer.position - zombieTransform.position;ntttfloat degree = Vector3.Angle (direction, zombieTransform.forward);ntttif (degree < attackFieldOfView / 2 && degree > -attackFieldOfView / 2) {nttttanimator.SetBool ("isAttack", true);nttttif (attackTimer > attackInterval) {ntttttattackTimer = 0;ntttttif (zombieAttackAudio != null)ttttnttttttAudioSource.PlayClipAtPoint (zombieAttackAudio, zombieTransform.position);ntttttph.TakeDamage (attackDamage);ntttt}nttttattackTimer += Time.deltaTime;nttt} else {ntttt//如果玩家不在殭屍前方,殭屍需要轉向後才能攻擊nttttanimator.SetBool ("isAttack", false);nttttzombieTransform.LookAt(targetPlayer);nttt}n

當殭屍處於搜索,追蹤,攻擊,隨機遊盪等狀態時,如果生命值小於0的時候,會進入死亡狀態。判定代碼位於 FSMUpdate 函數中。

//如果殭屍處於非死亡狀態,但是生命值減為0,那麼進入死亡狀態nttif (currentState != FSMState.Dead && !zombieHealth.IsAlive) ntt{ntttcurrentState = FSMState.Dead;ntt}n

死亡狀態處理函數(UpdateDeadState)

void UpdateDeadState()nt{nttntt//如果殭屍初次進入死亡狀態,那麼需要禁用殭屍的一些組件(導航代理組件和碰撞體組件)nttif (firstInDead) {ntttfirstInDead = false;nntttagent.ResetPath ();ntttagent.enabled = false;ntttGetComponent<CapsuleCollider> ().enabled = false;n //播放死亡動畫ntttanimator.applyRootMotion = true;ntttanimator.SetTrigger ("toDie");n //開始統計死亡時間ntttdisappearTimer = 0;ntttdisappeared = false;n

//統計殭屍死亡後經過的時間,如果超過了屍體消失時間,那麼禁用該殭屍對象nttif (!disappeared) {nntttif ( disappearTimer > disappearTime) {nttttzombieTransform.gameObject.SetActive (false);nttttdisappeared = true;nttt}n //消失計時器不斷統計時間ntttdisappearTimer += Time.deltaTime;ntt}n

推薦閱讀:

從零開始手敲次世代遊戲引擎(三十二)
從零開始手敲次世代遊戲引擎(七)
在奇葩的 Json 世界裡冒險! - 《沒有勇者的世界》開發日誌 (2)
從零開始手敲次世代遊戲引擎(三十六)
從零開始手敲次世代遊戲引擎(二十六)

TAG:Unity游戏引擎 | 游戏开发 | 游戏 |