状态驱动在游戏中应用的很多,有应用于UI的管理,角色的状态,流程控制等。 状态机的优势主要有:
编程快速简单,状态机的实现有多种但普遍简单。
易于调试。 对于一个 智能体行为,分成几个易于管理的模块,如果一个智能体行为变得怪异可以通过对每一个状态进行跟踪来调试。
很少的计算开销,它本质上遵循硬件的编码规则
直觉性,对于状态性的行为更容易符合我们的思维以创建规则来控制他们
创建一个可复用的状态机:
using UnityEngine; using System.Collections; public abstract class State : MonoBehaviour { public virtual void Enter() { AddListener(); } public virtual void Exit() { RemoveListener(); } protected virtual void OnDestroy() { RemoveListener(); } protected virtual void AddListener(){ } protected virtual void RemoveListener() {} }
将它定义为abstract 是为了告述其他程序员要创建具体的子类来使用它,StateMachine会决定哪个是当前的状态,并且调用Exit 和 Enter后是哪个状态。
由于使用了mono行为,在Ondestroy中移除监听是很方便的,不用考虑由于应用占用而无法移除的问题。
AddListener 和 RemoveListener 将它们设为abstract 是可以的,因为a他们没有具体的实现。但是这样子类必须要从写它,不论是否需要,所以设为虚方法更好。
编写状态机类 它负责 状态间的切换,也提供一些必要的函数:
using UnityEngine; using System.Collections; public class StateMachine : MonoBehaviour { public virtual State CurrentState { get { return m_sCurrentState; } set { Transition(value); } } protected State m_sCurrentState; protected bool m_bInTransition = false; //采用mono行为 那就意味着要有挂载脚本的gameobj public virtual T GetState<T>() where T:State { T target = GetComponent<T>(); if(target == null) { target = gameObject.AddComponent<T>(); } return target; } public virtual void ChangeState<T>() where T:State { CurrentState = GetState<T>(); } protected void Transition(State target) { //等于当前的状态或正在转换 是不切换的 if (m_sCurrentState ==target || m_bInTransition) return; m_bInTransition = true; if (m_sCurrentState != null) m_sCurrentState.Exit(); m_sCurrentState = target; if (m_sCurrentState != null) m_sCurrentState.Enter(); m_bInTransition = false; } }
这里状态机结合了mono行为,对于状态的缓存是挂载在GameObject上的。 不管是使用mono行为还是不使用(使用集合对状态缓存) ,要注意的有两点,一个状态切换过程中要注意的判断 , 一个是状态的缓存。
这个脚本也相当简单。它拿到一个State 实例作为当前状态。该实例是一个叫做 _currentState的保护字段,并且可以通过属性CurrentState来访问。该属性支持赋值和获取两种操作,赋值操作调用Transition 方法有一些其它的逻辑:
- 如果设置当前状态传入的值已经是当前状态了,就直接返回(而不是退出当前状态再重新进入)。
- 你不能在transition过程中设置状态(例如,不能存在那种调用Exit或Enter方法后导致其它状态变为当前状态的状态)。我选择避免这种问题以防我想在状态改变时抛出事件。否则,在切换状态之前transition过程还未完成这种情况下,会看到抛出很多的事件都把最新创建的事件作为当前事件(而不是每个状态一个事件),这样会导致意想不到的bug。
- 标记transition开始。。。
- 如果之前状态不为空,通知其退出。
- 在setter中将保护字段 _currentState设为传入的状态。
- 如果新状态不为空,通知其调用Enter。
- 标记transition结束。
我还添加了几个简便的方法。我想以很简单的方式通知StateMachineqie换状态,根据当前状态的类型调用泛型方法。这样一来,就不用对将切换到的状态实例的引用进行硬编码了。我是通过ChangeState 方法来实现的,约束条件是其泛型参数类型只能是State。在方法内部调用了另外一个泛型方法GetState ,其参数就是泛型类型。GetState方法尝试通过Unity的GetComponent 方法来获取状态,如果获取不到,就调用AddComponent添加。