本系列为Unity中文课堂RPG战斗系统PLUS的课程笔记与解读。

战斗系统功能概要

  1. 多段普攻
  2. 跳跃、跳劈
  3. 格挡、格挡反击(弹反)
  4. 闪避
  5. 近战技能
  6. 其他高级功能

游戏中的状态机(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

}
  1. StateMachine主要用于控制状态的切换,其拥有当前所有已切换过的状态的缓存字典。
  2. 初始化时要指定当前StateMachine的宿主,并可以通过传T设置初始状态。
  3. 切换状态时,通过reCurrstate指定是否需要刷新重复状态,先退出当前状态,移除MonoManger中的生命周期函数监听,再进入新状态,此时先获取一个状态,在更新当前状态的记录,进入新状态,并添加MonoManager中的生命周期函数监听。
  4. 获取状态时,首先看状态缓存字典中是否存在,如果不存在,则new一个,并调用初始化函数,将当前新状态对象加入缓存字典中并返回新状态。
  5. 当状态机停止工作时,退出当前状态,状态记录置空,并执行当前状态缓存字典中的所有反初始化方法。

RPG战斗系统的状态机是框架状态机的简化版本,不包括状态机本身通过对象池的回收利用,以及共享状态数据,框架的状态机向下兼容,可以直接导入使用。