[Unity] 状态机事件流程框架 (一)(C#事件系统,Trigger与Action)

        设计游戏时,框架设计时十分重要的,项目内容一大,各个功能系统间的耦合性就会不可避免的增加,如果初期时没有规划好,后期维护时成本就会大大增加。

        本期是在Unity实现一个简单的事件框架应用。通过预先在脚本中写好可供复用的具体模块,将事件的搭建流程搭建脱离代码,取而代之的是使用更方便的组合思想去构建事件。这可以减少脚本的编写量,也更好的去规范游戏的制作管线。

        在本期框架中,我们将游戏的各种游戏中的逻辑触发抽象成【触发器->执行行为序列】的流程,将触发器脚本作为父物品,各个行为作为该触发器的子物品。当触发器条件被满足时,顺序执行自身子物品下的所有事件节点,而作为触发器的本身的时机,大多使用观察者模式监听事件广播或者单向委托绑定的方式去注册事件。

        这套框架是今年打CUSGA和项目主程(帅神)学到的,自己从中总结了很多知识,自己也针对其中的思想做了优化。也欢迎来bilibili给游戏宣传片贡献播放量。

【CUSGA 2022】《藏星》温暖治愈的剧情解谜游戏

框架概要

(本文脚本中使用了odin插件)

我们先使用一个简单的例子作为引入

        可以看到,在我们点击了按钮后,一个游戏方块开始向右移动,在向右移动完成后,方块被摧毁并且debug了一段信息。是一段简单的游戏逻辑。如何让这段逻辑变得可复用和好维护,是我们今天框架的主要目标(比如我们写好了一个移动功能,那么下次再遇到类似的逻辑就可以拿出来直接使用,而不需要再创建一段代码)

我们从以上逻辑拆分以上模块。这段逻辑可以由以下四个部分组成。

玩家按下按钮(触发器Trigger)

->物品移动(事件Action)

->物品销毁(事件Action)

->Debug信息(事件Action)

        我们使用单个游戏物品,用来表示一个触发器或者一个事件。每个单独事件作为一个子物品,将放在一个触发器下,当触发器条件被满足时,事件将会被会被顺序执行。且trigger和Action可以再Inspector窗口中编辑,查看他们当前的状态。

 

上面举了一个例子。我们从这个例子中更进一步抽象出来。如果我们需要在游戏中执行某个逻辑(事件),一个流程如下:

收到触发该事件的指令->执行该事件内容->事件执行完成,执行下一步操作(回调)

我们定义几个基本概念:

Trigger:触发器-决定了事件什么时候被触发。比如玩家按下某一按键、某个游戏状态改变、另一个事件完成时会触发。我们需要根据需求,自定义各个类型的触发器,当条件被满足时,便执行该触发器所绑定的事件内容。

Action:事件执行内容。游戏中会有各种各样需要实现的逻辑,为方便游戏搭建和维护,我们需要将各个功能模块划分出来,并提供一个统一的接口给Trigger调用,支持各个不同类型事件的排列组合。

回调:当事件执行完成时,通知其他事务执行下一个操作(本篇文章中的回调操作为:找到下一个事件并执行 / 已经是最后一个事件了,通知触发器已经完成)

我们先为上面两个概念提供最基础的状态框架,这里使用了一个抽象类作为Trigger和Action的统一基类,通过 protected virtual 留下了几个经典的状态机函数(进入,执行,退出), executeType表示了当前的状态。

public abstract class BaseState : MonoBehaviour
    {
        [TextArea, LabelText("说明")]
        public string content;
        [LabelText("当前状态")]
        public ExecuteType executeType;

        public BaseState parentState;

        public enum ExecuteType
        {
            [LabelText("未执行")] None,
            [LabelText("准备执行")] Enter,
            [LabelText("正在执行")] Running,
            [LabelText("执行完成 待机")] RunOver,
            [LabelText("执行完成 退出")] Exit,
        }

        protected virtual void Awake()
        {
            executeType = ExecuteType.None;
        }

        protected virtual void OnEnter()
        {
            executeType = ExecuteType.Enter;
        }

        protected virtual void OnRunning()
        {
            executeType = ExecuteType.Running;
        }

        protected virtual void OnRunOver()
        {
            executeType = ExecuteType.RunOver;
        }

        protected virtual void OnExit()
        {
            executeType = ExecuteType.Exit;
        }


        [Button("执行该状态")]
        public virtual void Execute()
        {
            OnEnter();
        }

        public virtual void Running()
        {
            OnRunning();
        }

        public virtual void RunOver()
        {
            OnRunOver();
        }
        public virtual void Exit()
        {
            OnExit();
        }

        protected virtual void OnAddState()
        {

        }
    }

Trigger-触发器

        触发器是决定事件何时被触发的关键。在传统的消息事件框架中,使用SendMessage等进行发送事件,这依靠反射机制查找消息不仅损耗性能,也不利于后期维护;而直接引用对应脚本调用对应的public函数等方式,则会大大提高程序的耦合度。

        这里我们主要使用观察者模式和委托的思想去设置对应的Trigger。因为触发器应该是独立存在的个体,当它存在时它应该自动去监听事件是否发生,而消失时也会自动注销监听,不会与其他功能组件和脚本发生嵌套关系。我们继承BaseState重写方法来实现它:

一个Trigger需要的模块有:

1.一个触发这个Trigger的方式

2.执行事件,即调用子物体下的第一个Action,并能保证Action按照顺序执行

public class BaseTrigger : BaseState
    {
        [Header("Trigger")]
        [LabelText("执行Action")]
        public List<BaseAction> actions;

        [LabelText("在DisOnEnable中注销事件")]
        public bool DeleteEventOnDisEnable = false;    

        public void GetActions()
        {
            actions = new List<BaseAction>(GetComponentsInChildren<BaseAction>());
        }
        protected override void Awake()
        {
            GetActions();
        }

        public override void Execute()
        {
            if (executeType == ExecuteType.Running)
                return;

            base.Execute();

            Running();
        }

        public override void Running()
        {
            base.Running();
            //执行第一个命令
            if (actions != null && actions.Count > 0)
            {
                actions[0].Execute();
            }
            else
                RunOver();

        }

        public override void RunOver()
        {
            base.RunOver();

            Exit();
        }

        public override void Exit()
        {
            base.Exit();
        }

        protected void OnDisable()
        {
            base.OnDisable();

            if (DeleteEventOnDisEnable)
                DeleteSaveTypeEvent();
        }

        [Button("绑定事件")]
        public virtual void RegisterSaveTypeEvent()
        {
            //EventManager.StartListening("");
        }

        [Button("注销事件")]
        public virtual void DeleteSaveTypeEvent()
        {
            //EventManager.StopListening("");
        }
    }

        可以看到,我们先用了一个List<Action>获得了搭载该脚本物品的子物品下的Action函数,重写了Running(),状态的执行函数,让他执行子物品下第一个Action。同时,我们新建了两个函数RegisterSaveTypeEvent()和DeleteSaveTypeEvent(),用于监听事件的发生,当事件发生时执行对应的Execute函数即可。

        比如这个Trigger用来绑定一个按钮,我们来实现一个ButtonTrigger,当玩家按下某个button时执行对应的事件。

public class ButtonTrigger : BaseTrigger
    {
        [Header("ButtonTrigger")]
        public Button btn;

        protected void Start()
        {
            base.Start();

            RegisterSaveTypeEvent();
        }
        protected void OnDestroy()
        {
            base.OnDestroy();

            DeleteSaveTypeEvent();
        }

        public override void RegisterSaveTypeEvent()
        {
            btn.onClick.AddListener(Execute);
        }

        public override void DeleteSaveTypeEvent()
        {
            btn.onClick.RemoveListener(Execute);
        }
    }

Action-事件执行内容

Trigger将顺序运行子物品下的所有Action,因此我们的Action需要的模块有:

1.实现自己需要的功能

2.当执行完成后,告知Trigger可以执行下一个Action了

3.当自己为Action列表最后一个时,告知Trigger所有事件已执行完毕,Trigger切换成已执行完成状态

我们来实现一个通用的BaseAction基类

public class BaseAction : BaseState
    {
        [Header("Action")]
        [LabelText("延迟多少秒进入")]
        public float delayTime;
        [LabelText("延迟多少秒执行下一个")]
        public float waitTime;
        [LabelText("增加一段具体逻辑")]
        public UnityEvent _unityEvent;

        protected override void Awake()
        {
            parentState = GetComponentInParent<BaseTrigger>();
        }

        public override void Execute()
        {
            base.Execute();

            if(delayTime > 0)
                Invoke(nameof(Running), delayTime);
            else
                Running();
        }

        public override void Running()
        {
            base.Running();
            RunningLogic();

        }

        //新Action只需重写RunningLogic()即可,并在结束时调用RunOver();
        protected virtual void RunningLogic()
        {
            _unityEvent?.Invoke();

            //RunOver();
        }

        public override void RunOver()
        {
            base.RunOver();

            if (waitTime > 0)
                Invoke(nameof(Exit), waitTime);
            else
                Exit();

        }

        public override void Exit()
        {
            //检查父节点Trigger,执行下一条命令或者结束Trigger
            if (parentState != null && parentState is BaseTrigger baseTrigger)
            {
                //顺序执行
                var index = baseTrigger.actions.IndexOf(this);
                index++;
                if (index >= baseTrigger.actions.Count)
                {
                    baseTrigger.RunOver();
                }
                else
                {
                    baseTrigger.actions[index].Execute();
                }
            }

            base.Exit();
        }

    }

可以看到,我们使用了UnityEvent方便我们可能的后续扩展。在派生类中可通过重写RunLogic()实现我们需要的功能。在退出状态Exit()中,我们需要获得父物体Trigger,并检测自己对应Action列表的索引Index,并执行Trigger下Index+1的事件或者实行Trigger的RunOver事件来表示事件已经被执行完成。

我们用它来实现一个简单的Debug功能,代码如下:

public class DebugLogAction : BaseAction
    {
        [Header("DebugLogAction"),TextArea]
        public string DebugContent = "DebugLogAction Execute";

        public bool Log = true;
        public bool LogWarning = false;
        public bool LogError = false;

        protected override void RunningLogic()
        {
            base.RunningLogic();
            if(Log)
                Debug.Log(DebugContent);
            if(LogWarning)
                Debug.LogWarning(DebugContent);
            if(LogError)
                Debug.LogError(DebugContent);

            RunOver();
        }
    }

        通过这个简单的例子,介绍了框架基本原理,在此基础上也很方便进行扩展。如果还有下期,会讲讲在这个框架上控制游戏状态的存储改变和其他拓展功能。

        接着我们联立以下这篇帖子,实现一个事件在观察者模式下的监听Trigger

[Unity] C#使用委托事件与字典实现unity消息中心(观察者模式)_Sugarzo的博客-CSDN博客

public class EventTrigger : BaseTrigger
    {    
        [Header("EventTrigger")]
        public EventEnum eventEnum;

        protected void Awake()
        {
            base.Awake();
            
            EventManager.StartListening(eventEnum.ToString(),Execute);
        }

        protected void OnDestroy()
        {
            base.OnDestroy();

            EventManager.StopListening(eventEnum.ToString(), Execute);
        }
    }
}
//一个事件类型的枚举,根据项目需求进行添加
public enum EventEnum
{
}

第二篇文章:

[Unity] 状态机事件流程框架 (二) 设计游戏状态的保存框架,存档功能 ScriptableObject、EasySave_Sugarzo的博客-CSDN博客

  • 10
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值