前言:
这个框架最近自己终于补充完成了,使用文档和源码已经放在了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。文章来源:https://www.toymoban.com/news/detail-731891.html
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篇。文章来源地址https://www.toymoban.com/news/detail-731891.html
到了这里,关于[Unity] GraphView 可视化节点的事件行为树(一) Runtime Node的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!