现在要实现一个游戏中的一个NPC的AI, 他只做三件事,吃饭,睡觉,打豆豆,最直接,最简答想到的代码应该是这样。
void Update() { if(Hungry) { Eat(); return; } if(Sleepy) { Sleep(); return; } if(Bored) { KickDD(); return; } }
这样的代码是Work,但是,当NPC的状态和行为不断复杂化的时候,慢慢的,你就会添加各种条件变量,慢慢的,你就会用上了各种switch case,慢慢的,判断层级越来越多,慢慢的....
这个时候你就需要一个状态机了。
主要的两个类,FSMState表示状态,FSMSystem里面维护了一个状态的列表,最后需要一个StateController作为状态的控制器。
代码清单
FSMState.cs
using UnityEngine; using System; using System.Collections.Generic; using System.Collections; public enum Transition { NullTransition = 0, // Use this transition to represent a non-existing transition in your system SawPlayer, LostPlayer, NoHealth, ReadytoAim, ReadytoShot, ReadytoIdle, ReadytoAttack, ReadytoChasing } public enum StateID { NullStateID = 0, // Use this ID to represent a non-existing State in your system Idle, Chasing, // jump Attack, Shooting, Aiming, BacktoIdle,//jump Dead, } public abstract class FSMState{ protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>(); protected StateID stateID; public StateID ID { get { return stateID; } } public void AddTransition(Transition trans, StateID id) { // Check if anyone of the args is invalid if (trans == Transition.NullTransition) { Debug.LogError("FSMState ERROR: NullTransition is not allowed for a real transition"); return; } if (id == StateID.NullStateID) { Debug.LogError("FSMState ERROR: NullStateID is not allowed for a real ID"); return; } // Since this is a Deterministic FSM, // check if the current transition was already inside the map if (map.ContainsKey(trans)) { Debug.LogError("FSMState ERROR: State " + stateID.ToString() + " already has transition " + trans.ToString() + "Impossible to assign to another state"); return; } map.Add(trans, id); } /// <summary> /// This method deletes a pair transition-state from this state's map. /// If the transition was not inside the state's map, an ERROR message is printed. /// </summary> public void DeleteTransition(Transition trans) { // Check for NullTransition if (trans == Transition.NullTransition) { Debug.LogError("FSMState ERROR: NullTransition is not allowed"); return; } // Check if the pair is inside the map before deleting if (map.ContainsKey(trans)) { map.Remove(trans); return; } Debug.LogError("FSMState ERROR: Transition " + trans.ToString() + " passed to " + stateID.ToString() + " was not on the state's transition list"); } /// <summary> /// This method returns the new state the FSM should be if /// this state receives a transition and /// </summary> public StateID GetOutputState(Transition trans) { // Check if the map has this transition if (map.ContainsKey(trans)) { return map[trans]; } return StateID.NullStateID; } /// <summary> /// This method is used to set up the State condition before entering it. /// It is called automatically by the FSMSystem class before assigning it /// to the current state. /// </summary> public virtual void DoBeforeEntering() { } /// <summary> /// This method is used to make anything necessary, as reseting variables /// before the FSMSystem changes to another one. It is called automatically /// by the FSMSystem before changing to a new state. /// </summary> public virtual void DoBeforeLeaving() { } /// <summary> /// This method decides if the state should transition to another on its list /// NPC is a reference to the object that is controlled by this class /// </summary> public abstract void Reason(GameObject player, GameObject npc); /// <summary> /// This method controls the behavior of the NPC in the game World. /// Every action, movement or communication the NPC does should be placed here /// NPC is a reference to the object that is controlled by this class /// </summary> public abstract void Act(GameObject player, GameObject npc); }
FSMSystem.cs
using UnityEngine; using System.Collections; using System.Collections.Generic; public class FSMSystem{ private List<FSMState> states; // The only way one can change the state of the FSM is by performing a transition // Don't change the CurrentState directly private StateID currentStateID; public StateID CurrentStateID { get { return currentStateID; } } private FSMState currentState; public FSMState CurrentState { get { return currentState; } } public StateID defaultState {set{defaultState = value;} get {return defaultState;}} public void resetToDefaultState() { currentState = states[0]; currentStateID = states[0].ID; /*for(int i =0; i< states.Count; i++) { if(states[i].ID == defaultState) { currentState = states[i]; currentStateID = states[i].ID; } }*/ } public FSMSystem() { states = new List<FSMState>(); } /// <summary> /// This method places new states inside the FSM, /// or prints an ERROR message if the state was already inside the List. /// First state added is also the initial state. /// </summary> public void AddState(FSMState s) { // Check for Null reference before deleting if (s == null) { Debug.LogError("FSM ERROR: Null reference is not allowed"); } // First State inserted is also the Initial state, // the state the machine is in when the simulation begins if (states.Count == 0) { states.Add(s); currentState = s; currentStateID = s.ID; return; } // Add the state to the List if it's not inside it foreach (FSMState state in states) { if (state.ID == s.ID) { Debug.LogError("FSM ERROR: Impossible to add state " + s.ID.ToString() + " because state has already been added"); return; } } states.Add(s); } /// <summary> /// This method delete a state from the FSM List if it exists, /// or prints an ERROR message if the state was not on the List. /// </summary> public void DeleteState(StateID id) { // Check for NullState before deleting if (id == StateID.NullStateID) { Debug.LogError("FSM ERROR: NullStateID is not allowed for a real state"); return; } // Search the List and delete the state if it's inside it foreach (FSMState state in states) { if (state.ID == id) { states.Remove(state); return; } } Debug.LogError("FSM ERROR: Impossible to delete state " + id.ToString() + ". It was not on the list of states"); } /// <summary> /// This method tries to change the state the FSM is in based on /// the current state and the transition passed. If current state /// doesn't have a target state for the transition passed, /// an ERROR message is printed. /// </summary> public void PerformTransition(Transition trans) { // Check for NullTransition before changing the current state if (trans == Transition.NullTransition) { Debug.LogError("FSM ERROR: NullTransition is not allowed for a real transition"); return; } // Check if the currentState has the transition passed as argument StateID id = currentState.GetOutputState(trans); if (id == StateID.NullStateID) { Debug.LogError("FSM ERROR: State " + currentStateID.ToString() + " does not have a target state " + " for transition " + trans.ToString()); return; } // Update the currentStateID and currentState currentStateID = id; foreach (FSMState state in states) { if (state.ID == currentStateID) { // Do the post processing of the state before setting the new one currentState.DoBeforeLeaving(); currentState = state; // Reset the state to its desired condition before it can reason or act currentState.DoBeforeEntering(); break; } } } // PerformTransition() }
角色的控制器通常也是通过状态机来实现。
首先要定义出角色的各种状态已经状态间的转换条件,就像这样:
接下来就是用代码定义各种状态的执行逻辑,跳转条件等。有些复杂的游戏还有通过分层的概念来处理角色的。
下面是最简单的两个状态,Idle和Move。
IdleState.cs
using UnityEngine; using System.Collections; namespace CharacterFSM { public class IdleState : CharacterState { float horizontalMove; float verticalMove; public IdleState(Character _host) { host = _host; stateID = CharacterStateID.Idle; } public override void HandleInput(MovementInput movementInput) { horizontalMove = movementInput.moveStrafe; verticalMove = movementInput.moveForward; } public override void Act() { } public override void Reason() { if (horizontalMove * horizontalMove + verticalMove * verticalMove < 0.1f) { return; } else { host.stateController.SetTransition(CharacterStateTransition.ToMove); } } public override void DoBeforeEntering() { host.animator.SetBool("Static_b", true); host.animator.SetFloat("Speed_f", 0); } } }
MoveState.cs
using UnityEngine; using System.Collections; namespace CharacterFSM { public class MoveState : CharacterState { float stepDelta; float stepMark; public MoveState(Character _host) { stepMark = -1f; stepDelta = 0.3f; host = _host; stateID = CharacterStateID.Move; } public override void HandleInput(MovementInput movementInput) { float maxSpeed = host.MaxSpeed * Mathf.Sqrt(movementInput.moveStrafe * movementInput.moveStrafe + movementInput.moveForward * movementInput.moveForward); host.CurrentSpeed -= 2 * host.Acceleration * Time.deltaTime; host.CurrentSpeed = Mathf.Max(maxSpeed, host.CurrentSpeed); Vector2 tmp = new Vector2(movementInput.moveStrafe, movementInput.moveForward).normalized * host.CurrentSpeed; host.CurrentVelocity = new Vector3(tmp.x, 0, tmp.y); host.animationController.SetSpeed(host.CurrentSpeed); } public override void Act() { } public override void Reason() { if(host.CurrentSpeed < 0.01f ) { host.stateController.SetTransition(CharacterStateTransition.ToIdle); } } public override void DoBeforeLeaving() { } public override void DoBeforeEntering() { host.animationController.PerformMove(); } } }
还有一个比较重要的类,CharacterStateController
using UnityEngine; using System.Collections; using CharacterFSM; public class CharacterStateController { CharacterFSMSystem characterFsm; Character character; public CharacterStateController(Character _character) { character = _character; } public CharacterStateID GetCurrentStateID() { return characterFsm.CurrentStateID; } public void Init() { ConstructFSM(); } // Update is called once per frame public void Update () { Debug.Log(GetCurrentStateID()); //Debug.Log(character.movementInput.moveForward + " " +character.movementInput.moveStrafe); characterFsm.CurrentState.HandleInput(character.movementInput); characterFsm.CurrentState.Reason(); characterFsm.CurrentState.Act(); } public void SetTransition(CharacterStateTransition t) { if (characterFsm != null) { characterFsm.PerformTransition(t); } } void ConstructFSM() { IdleState idleState = new IdleState(character); idleState.AddTransition(CharacterStateTransition.ToMove, CharacterStateID.Move); MoveState moveState = new MoveState(character); moveState.AddTransition(CharacterStateTransition.ToIdle, CharacterStateID.Idle); characterFsm = new CharacterFSMSystem(); characterFsm.AddState(idleState); characterFsm.AddState(moveState); } }
Unity 4.x Game AI Programming
Game programming pattern - State