NodeCanvas自定义图树

NodeCanvas是一款很棒的插件,包含了行为树、对话树、状态机等数据结构,提供可视化的图形服务,依此我们可以扩展一种FlowTree的流式图。

思考

FlowTree

FlowTree是我想要实现的一种数据结构,即流程树结构;在流程树中的所有节点会根据检查条件选择适合的分支依次执行。这其实和官方提供的DialogTree对话树的逻辑是差不多的;但是在对话树基本节点是对话节点。

为了能够执行一些列操作,而这一些操作可能包含对话也可能只是纯粹的Action,故而需要一种新的流程树形式。

image-20240403103133264

从上图中可以看出,我们可以不再依赖于对话节点,我们可以 行为、条件、子图作为起始,也可以根据这些元素作为结尾任意的连接数据。{.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; //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);
}
}
}
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;

/// <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
}
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; //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节点执行完时:

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();
}
}
}