Unity 최종 프로젝트 - 24 (Behavior Tree Editor 제작-2)

이준호·2024년 2월 14일
0
post-custom-banner

📌 Unity 최종 프로젝트



📌 Behavior Tree Graph View Editor 제작

➔ Commit Record & Explanation




UI Builder Editor Window

  • Behavior Tree Editor Base 생성

  • BehaviorTree UXML, USS 추가

  • BehaviorTreeView,
    BehaviorTreeEditor,
    InspectorView,
    SplitView

  • BehaviorTree Editor 에 InspectorView 와 SplitView를 넣고 BackGround 및 Label, Size 조정.




BT Editor Nodes Create & Delete

  • BT Editor 에서 오른쪽 마우스 클릭으로 노드들을 생성하고, 알트+델리트 로 삭제할 수 있다.

  • 오른쪽 마루스 클릭으로 생성시, 베이스 타입과 노드의 이름이 나온다.

  • 각 노드들의 정보가 인스펙터창에 나온다.

  • 배치된 노드들의 위치가 저장된다.




BT Editor Creating Edges

  • BT Editor 에서 노드간 연결(Edge) 가능

  • input/output 으로 연결을 하는데
    자식을 가질 수 없는 Action 은 output이 없고,
    Decorator 는 하나의 output만 가능,
    Composite 는 여러개의 output이 가능

  • input은 모두 하나씩만 가능하다.

  • 연결(Edge)된 노드들은 자동으로 하위 자식으로 들어간다. (해당 SO Inspector 창에서 확인 가능)

  • 연결(Edge)된 노드들의 정보를 연결 할 때마다 저장. (다른 작업을 하고 오거나 껏다 켜도 유지가 된다.)




BT Editor Inspector View

  • Editor 상에서 Node들의 Inspector 확인이 가능

  • Editor의 Inspector는 매번 노드를 클릭 할 때마다 지우고 정보를 다시 업데이트.

  • Node의 State, started, guid, position은 HideInInspector (필요시 공개 가능)




RootNode Setting

  • BT SO 생성시 Root 자동 생성

  • 자동으로 BT SO의 RootNode 변수로 할당됨.

  • Root는 유일함. (부모를 가질 수 없음)

  • 모든 행동 트리의 시작은 Root로 시작됨.




Editor Node Instanting Clone

  • Editor 에 행동 트리를 배치하고 런타임에 SO들의 Clone들을 Instantiate로 생성

  • 하나의 SO를 공유해서 사용하기 위해서는 그대로 사용하면 한 명의 AI가 다른 반환을 하면 전체가 영향을 받음(Shallow Copy).
    그래서 Instantiate로 각자 Clone 행동 트리를 생성하여 할당하여(Deep Copy) 객체마다 개별적인 독립성 보장.






➔ Editor Image






➔ Editor Code

BehaviorTree.cs

[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;
    }
}

BehaviorTreeRunner.cs

public class BehaviorTreeRunner : MonoBehaviour
{
    /*===========================================================================================================*/
    
    public BehaviorTree tree;
    
    /*===========================================================================================================*/
    
    void Start()
    {
        tree = tree.Clone();
    }
    
    void Update()
    {
        tree.Update();
    }
}

BehaviorTreeEditor.cs

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

BehaviorTreeView.cs

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);
    }
    
    /*===========================================================================================================*/
}

InspectorView.cs

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

NodeView.cs

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

Node.cs

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

Root.cs

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











📌 출저

TheKiwiCoder

profile
No Easy Day
post-custom-banner

0개의 댓글