一个通用的有限状态机(FSM)框架

吃饭,睡觉,打豆豆

现在要实现一个游戏中的一个NPC的AI, 他只做三件事,吃饭,睡觉,打豆豆,最直接,最简答想到的代码应该是这样。

void Update()
{
	if(Hungry)
	{
		Eat();
		return;
	}

	if(Sleepy)
	{
		Sleep();
		return;
	}

	if(Bored)
	{
		KickDD();
		return;
	}
	
}



这样的代码是Work,但是,当NPC的状态和行为不断复杂化的时候,慢慢的,你就会添加各种条件变量,慢慢的,你就会用上了各种switch case,慢慢的,判断层级越来越多,慢慢的....


这个时候你就需要一个状态机了。


FSM简介

FSM定义:

一个有限状态机是一个设备,或者是一个设备模型,具有有限数量的状态,它可以在任何给定的时间根据输入进行操作,使得一个状态变换到另一个状态,或者是使一个输入或者一种行为的发生。一个有限状态机在任何瞬间只能处在一种状态。

它的优点:

1.编程快速简单,2.易于调试,3.很少的计算开销,4.直觉性,5.灵活性。



简单的框架



主要的两个类,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);

    }
}

这个类没必要声明称Monobehavior,只需要作为Character的一个成员来处理就可以了。运行的效果是这样的。



参考

Unity 4.x Game AI Programming

Game programming pattern - State

猜你喜欢

转载自blog.csdn.net/qp120291570/article/details/51155805