[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篇。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Unity3D状态机用于管理游戏对象的状态和状态转换。状态是指游戏对象可能处于的不同状态,例如待机、行走、跳跃等。状态机定义了游戏对象的所有状态及其之间的转换规则。 Unity状态机系统主要通过Animator组件实现。Animator组件内部包含多个状态,每个状态代表一个动画片段,可以通过连接这些状态来创建动画状态机状态机可以使用Animator Controller来控制,Animator Controller是一个可视化编辑器,通过在其中创建状态和转换来管理状态机状态机的基本工作原理是根据条件决定游戏对象之间的状态转换。每个状态都有一个或多个条件,当这些条件满足时,状态机会从当前状态转移到下一个状态。可以通过动画事件、脚本和条件参数来触发状态转换。 在使用Unity3D状态机时,首先需要创建状态和转换。可以在Animator Controller中通过添加状态机层来创建新的状态机。然后,在状态机中添加状态,并设置状态对应的动画。接下来,通过建立状态之间的转换来定义状态机的流程。可以通过定义条件、设置触发时间和设置转换条件等方式来进行状态转换的配置。 Unity3D状态机的优点包括易于使用和管理、通过动画片段和转换规则有效控制游戏对象的状态变化、支持在动画事件、脚本和条件参数的基础上自定义状态转换等。通过合理设计和使用状态机,可以有效地控制游戏对象的行为,提升游戏的表现力和交互性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值