【Unity】行为树

花时间手撸了个行为树,现在通过做笔记的方式巩固一下学习的内容

总体来说,脚本 可以分为俩大类

  • 为ScriptableObject(行为树)服务的脚本
  • 为视图服务的Editor脚本

类图如下


行为树脚本

控制人物行为的主要脚本,主要也可以分为两种

行为树主干

  • BehaviourTreeRunner:挂载到实例上,选择行为树实例,运行行为树
  • BehaviourTree:控制行为树的行为。如:当根节点退出(即失败或成功)时结束整个行为树;创建、删除节点;添加、删除、获取子项,这些功能是为视图服务,在视图中调用这个方法就实现在更改视图的同时,更改ScriptableObject中的内容

节点

  • Node:抽象类,一个进口,没有出口,后面将采用(1,0)的方式表达,所有节点都继承该类。存储一些数据:该实例节点当前状态、guid、该实例节点在视图中的位置坐标
  • ActionNode:抽象类**(1,0)**。特点是没有出口
  • CompositeNode:抽象类**(1,n)**。特点是多个出口
  • DecoratorNode:抽象类**(1,1)**。特点是一个出口
  • RootNode:根节点**(0,1)**。持续不断的运行子节点

以上的最基础的节点,其中Node是所有节点的父类。
ActionNodeSequencerNodeDecoratorNode都是抽象类,只是为了区分接口有多少个出口,没有其他特别的作用。
RootNode是根节点,所有行为树都必有的一个,后续一系列的运行将根节点开始。

如果有想要实现的节点都可以继承三大最基本的抽象类,即ActionNodeSequencerNodeDecoratorNode,如:

  • DebugLogNode:继承ActionNode**(1,0)**,打印字符串
  • WaitNode:继承ActionNode**(1,0)**,延时
  • SequencerNode:继承SequencerNode**(1,n)**,定序器,将按照出口节点的顺序执行
  • RepeatNode:继承DecoratorNode**(1,1)**,重复执行出口的节点

小结

行为树脚本中,每个脚本并不是特别复杂。但是需要注意两点

  • 类与类之间继承的很频繁,需要搞清楚他们之间的关系
  • 在运行时脚本直接的跳转,这一点使用文字不是特别好表达,最好的办法还是启动unity在程序中执行一遍,大致的方向是BehaviourTreeRunner==>BehaviourTree==>RootNode==>根节点的子节点==>跟节点的子节点的子节点……,在程序运行时可以使用调试或者打印的方法查看程序运行的情况

视图脚本

将ScriptableObject中的内容展示到视图上,并且用户在操作视图中的内容时,ScriptableObject也会对应着改变

为了区分行为树中的节点与视图窗口中的节点,在本篇文章中我们将上面的行为树节点称为“节点”,将下方的视图脚本中的节点称为“节点元素”

BehaviourTreeEditor行为树窗口

三个作用:

  • OpenWindowCreateGUI:创建一个窗口,用来显示行为树的视图
  • OnSelectionChange方法:选择HierarchyProject中的行为树后,将行为树的内容展示到BehaviourTreeView视图中
  • OnNodeSelectionChanged方法:选择BehaviourTreeView视图中的节点元素后,将该节点的Inspector窗口的内容映射到BehaviourTreeEditor行为树窗口左边的InspectorView视图中

SplitView

继承TwoPaneSplitView,将窗口分为两个可滑动的视图。如果你将所有脚本都放在了一个新的命名空间中时,创建完该脚本后会报错,原因是uxml脚本中找不到BehaviourTreeViewInspectorView这两个类,可以打开uxml文件,手动更改,在前面加上命名空间,具体可看最后的源码

InspectorView节点信息视图

摆放在行为树窗口的左侧,将选中的节点元素的Inspector窗口中的内容映射到该视图上

  • UpdateSelection:显示当前选中的节点元素的Inspector内容

BehaviourTreeView行为树视图

行为树窗口的主体部分,将选中的ScriptableObject行为树内容展示到视图中,并且在该视图中对各个节点元素的操作也会更改到ScriptableObject中存储起来

  • BehaviourTreeView:设置视图背景,为了达到透明网格的背景效果,可以直接复制uss中的代码,具体可看最后的源码;定义一些基础功能:使该视图可缩放、拖动、选择、方框选择

  • PopulateView:将ScriptableObject中的内容显示到视图中,并注册委托:在视图中发生更改时执行OnGraphViewChanged方法

  • FindNodeView:通过节点的guid查找与其对应的节点元素

  • GetCompatiblePorts:重写父类方法。该方法能让视图中的节点相互连接,如果不写该方法将无法连接

  • OnGraphViewChanged:在打开新的ScriptableObject时委托注册了该方法,该委托会在视图中有任何改变时调用该方法,功能如下

    • 因移除节点元素或连线而调用该方法时:同时删除ScriptableObject中对应的节点,以及该节点的父节点的child。

      值得注意的是graphViewChange.elementsToRemove返回的不仅是移除的节点元素(NodeView),还有连线(Edge)

    • 因创建连线而调用该方法时:同时给ScriptableObject中对应的节点添加子节点

  • BuildContextualMenu:重写父类方法。添加右键菜单功能

    var types = TypeCache.GetTypesDerivedFrom<ActionNode>()返回的是ActionNode所有的派生类

  • CreateNode:调用方法BehaviourTree.CreateNode创建节点CreateNodeView创建节点元素

  • CreateNodeView:创建节点元素

NodeView节点元素

控制各个节点的基础信息

  • NodeViewCreateinputPortsCreateOutputPorts:创建节点元素时设置其标题、guid、位置、进口和出口的类型
  • SetPosition:重写父类方法。在移动该节点元素后,将其新的位置信息存储到ScriptableObject节点中
  • OnSelected:重写父类方法。定义一个委托,在窗口中选择该节点元素后,执行这个委托。每个节点元素在创建的时候就会注册这个委托了,但是注册这个委托的又是另外一个委托(我们称前一个委托为委托A,后一个委托为委托B),委托B是在创建行为树窗口时就注册了,注册实现的内容是:将该节点元素的节点Inspector信息映射到左侧的InspeView视图中。看起来比较绕,但其实就是为了保持代码的可维护性,没有让InspectorView的实例到出些,而为了让NodeView中的方法与BehaviourTreeEditor中的InspectorView实例联系起来而用了两个委托

小结

这样就成功手撸出了一个简单的行为树了,还有可以增添一些小功能,比如新增选择节点、判断节点;右键创建节点元素时,让其生成在鼠标所在的位置;让每个节点元素展示不同的外观方便区分;时节点元素之间的连线律动起来,实时展示当前怪物的行为树进行到哪一步了

到目前为止,也只是实现了一个框架,我们最终的目的是要将这个行为树功能套用到怪物身上的,所以还并没有结束

【Unity教程搬运】使用UI Builder、GraphView和脚本化对象创建行为树


源码

BehaviourTreeRunner

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 行为树执行器
/// </summary>
public class BehaviourTreeRunner : MonoBehaviour
{
public BehaviourTree tree;

private void Start()
{
tree = tree.Clone();
}

private void Update()
{
tree.Update();
}
}
}

BehaviourTree

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 一个怪物的行为树
/// </summary>
[CreateAssetMenu()]
public class BehaviourTree : ScriptableObject
{
public Node rootNode;
public Node.State treeState = Node.State.Running;
public List<Node> nodes = new List<Node>();

public Node.State Update()
{
if (rootNode.state == Node.State.Running)
{
treeState = rootNode.Update();
}
return treeState;
}

public BehaviourTree Clone()
{
BehaviourTree tree = Instantiate(this);
tree.rootNode = tree.rootNode.Clone();
return tree;
}

#region 为视图服务的方法

/// <summary>
/// 创建节点ScriptableObject类
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public Node CreateNode(System.Type type)
{
Node node = ScriptableObject.CreateInstance(type) as Node;
node.name = type.Name;
node.guid = GUID.Generate().ToString();
nodes.Add(node);

AssetDatabase.AddObjectToAsset(node, this);
AssetDatabase.SaveAssets();
return node;
}

/// <summary>
/// 删除节点ScriptableObject类
/// </summary>
/// <param name="node"></param>
public void DeleteNode(Node node)
{
nodes.Remove(node);
AssetDatabase.RemoveObjectFromAsset(node);
AssetDatabase.SaveAssets();
}

/// <summary>
/// 在ScriptableObject中添加子节点
/// </summary>
/// <param name="parent"></param>
/// <param name="child"></param>
public void AddChild(Node parent, Node child)
{
RootNode rootNode = parent as RootNode;
if (rootNode)
{
rootNode.child = child;
}

CompositeNode composite = parent as CompositeNode;
if (composite)
{
composite.children.Add(child);
}

DecoratorNode decorator = parent as DecoratorNode;
if (decorator)
{
decorator.child = child;
}
}

public void RemoveChild(Node parent, Node child)
{
RootNode rootNode = parent as RootNode;
if (rootNode)
{
rootNode.child = null;
}

CompositeNode composite = parent as CompositeNode;
if (composite)
{
composite.children.Remove(child);
}

DecoratorNode decorator = parent as DecoratorNode;
if (decorator)
{
decorator.child = null;
}
}

public List<Node> GetChildren(Node parent)
{
List<Node> children = new List<Node>();

RootNode rootNode = parent as RootNode;
if (rootNode && rootNode.child != null)
{
children.Add(rootNode.child);
}

CompositeNode composite = parent as CompositeNode;
if (composite)
{
children = composite.children;
}

DecoratorNode decorator = parent as DecoratorNode;
if (decorator && decorator.child != null)
{
children.Add(decorator.child);
}
return children;
}
#endregion
}
}

Node

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
64
65
66
67
68
69
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 行为树节点抽象类,所有节点都要继承这个类
/// </summary>
public abstract class Node : ScriptableObject
{
public enum State
{
Running,
Failure,
Success
}

/// <summary>
/// 标志,用来判断执行OnStart还是OnStop
/// </summary>
private bool started = false;
/// <summary>
/// 当前节点的状态
/// </summary>
[HideInInspector] public State state = State.Running;
/// <summary>
/// guid,节点唯一标识
/// </summary>
[HideInInspector] public string guid;
/// <summary>
/// 记录element元素在视图中的位置
/// </summary>
[HideInInspector] public Vector2 position;

public State Update()
{
if (!started)
{
OnStart();
started = true;
}

state = OnUpdate();

if (state == State.Failure || state == State.Success)
{
OnStop();
started = false;
}

return state;
}

/// <summary>
/// 克隆节点
/// 如果不同的怪使用了同一个ScriptableObject行为树,那么他们之间运行的时候会相互影响
/// 可以克隆一遍节点
/// </summary>
/// <returns></returns>
public virtual Node Clone()
{
return Instantiate(this);
}

protected abstract void OnStart();
protected abstract void OnStop();
protected abstract State OnUpdate();
}
}

ActionNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 动作节点
/// 执行动作
/// 没有子节点
/// </summary>
public abstract class ActionNode : Node
{

}
}

CompositeNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 复合节点
/// 类似控制流,for if等
/// 多个子节点
/// </summary>
public abstract class CompositeNode : Node
{
//[HideInInspector]
public List<Node> children = new List<Node>();

public override Node Clone()
{
CompositeNode node = Instantiate(this);
node.children = children.ConvertAll(c => c.Clone());
return node;
}
}
}

DecoratorNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.VisualScripting.Metadata;


namespace BehaviourTree
{
/// <summary>
/// 装饰器节点
///
/// 一个子节点
/// </summary>
public abstract class DecoratorNode : Node
{
//[HideInInspector]
public Node child;


public override Node Clone()
{
DecoratorNode node = Instantiate(this);
node.child = child.Clone();
return node;
}
}
}

RootNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
public class RootNode : Node
{
[HideInInspector] public Node child;
protected override void OnStart()
{

}

protected override void OnStop()
{

}

protected override State OnUpdate()
{
return child.Update();
}
public override Node Clone()
{
RootNode node = Instantiate(this);
node.child = child.Clone();
return node;
}
}
}

DebugLogNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
public class DebugLogNode : ActionNode
{
public string startMessage;
public string stopMessage;
public string updateMessage;
protected override void OnStart()
{
Debug.Log($"OnStart{startMessage}");
}

protected override void OnStop()
{
Debug.Log($"OnStop{stopMessage}");
}

protected override State OnUpdate()
{
Debug.Log($"OnUpdate{updateMessage}");
return State.Success;
}
}
}

WaitNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
public class WaitNode : ActionNode
{
[SerializeField] private float duration = 1;
float startTime;
protected override void OnStart()
{
startTime = Time.time;
}

protected override void OnStop()
{

}

protected override State OnUpdate()
{
if (Time.time - startTime > duration)
{
return State.Success;
}
return State.Running;
}
}
}

SequencerNode

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
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 定序节点
/// 按顺序执行每个子节点
/// </summary>
public class SequencerNode : CompositeNode
{
private int current;
protected override void OnStart()
{
current = 0;
}

protected override void OnStop()
{

}

protected override State OnUpdate()
{
//foreach (var item in children)
//{
// state = item.Update();
// if (state == Node.State.Failure)
// return state;
//}
//return state;

// 上面的方法是将子节点在这一帧中执行完:执行到一个失败的节点,后续将不在执行
// 下面的方法是将子节点的分开逐帧执行:不管怎么样都会把所有节点执行完

var child = children[current];
switch (child.Update())
{
case State.Running:
return State.Running;
case State.Failure:
return State.Failure;
case State.Success:
current++;
break;
default:
break;
}
return current == children.Count ? State.Success : State.Running;
}
}
}

RepeatNode

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BehaviourTree
{
/// <summary>
/// 重复执行节点
/// 重复执行子节点
/// 继承DecoratorNode一个子节点
/// </summary>
public class RepeatNode : DecoratorNode
{
protected override void OnStart()
{

}

protected override void OnStop()
{

}

protected override State OnUpdate()
{
child.Update();
return Node.State.Running;
}
}
}

BehaviourTreeEditor

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
64
65
66
67
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;


namespace BehaviourTree
{
/// <summary>
/// 行为树视图
/// </summary>
public class BehaviourTreeEditor : EditorWindow
{
BehaviourTreeView treeView;
InspectorView inspectorView;

[SerializeField]
private VisualTreeAsset m_VisualTreeAsset = default;

[MenuItem("BehaviourTreeEditor/Editor ...")]
public static void OpenWindow()
{
BehaviourTreeEditor wnd = GetWindow<BehaviourTreeEditor>();
wnd.titleContent = new GUIContent("BehaviourTreeEditor");
}

public void CreateGUI()
{
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;

//// Instantiate UXML
//VisualElement labelFromUXML = m_VisualTreeAsset.Instantiate();
//root.Add(labelFromUXML);
m_VisualTreeAsset.CloneTree(root);

var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/BehaviourTree/BehaviourTreeEditor.uss");
root.styleSheets.Add(styleSheet);

treeView = root.Q<BehaviourTreeView>();
inspectorView = root.Q<InspectorView>();
treeView.OnNodeSelected = OnNodeSelectionChanged;

// 确保我们在选中ScriptableObject时打开视图,能直接将选中的内容显示到视图中
OnSelectionChange();
}

/// <summary>
/// 选择Hierarchy和Project时会执行该方法
/// </summary>
private void OnSelectionChange()
{
BehaviourTree tree = Selection.activeObject as BehaviourTree;
// 选中的是行为树,更改视图中所展示的内容,变成最新选中的行为树
// 后半部分的判断是防止,在创建新的行为树,但unity还没准备好就选择,会报错
if (tree && AssetDatabase.CanOpenAssetInEditor(tree.GetInstanceID()))
{
treeView.PopulateView(tree);
}
}

private void OnNodeSelectionChanged(NodeView nodeView)
{
inspectorView.UpdateSelection(nodeView);
}
}
}

SplitView

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine.UIElements;


/// <summary>
/// 将窗口左右分成两半
/// </summary>
public class SplitView : TwoPaneSplitView
{
public new class UxmlFactory : UxmlFactory<SplitView, TwoPaneSplitView.UxmlTraits> { }
}

InspectorView

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
using UnityEditor;
using UnityEngine.UIElements;


namespace BehaviourTree
{
/// <summary>
/// 行为树视图左边区域
/// </summary>
public class InspectorView : VisualElement
{
public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }

private Editor editor;

public InspectorView()
{

}

/// <summary>
/// 显示当前选中的节点元素的Inspector内容
/// </summary>
/// <param name="nodeView"></param>
internal void UpdateSelection(NodeView nodeView)
{
Clear();

UnityEngine.Object.DestroyImmediate(editor);
editor = Editor.CreateEditor(nodeView.node);
// 创建一个容器,并将节点的Inspector窗口上的内容显示到左侧窗口
IMGUIContainer container = new IMGUIContainer(() => { editor.OnInspectorGUI(); });
Add(container);
}
}
}

BehaviourTreeView

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using System;


namespace BehaviourTree
{
/// <summary>
/// 行为树视图右边区域
/// </summary>
public class BehaviourTreeView : GraphView
{
public Action<NodeView> OnNodeSelected;
public new class UxmlFactory : UxmlFactory<BehaviourTreeView, GraphView.UxmlTraits> { }

BehaviourTree tree;
public BehaviourTreeView()
{
Insert(0, new GridBackground());

// 可缩放
this.AddManipulator(new ContentZoomer());
// 可拖动
this.AddManipulator(new ContentDragger());
// 可选择
this.AddManipulator(new SelectionDragger());
// 可框中选择
this.AddManipulator(new RectangleSelector());

var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/BehaviourTree/BehaviourTreeEditor.uss");
styleSheets.Add(styleSheet);
}

/// <summary>
/// 将ScriptableObject中的内容显示到视图中,并注册委托:在视图中发生更改时执行`OnGraphViewChanged`方法
/// </summary>
/// <param name="tree"></param>
internal void PopulateView(BehaviourTree tree)
{
this.tree = tree;

graphViewChanged -= OnGraphViewChanged;
// 删除视图中所有的内容
DeleteElements(graphElements);
graphViewChanged += OnGraphViewChanged;

// 在打开视图时,没有根节点(比如刚创建的新行为树),就创建一个根节点并赋值给行为树
if (tree.rootNode == null)
{
tree.rootNode = tree.CreateNode(typeof(RootNode)) as RootNode;
// 修改一个prefab的MonoBehaviour或ScriptableObject变量,必须告诉Unity该值已经改变。
// 每当一个属性发生变化,Unity内置组件在内部调用setDirty。
// MonoBehaviour或ScriptableObject不自动做这个,因此如果你想值被保存,必须调用SetDirty。
EditorUtility.SetDirty(tree);
AssetDatabase.SaveAssets();
}

// 逐一创建每个节点
tree.nodes.ForEach(n => CreateNodeView(n));

// 逐一连接每个节点
tree.nodes.ForEach(n =>
{
var children = tree.GetChildren(n);
NodeView parentView = FindNodeView(n);
children.ForEach(c =>
{
NodeView childView = FindNodeView(c);

Edge edge = parentView.output.ConnectTo(childView.input);
AddElement(edge);
});
});
}

/// <summary>
/// 通过节点的guid查找与其对应的节点元素
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private NodeView FindNodeView(Node node)
{
return GetNodeByGuid(node.guid) as NodeView;
}

/// <summary>
/// 重写父类方法
/// 该方法能让视图中的节点相互连接,如果不写该方法将无法连接
/// </summary>
/// <param name="startPort"></param>
/// <param name="nodeAdapter"></param>
/// <returns></returns>
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
//return base.GetCompatiblePorts(startPort, nodeAdapter);
return ports.ToList().Where(endPort =>
endPort.direction != startPort.direction &&
endPort.node != startPort.node).ToList();
}

/// <summary>
/// 每当视图有变化时就会调用此方法
/// </summary>
/// <param name="graphViewChange"></param>
/// <returns></returns>
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
// 如果正在移除的元素不为空
if (graphViewChange.elementsToRemove != null)
{
graphViewChange.elementsToRemove.ForEach(elem =>
{
// 在移除该元素的同时,移除ScriptableObject中对应的节点
NodeView nodeView = elem as NodeView;
if (nodeView != null)
{
tree.DeleteNode(nodeView.node);
}

// 移除的同时,移除他的父级的child
Edge edge = elem as Edge;
if (edge != null)
{
NodeView parentView = edge.output.node as NodeView;
NodeView childView = edge.input.node as NodeView;

tree.RemoveChild(parentView.node, childView.node);
}
});
}

// 当连线的同时,在ScriptableObject中添加子节点
if (graphViewChange.edgesToCreate != null)
{
graphViewChange.edgesToCreate.ForEach(edge =>
{
NodeView parentView = edge.output.node as NodeView;
NodeView childView = edge.input.node as NodeView;
tree.AddChild(parentView.node, childView.node);
});
}

return graphViewChange;
}

/// <summary>
/// 添加右键菜单内容
/// </summary>
/// <param name="evt"></param>
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
//base.BuildContextualMenu(evt);
{
var types = TypeCache.GetTypesDerivedFrom<ActionNode>(); // 返回派生类的无序集合
foreach (var type in types)
{
// 将查找到的类赋给菜单,并绑定创建事件
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (a) => CreateNode(type));
}
}
{
var types = TypeCache.GetTypesDerivedFrom<CompositeNode>();
foreach (var type in types)
{
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (a) => CreateNode(type));
}
}
{
var types = TypeCache.GetTypesDerivedFrom<DecoratorNode>();
foreach (var type in types)
{
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (a) => CreateNode(type));
}
}
}

/// <summary>
/// 在行为树中创建节点,并在视图中创建elements节点元素
/// </summary>
/// <param name="type"></param>
private void CreateNode(System.Type type)
{
Node node = tree.CreateNode(type);

CreateNodeView(node);
}

/// <summary>
/// 在视图中创建elements节点元素
/// </summary>
/// <param name="node"></param>
private void CreateNodeView(Node node)
{
NodeView nodeView = new NodeView(node);
nodeView.OnNodeSelected = OnNodeSelected;
AddElement(nodeView);
}
}
}

NodeView

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using UnityEngine;
using UnityEditor.Experimental.GraphView;
using System;


namespace BehaviourTree
{
public class NodeView : UnityEditor.Experimental.GraphView.Node
{
public Action<NodeView> OnNodeSelected;
public Node node;
public Port input;
public Port output;

public NodeView(Node node)
{
this.node = node;
this.title = node.name;
this.viewDataKey = node.guid;

style.left = node.position.x;
style.top = node.position.y;

CreateInputPorts();
CreateOutputPorts();
}

/// <summary>
/// 创建节点的接口和出口
/// </summary>
private void CreateInputPorts()
{
// 注册输入
if (node is ActionNode)
{
// 水平 方向 单个 类型
input = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is CompositeNode)
{
input = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is DecoratorNode)
{
input = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is RootNode)
{

}

// 设置属性
if (input != null)
{
input.portName = "";
inputContainer.Add(input);
}
}

private void CreateOutputPorts()
{
if (node is ActionNode)
{

}
else if (node is CompositeNode)
{
output = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(bool));
}
else if (node is DecoratorNode)
{
output = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool));
}
else if (node is RootNode)
{
output = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool));
}

if (output != null)
{
output.portName = "";
outputContainer.Add(output);
}
}

/// <summary>
/// 重写父类方法
/// 在移动该节点元素后,将其新的位置信息存储到SCriptableObject节点中
/// </summary>
/// <param name="newPos"></param>
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
// UnityEngine.Rect 无法直接转换成Vector2,需要使用Rect的方法
// 保存位置信息到ScriptableObject
node.position.x = newPos.xMin;
node.position.y = newPos.yMin;
}

/// <summary>
/// 重写父类方法
/// 定义一个委托,在窗口中选择该节点元素后,执行这个委托,每个节点元素在创建的时候就会注册这个委托了。
/// 但是注册这个委托的又是另外一个委托(我们称前一个委托为委托A,后一个委托为委托B),
/// 委托B是在创建行为树窗口时就注册了,注册实现的内容是:将该节点元素的节点Inspector信息映射到左侧的InspeView视图中。
/// 看起来比较绕,但其实就是为了保持代码的可维护性,
/// 没有让InspectorView的实例到出些,而为了让NodeView中的方法与BehaviourTreeEditor中的InspectorView实例联系起来而用了两个委托
/// </summary>
public override void OnSelected()
{
base.OnSelected();
if (OnNodeSelected != null)
{
OnNodeSelected.Invoke(this);
}
}
}
}

BehaviourTreeEditor.uxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Assets/Editor/BehaviourTreeEditor.uss?fileID=7433441132597879392&amp;guid=29b7713829d781d489b826020f33285b&amp;type=3#BehaviourTree.BehaviourTreeEditor" />
<uie:Toolbar>
<uie:ToolbarMenu tabindex="-1" parse-escape-sequences="true" display-tooltip-when-elided="true" text="Assets" />
</uie:Toolbar>
<SplitView fixed-pane-initial-dimension="300">
<ui:VisualElement name="left-panel" style="flex-grow: 1; -unity-slice-scale: 0;">
<ui:Label tabindex="-1" text="Inspector" parse-escape-sequences="true" display-tooltip-when-elided="true" style="-unity-background-image-tint-color: rgb(60, 60, 60); background-color: rgb(37, 37, 37); color: rgb(196, 196, 196); -unity-slice-scale: 1px; flex-shrink: 0; flex-grow: 0; font-size: 14px; -unity-text-align: upper-left; white-space: nowrap; text-overflow: clip; -unity-background-scale-mode: stretch-to-fill; border-top-width: 0; -unity-text-outline-width: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-left: 0; padding-bottom: 0; border-left-width: 5px;" />
<BehaviourTree.InspectorView style="flex-grow: 1; background-color: rgb(56, 56, 56);" />
</ui:VisualElement>
<ui:VisualElement name="right-panel" style="flex-grow: 1;">
<ui:Label tabindex="-1" text="TreeView" parse-escape-sequences="true" display-tooltip-when-elided="true" style="background-color: rgb(37, 37, 37); color: rgb(196, 196, 196); margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-left-width: 5px;" />
<BehaviourTree.BehaviourTreeView focusable="true" style="flex-grow: 1;" />
</ui:VisualElement>
</SplitView>
</ui:UXML>

BehaviourTreeEditor.uss

1
2
3
4
5
6
GridBackground {
--grid-background-color: rgb(40, 40, 40);
--line-color: rgba(193, 196, 192, 0.1);
--thick-line-color: rgba(193, 196, 192, 0.1);
--spacing: 15;
}