1、West World实例
场景:在一个西部小镇有一个居民Bob,他的职业是矿工,小镇上有四个标志物,金矿、酒吧、银行、家,他疲劳时要回家睡觉,口渴时会去酒吧喝酒,金子到达一定数目会去银行存储金子。他准确地向哪走,到达后要干什么,都由Bob当前的状态决定。
金矿工人的状态图:
居民基类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BaseGameEntity
{
//每个实体具有一个唯一的识别数字
private int m_ID;
//这是下一个有效的ID。每次BaseGameEntity被实例化,这个值就被更新
private static int m_iNextValidID;
//用于测试ID是否被设置正确
void SetID(int val)
{
}
public BaseGameEntity(int id)
{
SetID(id);
}
//所有的实体必须执行一个更新函数
public virtual void Update()
{
}
}
矿工类(这里指的其实就是Bob)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Miner : BaseGameEntity
{
//当前状态
private BobState curState;
//当前位置
private Vector3 localPos;
//金块个数
private int goldCarried;
//银行存储的钱数量
private int moneyInBank;
//口渴程度
private int thirst;
//疲惫程度
private int fatigue;
public Miner(int id):base(id)
{
}
/// <summary>
/// 通过当前curState的值执行当前状态的更新函数
/// </summary>
public override void Update()
{
thirst += 1;
if(curState != null)
{
curState.Execute(this);
}
}
/// <summary>
/// 切换状态
/// </summary>
/// <param name="newState"></param>
public void ChangeState(BobState newState)
{
if(curState != null && newState != null)
{
curState.Exit(this);
curState = newState;
curState.Enter(this);
}
}
}
状态的基类(之后的新增状态都要继承自这个类)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//这个类在基础的状态基类上添加了进入状态和退出状态的函数
public class BobState : MonoBehaviour
{
/// <summary>
/// 状态进入时执行
/// </summary>
/// <param name="miner"></param>
public virtual void Enter(Miner miner)
{
}
/// <summary>
/// 每一更新步骤会被矿工调用这个函数
/// </summary>
/// <param name="miner"></param>
public virtual void Execute(Miner miner)
{
}
/// <summary>
/// 退出状态时执行
/// </summary>
/// <param name="miner"></param>
public virtual void Exit(Miner miner)
{
}
}
下面是UML图
每个具体的状态可以被简化为一个单例(Singleton)对象。这是为了确保每一个状态只有一个实例,这样消除了在状态每次改变时分配和释放内存的需要
但这样做有一个缺点:使用状态的每个智能体的属性都是不同的,比如说位置,那么这些可能会有差异的属性就不能存储在状态中,状态要通过智能体才能够访问这些属性。如果这样的属性数量非常的多,那在状态获取属性的过程中或耗费很多的时间和内存。
2、使state基类可重用
使用泛型可以使State基类被重用
如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//这个类在基础的状态基类上添加了进入状态和退出状态的函数
public class BobState<T> : MonoBehaviour
{
/// <summary>
/// 状态进入时执行
/// </summary>
/// <param name="t"></param>
public virtual void Enter(T t)
{
}
/// <summary>
/// 每一更新步骤会被矿工调用这个函数
/// </summary>
/// <param name="t"></param>
public virtual void Execute(T t)
{
}
/// <summary>
/// 退出状态时执行
/// </summary>
/// <param name="t"></param>
public virtual void Exit(T t)
{
}
}
若想表达对一个具体的类进行派生,可以写成下面的形式:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//这个类在基础的状态基类上添加了进入状态和退出状态的函数
public class EnterMinerAndDigForNugget : BobState<Miner>
{
//省略对Enter、Execute、Exit的重写
}
3、全局状态和状态翻转
在《模拟人生》里面,主角可能在任何时间,任何状态下,产生向上厕所状态转换的可能。这种时候你可能需要将判断要上厕所的条件和切换到上厕所状态的代码复制到每一个状态子类中。
基于上述问题,有两种解决方法
方法1、全局状态
声明一个额外的成员变量,这样每次FSM更新就会被调用
方法2、状态翻转
进入A状态前,记录下前一个状态B的记录,退出A状态时,返回状态B
4、创建一个StateMachine类
通过把所有与状态相关的数据和方法封装到一个StateMachine类中,可以使得设计更为简洁。这种方式下智能体只需要拥有一个StateMachine的实例即可委托他管理当前、全局、上一个状态。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMachine<T>
{
//智能体
private T owner;
//当前状态
private BobState<T> curState;
//上一个状态
private BobState<T> preState;
//全局状态,每当FSM更新时,这个状态逻辑会被调用
private BobState<T> globalState;
public StateMachine(T owner)
{
this.owner = owner;
this.curState = null;
this.preState = null;
this.globalState = null;
}
/// <summary>
/// 初始化当前状态
/// </summary>
/// <param name="state"></param>
public void SetCurrentState(BobState<T> state)
{
this.curState = state;
}
/// <summary>
/// 初始化全局状态
/// </summary>
/// <param name="state"></param>
public void SetGlobalState(BobState<T> state)
{
this.globalState = state;
}
/// <summary>
/// 初始化上一个状态
/// </summary>
/// <param name="state"></param>
public void SetPreviousState(BobState<T> state)
{
this.preState = state;
}
//调用这个函数来更新FSM
public void Update()
{
if (globalState != null) globalState.Execute(owner);
if (curState != null) curState.Execute(owner);
}
/// <summary>
/// 改变到新状态
/// </summary>
/// <param name="newState"></param>
public void ChangeState(BobState<T> newState)
{
if(newState != null && curState != null)
{
//保留上一个状态的记录
preState = curState;
//退出当前状态
curState.Exit(owner);
//更改当前状态
curState = newState;
//进入当前状态
curState.Enter(owner);
}
}
/// <summary>
/// 状态翻转
/// </summary>
public void RevertToPreviousState()
{
//回到上一个状态
ChangeState(preState);
}
/// <summary>
/// 获取当前状态
/// </summary>
/// <returns></returns>
public BobState<T> GetCurrentState()
{
return curState;
}
/// <summary>
/// 获取全局状态
/// </summary>
/// <returns></returns>
public BobState<T> GetGlobalState()
{
return globalState;
}
/// <summary>
/// 获得上一个状态
/// </summary>
/// <returns></returns>
public BobState<T> GetPreState()
{
return preState;
}
}
改进后的Miner类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Miner : BaseGameEntity
{
//当前位置
private Vector3 localPos;
//金块个数
private int goldCarried;
//银行存储的钱数量
private int moneyInBank;
//口渴程度
private int thirst;
//疲惫程度
private int fatigue;
//stateMachine的实例
private StateMachine<Miner> stateMachine;
public Miner(int id):base(id)
{
localPos = Vector3.zero;
goldCarried = 0;
moneyInBank = 0;
thirst = 0;
fatigue = 0;
//初始化stateMachine
stateMachine = new StateMachine<Miner>(this);
stateMachine.SetCurrentState(/*初始状态类的实例*/);
stateMachine.SetGlobalState(/*全局状态类的实例*/);
}
/// <summary>
/// 通过当前curState的值执行当前状态的更新函数
/// </summary>
public override void Update()
{
thirst += 1;
//更新stateMachine
stateMachine.Update();
}
//方便其他类获取stateMachine实例
public StateMachine<Miner> GetFSM()
{
return stateMachine;
}
}