使用ScriptableObject基於有限狀態機AI與自由拓展
參考視頻:https://v.qq.com/x/page/c0537cidpg9.html
目標:
- 創建基於FSM(Finite State Machine有限狀態機)的遊戲AI。
- 實現狀態,動作,決策以及轉換。
- 使用ScriptableObject實現AI配置。
有限狀態機:
- 一臺有有限個狀態的機器。
- 狀態機因爲外部輸入或者環境改變(持續檢測)而轉換或者保持狀態。
- 每個狀態包括一個或多個動作,和一個規定的轉換決策。
首先定義一個StateController,每個AI有且只有一個。其中的重要部分是當前狀態和保持狀態。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class StateController : MonoBehaviour
{
public State currentState;
public State remainState;
public EnemyStats enemyStats;
public Transform eyes;
public float HP = 100f;
[HideInInspector] public NavMeshAgent navMeshAgent;
//[HideInInspector] public Complete.TankShooting tankShooting;
public List<Transform> wayPointList;
[HideInInspector] public int nextWayPoint;
[HideInInspector] public Transform chaseTarget;
[HideInInspector] public float stateTimeElapsed;
private bool aiActive = true;
private bool isDead = false;
void Awake()
{
navMeshAgent = GetComponent<NavMeshAgent>();
}
void Start()
{
}
public void SetupAI(bool aiActivationFromTankManager, List<Transform> wayPointsFromTankManager)
{
wayPointList = wayPointsFromTankManager;
aiActive = aiActivationFromTankManager;
if (aiActive)
{
navMeshAgent.enabled = true;
}
else
{
navMeshAgent.enabled = false;
}
}
private void Update()
{
if (!aiActive)
{
return;
}
currentState.UpdateState(this);
}
private void OnDrawGizmos()
{
Gizmos.color = currentState.sceneGizmoColor;
Gizmos.DrawWireSphere(eyes.position, enemyStats.lookSphereCastRadius);
}
public void TransitionToState(State nextState)
{
if (nextState != remainState)
{
currentState = nextState;
OnExitState();
}
}
public bool CheckIfCountDownElapsed(float duration)
{
stateTimeElapsed += Time.deltaTime;
return stateTimeElapsed >= duration;
}
public void OnExitState()
{
stateTimeElapsed = 0;
}
public void SetTarget(Transform t)
{
chaseTarget = t;
}
public void Damage()
{
HP -= 10;
}
public void AIDead()
{
if (isDead)
return;
isDead = true;
aiActive = false;
this.SendMessage("Dead1");
this.SendMessageUpwards("OneMoreEnemyDead");
this.GetComponent<Collider>().enabled = false;
}
}
然後是定義狀態的部分State,其中包含一個每幀執行的UpdateState,運行兩個函數:
1執行當前N個Action動作
2執行N個Decision轉換檢測
using UnityEngine;
[CreateAssetMenu(menuName = "PluggableAI/State")]
public sealed class State : ScriptableObject
{
public Action[] action;
public Transition[] transitions;
public Color sceneGizmoColor = Color.grey;
public void UpdateState(StateController controller)
{
DoActions(controller);
CheckTransitions(controller);
}
private void DoActions(StateController controller)
{
for (int i = 0; i < action.Length; i++)
{
action[i].Act(controller);
}
}
private void CheckTransitions(StateController controller)
{
for (int i = 0; i < transitions.Length; i++)
{
bool decisionSucceeded = transitions[i].decision.Decide(controller);
if (decisionSucceeded)
controller.TransitionToState(transitions[i].trueState);
else
controller.TransitionToState(transitions[i].falseState);
}
}
}
定義Action和Decision
using UnityEngine;
public abstract class Action : ScriptableObject {
public abstract void Act(StateController controller);
}
using UnityEngine;
[CreateAssetMenu(menuName = "PluggableAI/Actions/Chase")]
public class ChaseAction : Action
{
private float chaseStopDis = 5f;
private float QuaterSlerpParam= 0.1f;
public override void Act(StateController controller)
{
Chase(controller);
}
public void Chase(StateController controller)
{
if(controller.chaseTarget == null)return;
// if (Vector3.Distance(controller.transform.position, controller.chaseTarget.position) >= chaseStopDis)
// {
// controller.navMeshAgent.destination = controller.chaseTarget.position;
// controller.navMeshAgent.isStopped = false;
// }
if (controller.navMeshAgent.remainingDistance <= chaseStopDis)
{
if (Vector3.Distance(controller.transform.position, controller.chaseTarget.position) <= chaseStopDis)
{
UnityEngine.AI.NavMeshHit hit;
if (!controller.navMeshAgent.Raycast(controller.chaseTarget.position, out hit))
{
Debug.DrawLine(controller.transform.position, controller.chaseTarget.position, Color.white);
// Target is "visible" from agent position.
controller.navMeshAgent.isStopped = true;
// 目標Rotation
Quaternion t = Quaternion.LookRotation((controller.chaseTarget.position - controller.transform.position), Vector3.up);
// 進行旋轉
controller.transform.rotation = Quaternion.Slerp(controller.transform.rotation, t, QuaterSlerpParam);
return;
}
}
}
Debug.DrawLine(controller.transform.position, controller.chaseTarget.position, Color.green);
controller.navMeshAgent.destination = controller.chaseTarget.position;
controller.navMeshAgent.isStopped = false;
}
}
using UnityEngine;
public abstract class Decision : ScriptableObject
{
public abstract bool Decide(StateController controller);
}
using UnityEngine;
[CreateAssetMenu(menuName = "PluggableAI/Decision/LookDecision")]
public class LookDecision : Decision
{
public override bool Decide(StateController controller)
{
return Look(controller);
}
private bool Look(StateController controller)
{
RaycastHit hit;
Debug.DrawRay(controller.eyes.position,controller.eyes.forward*controller.enemyStats.lookRange,Color.green);
if(Physics.SphereCast(controller.eyes.position,controller.enemyStats.lookSphereCastRadius,
controller.eyes.forward,out hit,controller.enemyStats.lookRange)
&&hit.collider.CompareTag("Player"))
{
controller.chaseTarget = hit.transform;
return true;
}
return false;
}
}
這裏稍微解釋一下,首先AI一上來沒有動作,執行一個觀察檢測,通過射線檢測獲得玩家目標,一旦獲得玩家目標,就會進入到追蹤玩家的狀態。
這裏設計三種不同的AI
巡邏車AI
初始狀態按照規定路線巡邏,發現玩家後持續追蹤和攻擊,玩家離開一定範圍後回到原始位置。
飛行器AI
初始狀態隨機路線巡邏,發現玩家後持續追蹤。
炮塔AI
初始在射程範圍內檢測玩家,檢測到之後進行攻擊,玩家離開範圍後停止攻擊。