本系列为Unity中文课堂RPG战斗系统PLUS的课程笔记与解读。
战斗系统功能概要
- 多段普攻
- 跳跃、跳劈
- 格挡、格挡反击(弹反)
- 闪避
- 近战技能
- 其他高级功能
游戏中的状态机(state machine)是一种在编程中常用的概念,它用于表示对象或系统的状态以及从一个状态到另一个状态的转换。在游戏中,状态机通常被用于表示游戏对象的状态,例如玩家角色的行动状态,或者敌人的攻击状态。每个状态都有一个特定的行为或属性,而状态之间的转换通常是由特定的事件触发的,例如按下某个按钮或达到某个条件。框架中提供了状态基类和转换功能逻辑。
不继承MonoBehaviour的Update和协程
这一节实际上就是框架的MonoSystem+拓展方法,学过框架可以略过。
在正式开始写状态机之前,由于状态类本身不继承MonoBehaviour也不挂在面板上,无法使用Unity生命周期函数和协程。为了实现不继承MonoBehavior的脚本使用Unity生命周期函数和协程,需要使用代理模式的设计模式(宿主,托管式执行本质都是代理模式)。
代理模式: 为其他对象提供一种代理以控制这个对象的访问。在状态机的应用场景下,代理本身继承自MonoBehavior,通过在代理的生命周期函数中调用状态类的Update和协程完成目的。
public class SingletonMono<T> : MonoBehaviour where T : SingletonMono<T>
{
public static T instance;
protected virtual void Awake()
{
if (instance == null)
{
instance = (T)this;
}
}
}
先写个Mono泛型单例,原理不赘述,使用饿汉式。
public class MonoManager : SingletonMono<MonoManager>
{
private Action updateAction;
private Action lateUpdateAction;
private Action fixedUpdateAction;
public void AddUpdateListener(Action action)
{
updateAction += action;
}
public void RemoveUpdateListener(Action action)
{
updateAction -= action;
}
public void AddFixedUpdateListener(Action action)
{
fixedUpdateAction += action;
}
public void RemoveFixedUpdateListener(Action action)
{
fixedUpdateAction -= action;
}
public void AddLateUpdateListener(Action action)
{
lateUpdateAction += action;
}
public void RemoveLateUpdateListener(Action action)
{
lateUpdateAction -= action;
}
private void Update()
{
updateAction.Invoke();
}
private void FixedUpdate()
{
fixedUpdateAction.Invoke();
}
private void LateUpdate()
{
lateUpdateAction.Invoke();
}
}
MonoManager通过在类内调用其他类(状态类)的生命周期函数逻辑实现了不继承mono脚本启用Update和协程,纯粹的代理模式会直接持有状态类对象,来访问其方法,进一步结合事件(本质是观察者模式)将状态类对象和代理对象解耦。
使用时在状态类内调用MonoManager对应的事件监听添加函数即可,协程同理,MonoManager本身要挂载在面板上,具体使用方式见使用手册。
有限状态机
我们将角色的待机、移动、攻击、跳跃等等都视为状态,为此制作一个有限状态机组件,后续无论是Al还是玩家脚本都可以持有然后达到一个状态切换的目的。这一部分学过框架也可以略过。
状态类基类
public abstract class StateBase
{
/// <summary>
/// 初始化状态
/// 只在状态第一次创建时执行
/// </summary>
/// <param name="owner">宿主</param>
public virtual void Init(IStateMachineOwner owner){}
/// <summary>
/// 反初始化
/// 把一些引用置空,防止不能被GC
/// </summary>
public virtual void UnInit(){}
/// <summary>
/// 状态进入
/// 每次进入都会执行
/// </summary>
public virtual void Enter() { }
/// <summary>
/// 状态退出
/// </summary>
public virtual void Exit() { }
public virtual void Update() { }
public virtual void LateUpdate() { }
public virtual void FixedUpdate() { }
}
状态类基类本身是抽象方法不可以实现,是对所有状态类都有的功能的抽象,包含初始化,反初始化,进入,退出,以及相应的生命周期函数并会通过MonoManager调用,其中,owner是使用状态机的类(宿主),通过初始化的方式传递引用便于在状态类中访问宿主。
状态机类
public interface IStateMachineOwner { }
首先定义一个接口被所有宿主继承,因为此时并不知道宿主具体是什么类,用的时候强转下就行。
/// <summary>
/// 状态机控制器
/// </summary>
public class StateMachine
{
// 当前状态
public Type CurrStateType { get; private set; } = null;
// 当前生效中的状态
private StateBase currStateObj;
// 宿主
private IStateMachineOwner owner;
// 所有的状态 Key:状态枚举的值 Value:具体的状态
private Dictionary<Type, StateBase> stateDic = new Dictionary<Type, StateBase>();
/// <summary>
/// 初始化
/// </summary>
/// <param name="owner">宿主</param>
/// <typeparam name="T">初始状态类型</typeparam>
public void Init<T>(IStateMachineOwner owner) where T : StateBase, new()
{
this.owner = owner;
ChangeState<T>();
}
/// <summary>
/// 初始化(无默认状态,状态机待机)
/// </summary>
/// <param name="owner">宿主</param>
public void Init(IStateMachineOwner owner)
{
this.owner = owner;
}
#region 状态
/// <summary>
/// 切换状态
/// </summary>
/// <typeparam name="T">具体要切换到的状态脚本类型</typeparam>
/// <param name="newState">新状态</param>
/// <param name="reCurrstate">新状态和当前状态一致的情况下,是否也要切换</param>
/// <returns></returns>
public bool ChangeState<T>(bool reCurrstate = false) where T : StateBase, new()
{
Type stateType = typeof(T);
// 状态一致,并且不需要刷新状态,则切换失败
if (stateType == CurrStateType && !reCurrstate) return false;
// 退出当前状态
if (currStateObj != null)
{
currStateObj.Exit();
MonoManager.instance.RemoveUpdateListener(currStateObj.Update);
MonoManager.instance.RemoveLateUpdateListener(currStateObj.LateUpdate);
MonoManager.instance.RemoveFixedUpdateListener(currStateObj.FixedUpdate);
}
// 进入新状态
currStateObj = GetState<T>();
CurrStateType = stateType;
currStateObj.Enter();
MonoManager.instance.AddUpdateListener(currStateObj.Update);
MonoManager.instance.AddLateUpdateListener(currStateObj.LateUpdate);
MonoManager.instance.AddFixedUpdateListener(currStateObj.FixedUpdate);
return true;
}
/// <summary>
/// 从对象池获取一个状态
/// </summary>
private StateBase GetState<T>() where T : StateBase, new()
{
Type stateType = typeof(T);
if (stateDic.ContainsKey(stateType)) return stateDic[stateType];
StateBase state = new T();
state.Init(owner);
stateDic.Add(stateType, state);
return state;
}
/// <summary>
/// 停止工作
/// 把所有状态都释放,但是StateMachine未来还可以工作
/// </summary>
public void Stop()
{
// 处理当前状态的额外逻辑
currStateObj.Exit();
CurrStateType = null;
currStateObj = null;
// 处理缓存中所有状态的逻辑
var enumerator = stateDic.GetEnumerator();
while (enumerator.MoveNext())
{
enumerator.Current.Value.UnInit();
}
stateDic.Clear();
}
#endregion
}
- StateMachine主要用于控制状态的切换,其拥有当前所有已切换过的状态的缓存字典。
- 初始化时要指定当前StateMachine的宿主,并可以通过传T设置初始状态。
- 切换状态时,通过reCurrstate指定是否需要刷新重复状态,先退出当前状态,移除MonoManger中的生命周期函数监听,再进入新状态,此时先获取一个状态,在更新当前状态的记录,进入新状态,并添加MonoManager中的生命周期函数监听。
- 获取状态时,首先看状态缓存字典中是否存在,如果不存在,则new一个,并调用初始化函数,将当前新状态对象加入缓存字典中并返回新状态。
- 当状态机停止工作时,退出当前状态,状态记录置空,并执行当前状态缓存字典中的所有反初始化方法。
RPG战斗系统的状态机是框架状态机的简化版本,不包括状态机本身通过对象池的回收利用,以及共享状态数据,框架的状态机向下兼容,可以直接导入使用。