Behavior Tree Editor Base 생성
BehaviorTree UXML, USS 추가
BehaviorTreeView,
BehaviorTreeEditor,
InspectorView,
SplitView
BehaviorTree Editor 에 InspectorView 와 SplitView를 넣고 BackGround 및 Label, Size 조정.
BT Editor 에서 오른쪽 마우스 클릭으로 노드들을 생성하고, 알트+델리트 로 삭제할 수 있다.
오른쪽 마루스 클릭으로 생성시, 베이스 타입과 노드의 이름이 나온다.
각 노드들의 정보가 인스펙터창에 나온다.
배치된 노드들의 위치가 저장된다.
BT Editor 에서 노드간 연결(Edge) 가능
input/output 으로 연결을 하는데
자식을 가질 수 없는 Action 은 output이 없고,
Decorator 는 하나의 output만 가능,
Composite 는 여러개의 output이 가능
input은 모두 하나씩만 가능하다.
연결(Edge)된 노드들은 자동으로 하위 자식으로 들어간다. (해당 SO Inspector 창에서 확인 가능)
연결(Edge)된 노드들의 정보를 연결 할 때마다 저장. (다른 작업을 하고 오거나 껏다 켜도 유지가 된다.)
Editor 상에서 Node들의 Inspector 확인이 가능
Editor의 Inspector는 매번 노드를 클릭 할 때마다 지우고 정보를 다시 업데이트.
Node의 State, started, guid, position은 HideInInspector (필요시 공개 가능)
BT SO 생성시 Root 자동 생성
자동으로 BT SO의 RootNode 변수로 할당됨.
Root는 유일함. (부모를 가질 수 없음)
모든 행동 트리의 시작은 Root로 시작됨.
Editor 에 행동 트리를 배치하고 런타임에 SO들의 Clone들을 Instantiate로 생성
하나의 SO를 공유해서 사용하기 위해서는 그대로 사용하면 한 명의 AI가 다른 반환을 하면 전체가 영향을 받음(Shallow Copy).
그래서 Instantiate로 각자 Clone 행동 트리를 생성하여 할당하여(Deep Copy) 객체마다 개별적인 독립성 보장.
[CreateAssetMenu()]
public class BehaviorTree : ScriptableObject
{
/*===========================================================================================================*/
public Node rootNode;
public Node.E_NodeState treeState = Node.E_NodeState.Running;
public List<Node> nodes = new List<Node>();
/*===========================================================================================================*/
public Node.E_NodeState Update()
{
if (rootNode.state == Node.E_NodeState.Running)
{
treeState = rootNode.Update();
}
return treeState;
}
// 노드 객체 생성
public Node CreateNode(System.Type type)
{
Node node = ScriptableObject.CreateInstance(type) as Node;
if (node == null)
Debug.LogError("node CreateInstance failed");
node.name = type.Name;
node.guid = GUID.Generate().ToString();
nodes.Add(node);
AssetDatabase.AddObjectToAsset(node, this);
AssetDatabase.SaveAssets();
return node;
}
// 노드 삭제
public void DeleteNode(Node node)
{
nodes.Remove(node);
AssetDatabase.RemoveObjectFromAsset(node);
AssetDatabase.SaveAssets();
}
// 입력 부모 타입에 대한 자식 추가
public void AddChild(Node parent, Node child)
{
Root root = parent as Root;
if (root)
root.child = child;
Decorator decorator = parent as Decorator;
if (decorator)
decorator.child = child;
Composite composite = parent as Composite;
if (composite)
composite.children.Add(child);
}
// 입력 부모 타입에 대한 자식 제거
public void RemoveChild(Node parent, Node child)
{
Root root = parent as Root;
if (root)
root.child = null;
Decorator decorator = parent as Decorator;
if (decorator)
decorator.child = null;
Composite composite = parent as Composite;
if (composite)
composite.children.Remove(child);
}
// 입력 부모에 대한 자식 리스트 반환
public List<Node> GetChildren(Node parent)
{
List<Node> children = new List<Node>();
Root root = parent as Root;
if (root && root.child != null)
children.Add(root.child);
Decorator decorator = parent as Decorator;
if (decorator && decorator.child != null)
children.Add(decorator.child);
Composite composite = parent as Composite;
if (composite)
return composite.children;
return children;
}
public BehaviorTree Clone()
{
BehaviorTree tree = Instantiate(this);
tree.rootNode = tree.rootNode.Clone();
return tree;
}
}
public class BehaviorTreeRunner : MonoBehaviour
{
/*===========================================================================================================*/
public BehaviorTree tree;
/*===========================================================================================================*/
void Start()
{
tree = tree.Clone();
}
void Update()
{
tree.Update();
}
}
public class BehaviorTreeEditor : EditorWindow
{
/*===========================================================================================================*/
private BehaviorTreeView _treeView;
private InspectorView _inspectorView;
/*===========================================================================================================*/
[MenuItem("BehaviorTree/BehaviorTree Editor")]
public static void OpenWindow()
{
BehaviorTreeEditor wnd = GetWindow<BehaviorTreeEditor>();
wnd.titleContent = new GUIContent("BehaviorTreeEditor");
}
public void CreateGUI()
{
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;
// Instantiate UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/__Scripts__/__Core__/Behavior Tree/Editor/BehaviorTreeEditor.uxml");
visualTree.CloneTree(root);
// Allocate StyleSheet
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/__Scripts__/__Core__/Behavior Tree/Editor/BehaviorTreeEditor.uss");
root.styleSheets.Add(styleSheet);
_treeView = root.Q<BehaviorTreeView>();
_inspectorView = root.Q<InspectorView>();
_treeView.OnNodeSelected = OnNodeSelectionChanged;
OnSelectionChange();
}
// 선택시 변경 이벤트 함수
private void OnSelectionChange()
{
BehaviorTree tree = Selection.activeObject as BehaviorTree;
if (tree && AssetDatabase.CanOpenAssetInEditor(tree.GetInstanceID()))
{
_treeView.PopulateView(tree);
}
}
// 노드가 선택될 때 호출, Inspector View를 입력받은 node 정보로 업데이트.
private void OnNodeSelectionChanged(NodeView node)
{
_inspectorView.UpdateSelection(node);
}
}
public class BehaviorTreeView : GraphView
{
/*===========================================================================================================*/
public Action<NodeView> OnNodeSelected;
private BehaviorTree _tree;
/*===========================================================================================================*/
public new class UxmlFactory : UxmlFactory<BehaviorTreeView, GraphView.UxmlTraits> { }
/*===========================================================================================================*/
public BehaviorTreeView()
{
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/__Scripts__/__Core__/Behavior Tree/Editor/BehaviorTreeEditor.uss");
styleSheets.Add(styleSheet); // 스타일 시트 직접참조
}
/*===========================================================================================================*/
// GUID(고유 식별자)를 사용하여 파라미터에 해당하는 NodeView 반환.
NodeView FindNodeView(Node node)
{
return GetNodeByGuid(node.guid) as NodeView;
}
// 파라미터 tree 객체를 사용해 그래프뷰를 채운다.
internal void PopulateView(BehaviorTree tree)
{
_tree = tree;
graphViewChanged -= OnGraphViewChanged;
DeleteElements(graphElements); // 두 개 이상 생성 대비 삭제
graphViewChanged += OnGraphViewChanged;
// RootNode 생성, 하나의 RootNode 보장
if (tree.rootNode == null)
{
tree.rootNode = tree.CreateNode(typeof(Root)) as Root; // 생성후 트리의 루트로 설정. (다운캐스팅)(.GetType() : 실제 자신을 반환)
EditorUtility.SetDirty(tree);
AssetDatabase.SaveAssets();
}
// Creates Node View
_tree.nodes.ForEach(n => CreateNodeView(n)); // 다시 생성
// 각 노드에 대해 자식 노드를 얻고, 각 노드에 대한 연결(Edge) 생성 후 보모-자식 노드의 출입력 포트 연결 후 그래프 뷰에 추가.
// Create Edges
_tree.nodes.ForEach(n =>
{
var children = tree.GetChildren(n);
children.ForEach(c =>
{
NodeView parentView = FindNodeView(n);
NodeView childView = FindNodeView(c);
Edge edge = parentView.outputPort.ConnectTo(childView.inputPort);
AddElement(edge);
});
});
}
/*===========================================================================================================*/
// 노드 입출력 호환 검사
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
return ports.ToList()
.Where(endPort => endPort.direction != startPort.direction && endPort.node != startPort.node).ToList();
}
/*===========================================================================================================*/
// 뷰 체인지 이벤트 함수
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
if (graphViewChange.elementsToRemove != null)
{
graphViewChange.elementsToRemove.ForEach(element =>
{
if (element is NodeView nodeView)
_tree.DeleteNode(nodeView.node);
// 연결된 노드 자식 삭제
if (element is Edge edge)
{
NodeView parentView = edge.output.node as NodeView;
NodeView childView = edge.input.node as NodeView;
_tree.RemoveChild(parentView.node, childView.node);
}
});
}
// 생성된 Edge가 있다면, 각 Edge에 대한 연결된 노드 간의 부모-자식 관계 추가
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;
}
/*===========================================================================================================*/
// 메뉴 재정의
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
{
var types = TypeCache.GetTypesDerivedFrom<LeafAction>();
foreach (var type in types)
{
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (t) => CreateNode(type));
}
}
{
var types = TypeCache.GetTypesDerivedFrom<Composite>();
foreach (var type in types)
{
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (t) => CreateNode(type));
}
}
{
var types = TypeCache.GetTypesDerivedFrom<Decorator>();
foreach (var type in types)
{
evt.menu.AppendAction($"[{type.BaseType.Name}] {type.Name}", (t) => CreateNode(type));
}
}
}
/*===========================================================================================================*/
private void CreateNode(System.Type type)
{
Node node = _tree.CreateNode(type);
CreateNodeView(node);
}
private void CreateNodeView(Node node)
{
NodeView nodeView = new NodeView(node);
nodeView.OnNodeSelected = OnNodeSelected;
AddElement(nodeView);
}
/*===========================================================================================================*/
}
public class InspectorView : VisualElement
{
private Editor _editor;
public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> {}
public InspectorView()
{
}
// 다른 노드를 선택했을 때 호출, 현재 정보를 지우고 새로 생성.
internal void UpdateSelection(NodeView nodeView)
{
Clear();
UnityEngine.Object.DestroyImmediate(_editor);
_editor = Editor.CreateEditor(nodeView.node);
IMGUIContainer container = new IMGUIContainer(() => { _editor.OnInspectorGUI(); });
Add(container);
}
}
public class NodeView : UnityEditor.Experimental.GraphView.Node
{
/*===========================================================================================================*/
public Action<NodeView> OnNodeSelected;
public Node node;
public Port inputPort;
public Port outputPort;
/*===========================================================================================================*/
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;
CreateInputPort();
CreateOutputPort();
}
private void CreateInputPort()
{
if (node is LeafAction)
{
inputPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is Composite)
{
inputPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is Decorator)
{
inputPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(bool));
}
else if (node is Root)
{
}
if (inputPort != null)
{
inputPort.portName = "";
inputContainer.Add(inputPort);
}
}
private void CreateOutputPort()
{
if (node is LeafAction)
{
}
else if (node is Composite)
{
outputPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(bool));
}
else if (node is Decorator)
{
outputPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool));
}
else if (node is Root)
{
outputPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool));
}
if (outputPort != null)
{
outputPort.portName = "";
outputContainer.Add(outputPort);
}
}
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
node.position.x = newPos.xMin;
node.position.y = newPos.yMin;
}
public override void OnSelected()
{
base.OnSelected();
OnNodeSelected?.Invoke(this);
}
}
public abstract class Node : ScriptableObject
{
public enum E_NodeState
{
Running,
Success,
Failure
}
[HideInInspector]public E_NodeState state = E_NodeState.Running;
[HideInInspector]public bool started = false;
[HideInInspector]public string guid;
[HideInInspector]public Vector2 position;
public E_NodeState Update()
{
if (!started)
{
OnStart();
started = true;
}
state = OnUpdate();
if (state != E_NodeState.Running)
{
OnStop();
started = false;
}
return state;
}
public virtual Node Clone()
{
return Instantiate(this);
}
protected abstract void OnStart();
protected abstract void OnStop();
protected abstract E_NodeState OnUpdate();
}
public class Root : Node
{
public Node child;
protected override void OnStart()
{
}
protected override void OnStop()
{
}
protected override E_NodeState OnUpdate()
{
return child.Update();
}
public override Node Clone()
{
Root node = Instantiate(this);
node.child = child.Clone();
return node;
}
}