NodeCanvas是一款很棒的插件,包含了行为树、对话树、状态机等数据结构,提供可视化的图形服务,依此我们可以扩展一种FlowTree的流式图。
思考 FlowTree FlowTree是我想要实现的一种数据结构,即流程树结构;在流程树中的所有节点会根据检查条件选择适合的分支依次执行。这其实和官方提供的DialogTree对话树的逻辑是差不多的;但是在对话树基本节点是对话节点。
为了能够执行一些列操作,而这一些操作可能包含对话也可能只是纯粹的Action,故而需要一种新的流程树形式。
从上图中可以看出,我们可以不再依赖于对话节点,我们可以 行为、条件、子图作为起始,也可以根据这些元素作为结尾任意的连接数据。{.yellow}
实现
以上是一个基本树的目录组成结构,分别包括:
++FlowTreeOwner++ 树持有者,继承自MonoBehaviour
++FlowTree++ 树数据,继承自Graph
++FTNode++ 基础节点数据,继承自Node
++FTConnection++ 基础连接数据,继承自Connection
FlowTreeOwner 1 2 3 public class FlowTreeOwner : GraphOwner <FlowTree >{ }
FlowTree 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 [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; 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 ); } } }
FTNode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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; 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 }
FTConnection 1 2 3 4 public class FTConnection :Connection { }
树步进和条件 有了数据结构还不行,我们还需要让数据按我们期望的方式流转起来,其实也就是我们需要告诉我们的树何时应该执行下一步。
树步进 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void GoNext (int index = 0 ){ if (index < 0 || index > _currentNode.outConnections.Count - 1 ) { Stop(true ); return ; } _currentNode.outConnections[index].status = Status.Success; 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节点执行完时:
1 2 3 4 5 6 7 8 9 10 11 void OnActionEnd (bool success ) { if ( success ) { status = Status.Success; FlowTree.GoNext(); return ; } status = Status.Failure; FlowTree.Stop(false ); }
例如当条件节点执行完时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; }
例如当子图执行完成时:
1 2 3 4 5 6 7 8 9 void OnDLGFinished (bool success ){ if (status == Status.Running) { status = success ? Status.Success : Status.Failure; FlowTree.GoNext(); } }
对于条件节点的处理,我们只需要提供一个ConditionTask用于处理即可:
ConditionNode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 [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。
FTNodeNested 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 [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:
NestedDT 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 [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(); } } }