前言:? ? ? ??
????????這個(gè)框架最近自己終于補(bǔ)充完成了,使用文檔和源碼已經(jīng)放在了Github上,可以在之前的文章中找到:
[Unity] 使用GraphView實(shí)現(xiàn)一個(gè)可視化節(jié)點(diǎn)的事件行為樹系統(tǒng)(序章/Github下載)_Sugarzo的博客-CSDN博客_unity graphview
????????
正文:
????????本文將開始介紹Runtime部分的事件節(jié)點(diǎn)邏輯。在本框架中,因?yàn)镚rapview的節(jié)點(diǎn)圖屬于Editor部分,在游戲運(yùn)行時(shí)是不會(huì)被加載進(jìn)來(lái)的。因此首先我們需要一個(gè)離開節(jié)點(diǎn)圖,也可以在游戲?qū)崟r(shí)運(yùn)行中執(zhí)行邏輯的節(jié)點(diǎn)結(jié)構(gòu)。文章涉及的事件觸發(fā)思想其實(shí)已經(jīng)在我之前寫過(guò)的一篇文章中了,也可以算作之前思想的延續(xù)。如果沒看過(guò)的可以淺看一下:
[Unity] 狀態(tài)機(jī)事件流程框架 (一)(C#事件系統(tǒng),Trigger與Action)_Sugarzo的博客-CSDN博客_c# 事件系統(tǒng)
? ? ? ?在半年前自己的文章中,自己曾介紹了一種事件觸發(fā)的框架:將每個(gè)節(jié)點(diǎn)作為一個(gè)游戲物品(GameObject GO),使用父子物品的結(jié)構(gòu)作為執(zhí)行順序來(lái)觸發(fā)事件。
? ? ? ? ?這種方式因?yàn)橹苯邮褂昧烁缸佑螒蛭锲纷鳛榻M織方式,當(dāng)大規(guī)模使用起來(lái)還是有點(diǎn)約束,因?yàn)橛|發(fā)器節(jié)點(diǎn)和事件節(jié)點(diǎn)形成的是一對(duì)一的關(guān)系,子物品也只能按照順序執(zhí)行事件下去,當(dāng)需要處理一些分支/循環(huán)邏輯的時(shí)候就不太方便了,而且游戲物品太多也會(huì)對(duì)性能優(yōu)化產(chǎn)生一定的影響。但是里面的設(shè)計(jì)思想我們還是可以保留的。
? ? ? ? 從實(shí)際圖中,可以看到在新版本里,節(jié)點(diǎn)可以執(zhí)行進(jìn)行分支和多輸入邏輯了。
? ? ? ? 在本框架中,我們依然延續(xù)之前的思想,所有的節(jié)點(diǎn)都是繼承自MonoBehaviour,節(jié)點(diǎn)作為一個(gè)Component附加在游戲物品上。這里選用Component當(dāng)然這自然不是理論上的最高效率。這里設(shè)計(jì)有幾個(gè)權(quán)衡,一是該節(jié)點(diǎn)的數(shù)據(jù)成員都是由Unity自帶的Inspector繪制,在Unity給我們提供的API中,Editor.CreateEditor只支持UnityEngine.Object的派生類。(當(dāng)然使用ScriptableObject也是可以的,但是就對(duì)比Mono少了生命周期函數(shù)了)
[ExcludeFromDocs]
public static Editor CreateEditor(UnityEngine.Object targetObject)
{
Type editorType = null;
return CreateEditor(targetObject, editorType);
}
????????首先是事件節(jié)點(diǎn)狀態(tài)的基類部分
?
? ? ? ? 在狀態(tài)基類里,需要表示的結(jié)構(gòu)有:
? ? ? ? 1.當(dāng)前節(jié)點(diǎn)的狀態(tài)機(jī)狀態(tài)
? ? ? ? 2.一些狀態(tài)行為的虛函數(shù)
? ? ? ? 3.該節(jié)點(diǎn)指向的下一節(jié)點(diǎn)(流,flow)
? ? ? ? 4.這個(gè)節(jié)點(diǎn)在節(jié)點(diǎn)圖中的位置(Vector2,僅Editor數(shù)據(jù),將在后面章節(jié)運(yùn)用)
? ? ? ? 在Runtime中,只需有123點(diǎn)就可以運(yùn)行節(jié)點(diǎn)邏輯了。
using UnityEngine;
using Sirenix.OdinInspector;
using System.Collections.Generic;
namespace SugarFrame.Node
{
public enum EState
{
[LabelText("未執(zhí)行")]
None,
[LabelText("正在進(jìn)入")]
Enter,
[LabelText("正在執(zhí)行")]
Running,
[LabelText("正在退出")]
Exit,
[LabelText("執(zhí)行完成")]
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
//流向下一節(jié)點(diǎn)的流
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);
}
}
}
? ? ? ? 接著設(shè)計(jì)觸發(fā)器節(jié)點(diǎn)和事件節(jié)點(diǎn)基類邏輯,在之前的文章中,有寫到過(guò)這兩個(gè)節(jié)點(diǎn)的設(shè)計(jì)思想。這里其實(shí)設(shè)計(jì)起來(lái)也差不多:
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("生命周期執(zhí)行")]
public ExecutePeriod executePeriod = ExecutePeriod.None;
[Header("允許狀態(tài)未結(jié)束時(shí)依然可以執(zhí)行")]
public bool canExecuteOnRunning = false;
[Header("只執(zhí)行一次")]
public bool runOnlyOnce = false;
//(可選)在子類中實(shí)現(xiàn)下面兩個(gè)方法
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不需要實(shí)現(xiàn)OnRunning,由Action回調(diào)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();
//使用協(xié)程模擬update,優(yōu)化不選擇ExecutePeriod.Update時(shí)的性能
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("進(jìn)入時(shí)等待一幀")]
public bool wait1Frame = false;
//在派生類中填寫邏輯,并回調(diào)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)
{
//繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn)
if (nextFlow is BaseAction nextAction)
nextAction.Execute(emitTrigger);
else
nextFlow.Execute();
}
else
{
//最后一個(gè)節(jié)點(diǎn)了,切換Trigger狀態(tài)
emitTrigger?.OnExit();
}
TransitionState(EState.Exit);
}
public override void OnRunning()
{
//不執(zhí)行任何操作,由RunOver觸發(fā)OnExit
}
[HideInInspector]
public event Action OnExitEvent;
IEnumerator DelayFrame(Action<BaseTrigger> action,BaseTrigger emitTrigger)
{
yield return null;
action?.Invoke(emitTrigger);
}
}
}
?????????在這里,我們?cè)诨愔斜┞秲蓚€(gè)API給Trigger和Action的派生類編輯。BaseAction節(jié)點(diǎn)是RunningLogic函數(shù),在新建事件節(jié)點(diǎn)時(shí)只需要重寫這個(gè)就可定義節(jié)點(diǎn)邏輯,RunOver決定函數(shù)何時(shí)結(jié)束。BaseTrigger是RegisterSaveTypeEvent和DeleteSaveTypeEvent,只需要拓展時(shí)自己決定什么時(shí)候調(diào)用Execute執(zhí)行即可,當(dāng)然也可以自己override GameObject的生命周期去實(shí)現(xiàn)觸發(fā)器邏輯。
????????以下是拓展這兩個(gè)節(jié)點(diǎn)的腳本模板。
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);
}
}
????????在沒有設(shè)計(jì)出節(jié)點(diǎn)圖框架前,我們可以手動(dòng)在Inspector窗口連接觸發(fā)器節(jié)點(diǎn)和事件節(jié)點(diǎn)的nextFlow屬性,就可以看到Runtime部分的節(jié)點(diǎn)邏輯正常運(yùn)行了。這里拿ButtonTrigger和DebugAction節(jié)點(diǎn)做示范:
????????(調(diào)整nextFlow,按下Button,執(zhí)行邏輯)
?????????其中兩個(gè)節(jié)點(diǎn)寫法:
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);
}
}
}
? ? ? ? 接著,我們把分支節(jié)點(diǎn)和序列節(jié)點(diǎn)這兩個(gè)節(jié)點(diǎn)也實(shí)現(xiàn)了。在分支節(jié)點(diǎn)邏輯中,我們給派生類流一個(gè)判斷虛函數(shù)的接口,根據(jù)判斷結(jié)構(gòu)設(shè)置nextFlow的流向即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SugarFrame.Node
{
public abstract class BaseBranch : BaseAction
{
//流向下一節(jié)點(diǎn)的流
[HideInInspector]
public MonoState trueFlow;
[HideInInspector]
public MonoState falseFlow;
//在派生類中實(shí)現(xiàn)該邏輯
public abstract bool IfResult();
public override void RunningLogic(BaseTrigger emitTrigger)
{
RunOver(emitTrigger);
}
public override void RunOver(BaseTrigger emitTrigger)
{
//判斷下一節(jié)點(diǎn)的流向
nextFlow = IfResult() ? trueFlow : falseFlow;
if (nextFlow)
{
//繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn)
if (nextFlow is BaseAction nextAction)
nextAction.Execute(emitTrigger);
else
nextFlow.Execute();
}
else
{
//最后一個(gè)節(jié)點(diǎn)了,切換Trigger狀態(tài)
emitTrigger?.OnExit();
}
TransitionState(EState.Finish);
}
}
}
????????序列節(jié)點(diǎn):可以負(fù)責(zé)多個(gè)流向的節(jié)點(diǎn)。這里就需要List<MonoState>了。因?yàn)楫?dāng)邏輯結(jié)束時(shí),我們需要回調(diào)Trigger切換條件。而Sequence可能由多個(gè)觸發(fā)器節(jié)點(diǎn)同時(shí)調(diào)用,每個(gè)流向的邏輯執(zhí)行完時(shí)間就可能存在不同。這里的處理就復(fù)雜一點(diǎn)。借用了委托和緩存的思想,每次觸發(fā)這個(gè)節(jié)點(diǎn)時(shí)都使用lambda注冊(cè)一個(gè)數(shù)據(jù)結(jié)構(gòu)用來(lái)標(biāo)記這個(gè)sequence后續(xù)所有流向有沒有被完成,只有當(dāng)所有的流向都執(zhí)行完成時(shí)才回調(diào)Trigger。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-731891.html
public abstract class BaseSequence : BaseAction
{
[HideInInspector]
public List<MonoState> nextflows = new List<MonoState>();
[Header("每個(gè)行為之間是否等待x秒,輸入-1時(shí)等待1幀")]
public float waitTimeEachAction = 0;
[ReadOnly]
public int runningAction = 0;
public override void OnEnter()
{
base.OnEnter();
}
/// <summary>
/// 向下執(zhí)行所有節(jié)點(diǎn)
/// </summary>
public override void RunningLogic(BaseTrigger emitTrigger)
{
if (nextflows != null && nextflows.Count > 0)
{
runningAction = nextflows.Count;
StartCoroutine(StartActions(emitTrigger));
}
else
{
//Sequence節(jié)點(diǎn)輸出為空,直接切換到結(jié)束狀態(tài)
RunOver(emitTrigger);
}
}
private IEnumerator StartActions(BaseTrigger emitTrigger)
{
DataCache cache = new DataCache();
cache.count = nextflows.Count;
cache.trigger = emitTrigger;
//繼續(xù)所有節(jié)點(diǎn)
foreach (var nextFlow in nextflows)
{
if (nextFlow is BaseAction nextAction)
nextAction.Execute();
else
nextFlow.Execute();
//依賴注入,當(dāng)所有Action執(zhí)行完成時(shí)回調(diào)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;
}
}
? ? ? ? 好了,在這里就已經(jīng)講完了Runtime部分的Node框架了。下一章節(jié)就會(huì)進(jìn)入Editor部分的UI? Toolkit篇。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-731891.html
到了這里,關(guān)于[Unity] GraphView 可視化節(jié)點(diǎn)的事件行為樹(一) Runtime Node的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!