[Unity] GraphView 可视化节点的事件行为树(一) Runtime Node

前言:        

        这个框架最近自己终于补充完成了,使用文档和源码已经放在了Github上,可以在之前的文章中找到:

[Unity] 使用GraphView实现一个可视化节点的事件行为树系统(序章/Github下载)_Sugarzo的博客-CSDN博客_unity graphview

        

正文:

        本文将开始介绍Runtime部分的事件节点逻辑。在本框架中,因为Grapview的节点图属于Editor部分,在游戏运行时是不会被加载进来的。因此首先我们需要一个离开节点图,也可以在游戏实时运行中执行逻辑的节点结构。文章涉及的事件触发思想其实已经在我之前写过的一篇文章中了,也可以算作之前思想的延续。如果没看过的可以浅看一下:

[Unity] 状态机事件流程框架 (一)(C#事件系统,Trigger与Action)_Sugarzo的博客-CSDN博客_c# 事件系统

       在半年前自己的文章中,自己曾介绍了一种事件触发的框架:将每个节点作为一个游戏物品(GameObject GO),使用父子物品的结构作为执行顺序来触发事件。

         这种方式因为直接使用了父子游戏物品作为组织方式,当大规模使用起来还是有点约束,因为触发器节点和事件节点形成的是一对一的关系,子物品也只能按照顺序执行事件下去,当需要处理一些分支/循环逻辑的时候就不太方便了,而且游戏物品太多也会对性能优化产生一定的影响。但是里面的设计思想我们还是可以保留的。

        从实际图中,可以看到在新版本里,节点可以执行进行分支和多输入逻辑了。

        在本框架中,我们依然延续之前的思想,所有的节点都是继承自MonoBehaviour,节点作为一个Component附加在游戏物品上。这里选用Component当然这自然不是理论上的最高效率。这里设计有几个权衡,一是该节点的数据成员都是由Unity自带的Inspector绘制,在Unity给我们提供的API中,Editor.CreateEditor只支持UnityEngine.Object的派生类。(当然使用ScriptableObject也是可以的,但是就对比Mono少了生命周期函数了)


[ExcludeFromDocs]
public static Editor CreateEditor(UnityEngine.Object targetObject)
{
    Type editorType = null;
    return CreateEditor(targetObject, editorType);
}

        首先是事件节点状态的基类部分

 

        在状态基类里,需要表示的结构有:

        1.当前节点的状态机状态

        2.一些状态行为的虚函数

        3.该节点指向的下一节点(流,flow)

        4.这个节点在节点图中的位置(Vector2,仅Editor数据,将在后面章节运用)

        在Runtime中,只需有123点就可以运行节点逻辑了。

using UnityEngine;
using Sirenix.OdinInspector;
using System.Collections.Generic;

namespace SugarFrame.Node
{
    public enum EState
    {
        [LabelText("未执行")]
        None,
        [LabelText("正在进入")]
        Enter,
        [LabelText("正在执行")]
        Running,
        [LabelText("正在退出")]
        Exit,
        [LabelText("执行完成")]
        Finish,
    }
    public interface IStateEvent
    {
        void Execute();
        void OnEnter();
        void OnRunning();
        void OnExit();
    }

    public abstract class NodeState : MonoBehaviour
    {
#if UNITY_EDITOR
        [HideInInspector]
        public Vector2 nodePos;
#endif
        //流向下一节点的流
        public MonoState nextFlow;
    }

    public abstract class MonoState : NodeState, IStateEvent
    {
        [SerializeField,Space]
        protected EState state;

        [TextArea,Space]
        public string note;

        protected void TransitionState(EState _state)
        {
            state = _state;

            switch (state)
            {
                case EState.Enter:
                    OnEnter();
                    break;
                case EState.Running:
                    OnRunning();
                    break;
                case EState.Exit:
                    OnExit();
                    break;
            }
        }

        public virtual void Execute()
        {
            TransitionState(EState.Enter);
        }
        public virtual void OnEnter()
        {
            TransitionState(EState.Running);
        }
        public virtual void OnRunning()
        {
            TransitionState(EState.Exit);
        }
        public virtual void OnExit()
        {
            TransitionState(EState.Finish);
        }
    }
}


        接着设计触发器节点和事件节点基类逻辑,在之前的文章中,有写到过这两个节点的设计思想。这里其实设计起来也差不多:

using System.Collections;
using UnityEngine;
using Sirenix.OdinInspector;

namespace SugarFrame.Node
{
    public enum ExecutePeriod
    {
        None,
        Awake,
        Enable,
        Start,
        Update,
        DisEnable,
        Destroy,
    }

    public interface ITriggerEvent
    {
        void RegisterSaveTypeEvent();
        void DeleteSaveTypeEvent();
    }

    public abstract class BaseTrigger : MonoState,ITriggerEvent
    {
        
        [LabelText("生命周期执行")]
        public ExecutePeriod executePeriod = ExecutePeriod.None;
        [Header("允许状态未结束时依然可以执行")]
        public bool canExecuteOnRunning = false;
        [Header("只执行一次")]
        public bool runOnlyOnce = false;

        //(可选)在子类中实现下面两个方法
        public virtual void RegisterSaveTypeEvent()
        {
            //EventManager.StartListening("");
        }
        public virtual void DeleteSaveTypeEvent()
        {
            //EventManager.StopListening("");
        }

        [Button]
        public override void Execute()
        {
            if(!canExecuteOnRunning)
                if (state == EState.Enter || state == EState.Running || state == EState.Exit)
                    return;

            base.Execute();

            if (runOnlyOnce)
                Destroy(this);
        }

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

            if (nextFlow != null)
            {
                if (nextFlow is BaseAction nextAction)
                    nextAction.Execute(this);
                else
                    nextFlow.Execute();
            }
                
        }

        public override void OnRunning()
        {
            //Trigger不需要实现OnRunning,由Action回调OnExit退出
            //base.OnRunning();
        }

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

        }

        protected virtual void Awake()
        {
            if (executePeriod == ExecutePeriod.Awake)
                Execute();
        }

        Coroutine updateCoroutine = null;

        protected virtual void OnEnable()
        {
            if (executePeriod == ExecutePeriod.Enable)
                Execute();

            RegisterSaveTypeEvent();

            //使用协程模拟update,优化不选择ExecutePeriod.Update时的性能
            if (executePeriod == ExecutePeriod.Update)
                updateCoroutine = StartCoroutine(IEUpdate());
        }

        protected virtual void Start()
        {
            if (executePeriod == ExecutePeriod.Start)
                Execute();
        }

        protected virtual IEnumerator IEUpdate()
        {
            while(true)
            {
                yield return null;
                if (gameObject.activeSelf)
                    Execute();
                else
                    yield break;
            }
        }

        protected virtual void OnDisable()
        {
            if (executePeriod == ExecutePeriod.DisEnable)
                Execute();

            DeleteSaveTypeEvent();

            if (updateCoroutine != null)
            {
                StopCoroutine(updateCoroutine);
                updateCoroutine = null;
            }
                
        }

        protected virtual void OnDestroy()
        {
            if (executePeriod == ExecutePeriod.Destroy)
                Execute();
        }
    }
}


using Sirenix.OdinInspector;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace SugarFrame.Node
{
    public abstract class BaseAction : MonoState
    {
        [Header("进入时等待一帧")]
        public bool wait1Frame = false;
        
        //在派生类中填写逻辑,并回调Runover()
        public abstract void RunningLogic(BaseTrigger emitTrigger);

        [Button]
        public override void Execute()
        {
            Execute(null);
        }

        public void Execute(BaseTrigger emitTrigger)
        {
            TransitionState(EState.Running);

            if (wait1Frame && gameObject.activeInHierarchy)
            {
                StartCoroutine(DelayFrame(RunningLogic, emitTrigger));
            }
            else
            {
                RunningLogic(emitTrigger);
            }  
        }

        public virtual void RunOver(BaseTrigger emitTrigger)
        {
            OnExitEvent?.Invoke();
            OnExitEvent = null;

            if (nextFlow)
            {
                //继续执行下一个节点
                if (nextFlow is BaseAction nextAction)
                    nextAction.Execute(emitTrigger);
                else
                    nextFlow.Execute();
            }
            else
            {
                //最后一个节点了,切换Trigger状态
                emitTrigger?.OnExit();
            }
            TransitionState(EState.Exit);
        }

        public override void OnRunning()
        {
            //不执行任何操作,由RunOver触发OnExit
        }

        [HideInInspector]
        public event Action OnExitEvent;

        IEnumerator DelayFrame(Action<BaseTrigger> action,BaseTrigger emitTrigger)
        {
            yield return null;
            action?.Invoke(emitTrigger);
        }
    }
}


         在这里,我们在基类中暴露两个API给Trigger和Action的派生类编辑。BaseAction节点是RunningLogic函数,在新建事件节点时只需要重写这个就可定义节点逻辑,RunOver决定函数何时结束。BaseTrigger是RegisterSaveTypeEvent和DeleteSaveTypeEvent,只需要拓展时自己决定什么时候调用Execute执行即可,当然也可以自己override GameObject的生命周期去实现触发器逻辑。

        以下是拓展这两个节点的脚本模板。

public class #TTT# : BaseAction
    {
        [Header("#TTT#")]
        public string content;

        public override void RunningLogic(BaseTrigger emitTrigger)
        {
            //Write Logic

            RunOver(emitTrigger);
        }
    }

public class #TTT# : BaseTrigger
    {
        //Called on Enable
        public override void RegisterSaveTypeEvent()
        {
            //EventManager.StartListening("",Execute);
        }

        //Called on DisEnable
        public override void DeleteSaveTypeEvent()
        {
            //EventManager.StopListening("",Execute);
        }
    }

        在没有设计出节点图框架前,我们可以手动在Inspector窗口连接触发器节点和事件节点的nextFlow属性,就可以看到Runtime部分的节点逻辑正常运行了。这里拿ButtonTrigger和DebugAction节点做示范:

        (调整nextFlow,按下Button,执行逻辑)

         其中两个节点写法:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace SugarFrame.Node
{
    public class ButtonTrigger : BaseTrigger
    {
        public List<Button> buttons;

        //Called on Enable
        public override void RegisterSaveTypeEvent()
        {
            foreach (var btn in buttons)
                btn?.onClick.AddListener(Execute);
        }

        //Called on DisEnable
        public override void DeleteSaveTypeEvent()
        {
            foreach (var btn in buttons)
                btn?.onClick.RemoveListener(Execute);
        }
    }
}
using UnityEngine;

namespace SugarFrame.Node
{
    public class DebugAction : BaseAction
    {
        [Header("Debug Action")]
        public string content;

        public override void RunningLogic(BaseTrigger emitTrigger)
        {
            Debug.Log(content);

            RunOver(emitTrigger);
        }
    }

}

        接着,我们把分支节点和序列节点这两个节点也实现了。在分支节点逻辑中,我们给派生类流一个判断虚函数的接口,根据判断结构设置nextFlow的流向即可。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace SugarFrame.Node
{
    public abstract class BaseBranch : BaseAction
    {
        //流向下一节点的流
        [HideInInspector]
        public MonoState trueFlow;
        [HideInInspector]
        public MonoState falseFlow;

        //在派生类中实现该逻辑
        public abstract bool IfResult();

        public override void RunningLogic(BaseTrigger emitTrigger)
        {
            RunOver(emitTrigger);
        }

        public override void RunOver(BaseTrigger emitTrigger)
        {
            //判断下一节点的流向
            nextFlow = IfResult() ? trueFlow : falseFlow;

            if (nextFlow)
            {
                //继续执行下一个节点
                if (nextFlow is BaseAction nextAction)
                    nextAction.Execute(emitTrigger);
                else
                    nextFlow.Execute();
            }
            else
            {
                //最后一个节点了,切换Trigger状态
                emitTrigger?.OnExit();
            }
            TransitionState(EState.Finish);
        }
    }
}


        序列节点:可以负责多个流向的节点。这里就需要List<MonoState>了。因为当逻辑结束时,我们需要回调Trigger切换条件。而Sequence可能由多个触发器节点同时调用,每个流向的逻辑执行完时间就可能存在不同。这里的处理就复杂一点。借用了委托和缓存的思想,每次触发这个节点时都使用lambda注册一个数据结构用来标记这个sequence后续所有流向有没有被完成,只有当所有的流向都执行完成时才回调Trigger。

public abstract class BaseSequence : BaseAction
    {

        [HideInInspector]
        public List<MonoState> nextflows = new List<MonoState>();
        [Header("每个行为之间是否等待x秒,输入-1时等待1帧")]
        public float waitTimeEachAction = 0;
        [ReadOnly]
        public int runningAction = 0;

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


        /// <summary>
        /// 向下执行所有节点
        /// </summary>
        public override void RunningLogic(BaseTrigger emitTrigger)
        {
            if (nextflows != null && nextflows.Count > 0)
            {
                runningAction = nextflows.Count;
                StartCoroutine(StartActions(emitTrigger));
            }
            else
            {
                //Sequence节点输出为空,直接切换到结束状态
                RunOver(emitTrigger);
            }
        }

        private IEnumerator StartActions(BaseTrigger emitTrigger)
        {
            DataCache cache = new DataCache();
            cache.count = nextflows.Count;
            cache.trigger = emitTrigger;

            //继续所有节点
            foreach (var nextFlow in nextflows)
            {
                if (nextFlow is BaseAction nextAction)
                    nextAction.Execute();
                else
                    nextFlow.Execute();

                //依赖注入,当所有Action执行完成时回调Trigger
                if (nextFlow is BaseAction action)
                    action.OnExitEvent += delegate ()
                    {
                        cache.count--;
                        if(cache.count == 0)
                        {
                            cache.trigger?.OnExit();
                        }
                    };

                nextFlow.Execute();

                if (waitTimeEachAction > 0)
                    yield return new WaitForSeconds(waitTimeEachAction);
                if (waitTimeEachAction == -1)
                    yield return null;
            }
            yield return null;
        }


        private class DataCache
        {
            public BaseTrigger trigger;
            public int count;
        }
    }

        好了,在这里就已经讲完了Runtime部分的Node框架了。下一章节就会进入Editor部分的UI  Toolkit篇。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值