NodeCanvas
是一款很棒的插件,包含了行为树、对话树、状态机等数据结构,提供可视化的图形服务,依此我们可以扩展一种FlowTree
的流式图。
# 思考
# FlowTree
FlowTree 是我想要实现的一种数据结构,即流程树结构;在流程树中的所有节点会根据检查条件选择适合的分支依次执行。这其实和官方提供的 DialogTree
对话树的逻辑是差不多的;但是在对话树基本节点是对话节点。
为了能够执行一些列操作,而这一些操作可能包含对话也可能只是纯粹的 Action,故而需要一种新的流程树形式。
从上图中可以看出,我们可以不再依赖于对话节点,我们可以 行为、条件、子图作为起始,也可以根据这些元素作为结尾任意的连接数据。
# 实现
以上是一个基本树的目录组成结构,分别包括:
- FlowTreeOwner 树持有者,继承自
MonoBehaviour
- FlowTree 树数据,继承自
Graph
- FTNode 基础节点数据,继承自
Node
- FTConnection 基础连接数据,继承自
Connection
public class FlowTreeOwner: GraphOwner<FlowTree> | |
{ | |
} |
[CreateAssetMenu(menuName = "ParadoxNotion/NodeCanvas/Flow Tree Asset")] | |
public class FlowTree : Graph | |
{ | |
public override Type baseNodeType => typeof(FTNode); | |
public override bool requiresAgent => false; | |
public override bool requiresPrimeNode => true; | |
public override bool isTree => true; | |
public override PlanarDirection flowDirection => PlanarDirection.Horizontal; | |
public override bool allowBlackboardOverrides => true; | |
public override bool canAcceptVariableDrops => false; | |
private float _intTime; | |
private float _updateTime; | |
private Status _rootStatus; | |
private FTNode _currentNode; | |
protected override void OnGraphStarted() | |
{ | |
Debug.Log($"FlowTree {name} 启动"); | |
_currentNode = primeNode as FTNode; | |
EnterNode(_currentNode != null ? _currentNode : (FTNode)primeNode); | |
} | |
protected override void OnGraphUpdate() | |
{ | |
if ( _currentNode is IUpdatable ) { | |
( _currentNode as IUpdatable )?.Update(); | |
} | |
} | |
protected override void OnGraphStoped() | |
{ | |
_currentNode = null; | |
} | |
public void GoNext(int index = 0) | |
{ | |
if (index < 0 || index > _currentNode.outConnections.Count - 1) | |
{ | |
Stop(true); | |
return; | |
} | |
_currentNode.outConnections[index].status = Status.Success; //editor vis | |
EnterNode(_currentNode.outConnections[index].targetNode as FTNode); | |
} | |
private void EnterNode(FTNode node) | |
{ | |
_currentNode = node; | |
_currentNode.Reset(false); | |
if (_currentNode.Execute(agent, blackboard) == Status.Error) | |
{ | |
Stop(false); | |
} | |
} | |
} |
public abstract class FTNode : Node | |
{ | |
public override int maxInConnections => -1; | |
public override int maxOutConnections => 1; | |
public override Type outConnectionType => typeof(FTConnection); | |
public override bool allowAsPrime => true; | |
public override bool canSelfConnect => false; | |
public override Alignment2x2 commentsAlignment => Alignment2x2.Right; | |
public override Alignment2x2 iconAlignment => Alignment2x2.Right; | |
/// <summary> | |
/// 流程树 | |
/// </summary> | |
protected FlowTree FlowTree => (FlowTree)graph; | |
#if UNITY_EDITOR | |
protected override void OnNodeInspectorGUI() | |
{ | |
base.OnNodeInspectorGUI(); | |
} | |
protected override UnityEditor.GenericMenu OnContextMenu(UnityEditor.GenericMenu menu) | |
{ | |
menu.AddItem(new GUIContent("Breakpoint"), isBreakpoint, () => { isBreakpoint = !isBreakpoint; }); | |
return menu; | |
} | |
#endif | |
} |
public class FTConnection:Connection | |
{ | |
} |
# 树步进和条件
有了数据结构还不行,我们还需要让数据按我们期望的方式流转起来,其实也就是我们需要告诉我们的树何时应该执行下一步。
public void GoNext(int index = 0) | |
{ | |
if (index < 0 || index > _currentNode.outConnections.Count - 1) | |
{ | |
Stop(true); | |
return; | |
} | |
_currentNode.outConnections[index].status = Status.Success; //editor vis | |
EnterNode(_currentNode.outConnections[index].targetNode as FTNode); | |
} | |
private void EnterNode(FTNode node) | |
{ | |
_currentNode = node; | |
_currentNode.Reset(false); | |
if (_currentNode.Execute(agent, blackboard) == Status.Error) | |
{ | |
Stop(false); | |
} | |
} |
在 FlowTree 中有这样一个方法用于控制树步进,传入一个 index 代表选择的链接分支索引,用于进入下一个节点,如果节点返回失败则停止图,如果成功则进入下一个。
我们需要在所有需要进入下一个节点的地方调用
GoNext
例如当 Action 节点执行完时:
void OnActionEnd(bool success) { | |
if ( success ) { | |
status = Status.Success; | |
FlowTree.GoNext(); | |
return; | |
} | |
status = Status.Failure; | |
FlowTree.Stop(false); | |
} |
例如当条件节点执行完时:
protected override Status OnExecute(Component agent, IBlackboard bb) | |
{ | |
if (outConnections.Count == 0) | |
{ | |
return Error("There are no connections on the Dialogue Condition Node"); | |
} | |
if (Condition == null) | |
{ | |
return Error("There is no Conidition on the Dialoge Condition Node"); | |
} | |
var isSuccess = Condition.CheckOnce(agent.transform, graphBlackboard); | |
status = isSuccess ? Status.Success : Status.Failure; | |
FlowTree.GoNext(status== Status.Success ? 0 : 1); | |
return status; | |
} |
例如当子图执行完成时:
void OnDLGFinished(bool success) | |
{ | |
if (status == Status.Running) | |
{ | |
status = success ? Status.Success : Status.Failure; | |
FlowTree.GoNext(); // 让流程树继续 | |
} | |
} |
对于条件节点的处理,我们只需要提供一个
ConditionTask
用于处理即可:
[global::ParadoxNotion.Design.Icon("Condition")] | |
[Color("b3ff7f")] | |
public class ConditionNode : FTNode, ITaskAssignable<ConditionTask> | |
{ | |
[SerializeField] private ConditionTask _condition; | |
public ConditionTask Condition | |
{ | |
get => _condition; | |
set => _condition = value; | |
} | |
public Task task | |
{ | |
get => Condition; | |
set => Condition = (ConditionTask)value; | |
} | |
public override int maxOutConnections => 2; | |
protected override Status OnExecute(Component agent, IBlackboard bb) | |
{ | |
if (outConnections.Count == 0) | |
{ | |
return Error("There are no connections on the Dialogue Condition Node"); | |
} | |
if (Condition == null) | |
{ | |
return Error("There is no Conidition on the Dialoge Condition Node"); | |
} | |
var isSuccess = Condition.CheckOnce(agent.transform, graphBlackboard); | |
status = isSuccess ? Status.Success : Status.Failure; | |
FlowTree.GoNext(status== Status.Success ? 0 : 1); | |
return status; | |
} | |
#if UNITY_EDITOR | |
public override string GetConnectionInfo(int i) | |
{ | |
return i == 0 ? "Then" : "Else"; | |
} | |
#endif | |
} |
# 子图 / 外接图
对于子图来说,其实也是属于一个节点,所以本质是继承自
FTNode
。
[Category("SubGraphs")] | |
[Color("ffe4e1")] | |
public abstract class FTNodeNested<T> : FTNode, IGraphAssignable<T> where T : Graph | |
{ | |
[SerializeField] private List<BBMappingParameter> _variablesMap; | |
public abstract BBParameter subGraphParameter { get; } | |
public T currentInstance { get; set; } | |
public abstract T subGraph { get; set; } | |
Graph IGraphAssignable.currentInstance { get => currentInstance; | |
set => currentInstance = (T)value; | |
} | |
Graph IGraphAssignable.subGraph { get => subGraph; | |
set => subGraph = (T)value; | |
} | |
public Dictionary<Graph, Graph> instances { get; set; } | |
public List<BBMappingParameter> variablesMap { get => _variablesMap; | |
set => _variablesMap = value; | |
} | |
} |
这里我们考虑使 FlowTree 中可以接入子图 DialogTree
:
[Name("Sub Dialogue")] | |
[Description("Executes a sub Dialogue Tree. Returns Running while the sub Dialogue Tree is active. You can Finish the Dialogue Tree with the 'Finish' node and return Success or Failure.")] | |
[global::ParadoxNotion.Design.Icon("Dialogue")] | |
[DropReferenceType(typeof(DialogueTree))] | |
public class NestedDT : FTNodeNested<DialogueTree> | |
{ | |
[SerializeField, ExposeField, Name("Sub Tree")] | |
private readonly BBParameter<DialogueTree> _nestedDialogueTree = null; | |
public override DialogueTree subGraph | |
{ | |
get => _nestedDialogueTree.value; | |
set => _nestedDialogueTree.value = value; | |
} | |
public override BBParameter subGraphParameter => _nestedDialogueTree; | |
// | |
protected override Status OnExecute(Component agent, IBlackboard blackboard) | |
{ | |
if (subGraph == null || subGraph.primeNode == null) | |
{ | |
return Status.Optional; | |
} | |
if (status == Status.Resting) | |
{ | |
status = Status.Running; | |
this.TryStartSubGraph(agent, OnDLGFinished); | |
} | |
if (status == Status.Running) | |
{ | |
currentInstance.UpdateGraph(this.graph.deltaTime); | |
} | |
return status; | |
} | |
void OnDLGFinished(bool success) | |
{ | |
if (status == Status.Running) | |
{ | |
status = success ? Status.Success : Status.Failure; | |
FlowTree.GoNext(); // 让流程树继续 | |
} | |
} | |
protected override void OnReset() | |
{ | |
if (currentInstance != null) | |
{ | |
currentInstance.Stop(); | |
} | |
} | |
} |