Unity 최종 프로젝트 - 22 (GraphView Custom Editor - 심화 분석 下)

이준호·2024년 2월 12일
0

📌 Unity 최종 프로젝트



📌 Behavior Tree GraphView Editor 코드 심화 분석

➔ InspectorView.cs

/*
 * 이 코드는 Unity 에디터의 일부로 동작하는 'InspectorView' 클래스를 정의한다.
 * 'InspectorView' 는 'VisualElement' 를 상속받아 구현되며, Unity 에디터에서 커스텀 인스펙터 뷰를 구현하는 데 사용된다.
 * 이 클래스는 주로 행동 트리 에디터와 같은 사용자 정의 에디터 환경에서 선택된 노드의 세부 정보를 표시하는 데 사용된다.
 *
 * 사용자 정의 에디터 환경에서 선택된 객체의 세부 정보를 표시하고 편집하는 기능을 제공하는 핵심 요소이다.
 * 이를 통해 개발자는 Unity의 기본 인스펙터와 유사한 사용자 경험을 커스텀 에디터 내에서 재현할 수 있다.
 */
namespace TheKiwiCoder {
    /// <summary>
    /// 'InspectorView' 클래스는 사용자가 행동 트리 에디터 내에서 노드를 선택했을 때, 해당 노드의 속성을 표시하는 인스펙터 뷰의 역할을 한다.
    /// 이는 Unity의 기본 인스펙터 기능을 모방하여, 선택된 객체의 세부 사항을 수정할 수 있는 인터페이스를 제공한다.
    /// </summary>
    public class InspectorView : VisualElement {
        
        /*=======================================================================================================*/
        
        public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
        // 'UxmlFactory<InspectorView, VisualElement.UxmlTraits>' 를 상속받는 'UxmlFactory' 내부 클래스는 'InspectorView' 를 UI Builder에서 사용할 수 있게 한다.
        // 이를 통해 'InspectorView' 의 인스턴스를 UXML 파일을 통해 선언적으로 생성할 수 있다.
        
        /*=======================================================================================================*/
        
        // 선택된 노드를 위한 'Editor' 인스턴스이다. 이 'Editor' 객체는 선택된 노드의 속성을 인스펙터 UI에 표시하고 편집할 수 있게 해준다.
        Editor editor;
        
        /*=======================================================================================================*/
        
        // 'InspectorView' 의 생성자는 기본적으로 비어있다.
        // 이 클래스의 인스턴스가 생성될 때 특별한 초기화 작업은 필요하지 않으며, 주된 기능은 선택된 노드를 인스펙터 뷰에 표시하는 것이다.
        public InspectorView() {

        }
        // 생성자는 현재 비어 있으며, 이는 'InspectorView' 의 인스턴스가 생성될 때 특별한 초기화 작업이 필요하지 않음을 의미한다.
        // 클래스의 주된 기능은 'UpdateSelection' 메서드에 의해 수행된다.
        
        /*=======================================================================================================*/
        
        // 사용자가 행동 트리 에디터 내에서 다른 노드를 선택했을 때 호출된다.
        internal void UpdateSelection(NodeView nodeView) {
            //현재 인스펙터 뷰 클리어
            Clear();
            // 'Clear()' 메서드를 호출하여 'InspectorView' 내의 모든 콘텐츠를 제거한다.
            // 이는 새로운 노드가 선택될 때마다 이전에 표시된 정보를 초기화하기 위함이다.

            //이전 에디터 객체 제거
            UnityEngine.Object.DestroyImmediate(editor);
            // 만약 이전에 생성된 'Editor' 객체가 있다면, 'UnityEngine.Object.DestroyImmediate(editor)' 를 호출하여 즉시 제거한다.
            // 이는 새로운 노드에 대한 새 'Editor' 인스턴스를 생성하기 전에 이전 인스턴스를 정리하는 데 필요하다.

            //새 에디터 생성
            editor = Editor.CreateEditor(nodeView.node);
            // 'Editor.CreateEditor(nodeView.node)' 를 호출하여 현재 선택된 노드(nodeView.node)를 위한 새 'Editor' 인스턴스를 생성한다.
            // 이 'Editor' 객체는 선택된 노드의 속성을 인스펙터 UI에 표시하고 편집할 수 있게 해준다.
            //IMGUIContainer를 통한 UI표시
            IMGUIContainer container = new IMGUIContainer(() => {
                if (editor && editor.target) {
                    editor.OnInspectorGUI();
                }
            });
            // 'IMGUIContainer' 를 생성하고, 그 안에 람다 함수를 정의하여 'editor.OnInspectorGUI()' 를 호출한다.
            // 이 호출은 'editor' 가 가지고 있는 노드의 속성을 IMGUI 방식으로 그리게 한다.
            // 생성된 'IMGUIContainer' 는 'InspectorView' 에 추가된다.
            // 이 컨테이너는 Unity의 IMGUI 시스템을 사용하여 노드의 속성을 시각적으로 표시한다.
            Add(container);
        }
        
        /*=======================================================================================================*/



➔ NodeView.cs

/*
 * 이 코드는 'NodeView' 클래스를 정의하고 있으며, Unity의 'GraphView' 시스템 내에서 사용되는 노드의 시각적 표현을 관리한다.
 * 이 클래스는 Unity의 'UnityEditor.Experimental.GraphView.Node' 클래스를 상속받아 구현된다.
 * 행동 트리 에디터 내에서 노드 간의 상호작용과 시각적으로 표현을 관리하는 핵심적인 역할을 한다.
 * 사용자가 에디터 내에서 직관적으로 노드를 조작하고, 트리 구조를 시각적으로 이해할 수 있게 돕는다.
 */
namespace TheKiwiCoder {
    
    /// <summary>
    /// 'NodeView' 클래스는 행동 트리 내의 각 노드를 그래픽 사용자 인터페이스(GUI) 상에서 표현하는 역할을 한다.
    /// 이 클래스는 노드의 상태, 타입, 이름, 위치 등의 정보를 시각적으로 나타내고, 사용자 입력에 대한 반응(예:선택, 드래그 앤 드롭)을 처리한다.
    /// </summary>
    public class NodeView : UnityEditor.Experimental.GraphView.Node {
        
        /*=======================================================================================================*/
        // 노드가 선택될 때 호출될 콜백 함수. 'Action<NodeView>' 타입으로, 'NodeView' 인스턴스를 매개변수로 받는다.
        public Action<NodeView> OnNodeSelected;
        // 이 뷰가 표현하는 'Node' 객체. 행동 트리의 구성 요소 중 하나이다.
        public Node node;
        // 'input', 'output'
        // 노드의 입력 포트와 출력 포트를 나타낸다. 이 포트들은 노드 간의 연결을 생성하는데 사용된다.
        public Port input;
        public Port output;
        
        /*=======================================================================================================*/
        
        // 생성자는 'Node' 객체를 매개변수로 받아, 노드의 시각적 표현을 초기화한다.
        // 이 과정에서 노드의 이름, 위치, 입력/출력 포트 생성, CSS 클래스 설정, 데이터 바인딩 설정 등을 수행한다.
        public NodeView(Node node) : base(AssetDatabase.GetAssetPath(BehaviourTreeSettings.GetOrCreateSettings().nodeXml)) {
            //노드 정보 초기화
            this.node = node;
            // 생성자는 'Node' 객체를 매개변수로 받아 멤버 변수 'this.node' 에 할당한다.
            //노드 이름 설정
            this.node.name = node.GetType().Name;
            // 생성된 노드의 이름을 해당 노드의 클래스 타입 이름으로 설정한다. (예: 'ActionNode' 타입의 노드 인스턴스의 경우, 노드의 이름은 'ActionNode')
            //타이틀 설정
            this.title = node.name.Replace("(Clone)", "").Replace("Node", "");
            // 노드 이름에서 "(Clone)" 및 "Node" 문자열을 제거하고 이를 'NodeView' 의 'tittle' 속성에 할당한다. (UI에서 노드를 표시할 때 더 깔끔하고 이해하기 쉬운 이름을 보기 위해, 예:ActionNode(Clone) => Action)
            //viewDataKey 설정
            this.viewDataKey = node.guid;
            // 'NodeView' 인스턴스에 고유 식별자인 'node' 의 'guid' 를 할당한다.
            // 'viewDataKey' 는 Unity의 UIElements 프레임워크에서 해당 뷰의 인스턴스를 식별하는 데 사용되는 키이다.
            // 이 고유 식별자를 통해, 시스템은 각 노드 뷰의 상태(예:선택 상태, 위치 등)를 유지하고, 세선 간에 이 상태를 복원할 수 있다.
            // 특히 사용자가 에디터를 닫았다가 다시 열 때 노드 뷰의 상태를 정확히 복원하는 데 필수이다.

            // 노드 위치 설정
            style.left = node.position.x;
            style.top = node.position.y;
            // 노드의 'position' 속성 값을 사용하여 'NodeView' 의 스타일 (style.left, style.top)을 설정한다.

            //입력/출력 포트 생성
            CreateInputPorts();
            CreateOutputPorts();
            //'CreateInputPorts' 와 'CreateOutputPorts' 메서드를 호출하여 노드의 입력 및 출력 포트를 생성하고 구성한다.
            //CSS 클래스 설정
            SetupClasses();
            // 'SetupClasses' 메서드를 호출하여 노드 타입에 따른 CSS 클래스를 동적으로 추가한다. 이는 노드의 시각적 스타일을 결정한다.
            //데이터 바인딩 설정
            SetupDataBinding();
            // 'SetupDataBinding' 메서드를 호출하여 노드의 속성(예:설명)을 UI 요소에 바인딩한다.
        }
        
        /*=======================================================================================================*/
        
        // 노드의 속성(예:설명)을 UI 요소에 바인딩하는 메서드이다. 이를 통해 노드의 데이터가 UI에 동적으로 반영된다.
        private void SetupDataBinding() {
            Label descriptionLabel = this.Q<Label>("description");
            descriptionLabel.bindingPath = "description";
            descriptionLabel.Bind(new SerializedObject(node));
        }
        // 노드의 'description' 속성을 'Label' UI요소에 바인딩하여, 노드의 설명이 UI에 동적으로 반영되도록 한다.
        // 이를 위해 'this.Q<Label>("description")' 을 호출하여 설명 라벨을 찾고, 'Bind' 메서드를 사용하여 'new SerializedObject(node)' 에 바인딩한다.
        
        /*=======================================================================================================*/
        
        // 노드 타입(Action, Composite, Decorate)에 따라 CSS 클래스(action, composite, decorator, root)를 동적으로 추가하는 메서드이다. 노드의 시각적 스타일을 결정한다.
        private void SetupClasses() {
            if (node is ActionNode) {
                AddToClassList("action");
            } else if (node is CompositeNode) {
                AddToClassList("composite");
            } else if (node is DecoratorNode) {
                AddToClassList("decorator");
            } else if (node is RootNode) {
                AddToClassList("root");
            }
        }
        // 노드 타입에 따라 해당하는 CSS 클래스를 'NodeView' 에 추가한다.
        // 이는 노드의 시각적 스타일을 타입별로 구분하기 위함이다.
        
        /*=======================================================================================================*/
        
        // CreateInputPorts, CreateOutputPorts : 노드의 입력 포트와 출력 포트를 생성한다.
        // 포트 타입과 용량은 노드의 종류에 따라 다르며, 이 메서드들은 해당 포트들을 노드 뷰에 추가한다.
        //입력 포트 구성
        private void CreateInputPorts() {
            if (node is ActionNode) {
                input = new NodePort(Direction.Input, Port.Capacity.Single);
            } else if (node is CompositeNode) {
                input = new NodePort(Direction.Input, Port.Capacity.Single);
            } else if (node is DecoratorNode) {
                input = new NodePort(Direction.Input, Port.Capacity.Single);
            } else if (node is RootNode) {

            }
            // 노드 타입에 따라 적절한 입력 포트를 생성한다. 예를 들어, 'ActionNode', 'CompositeNode', 'DecoratorNode' 는 'Single' 용량의 입력 포트를 가진다.

            if (input != null) {
                input.portName = "";
                // 'input' 포트가 'null' 이 아닌 경우 (즉, 포트가 성공적으로 생성된 경우) 포트의 이름(portName)을 빈 문자열로 설정한다.
                // 이는 포트에 특정 레이블을 표시하지 않기 위함이다.
                input.style.flexDirection = FlexDirection.Column;
                // 포트 스타일(style.flexDirection)을 'FlexDirection.Column' 으로 설정한다.
                // 이는 포트가 세로 방향으로 정렬되도록 한다.
                // 이는 입력 포트가 'NodeView' 의 상단에 세로로 배열되어 표시되도록 하는 데 도움이 된다.
                inputContainer.Add(input);
                // 최종적으로, 설정된 입력 포트를 'inputContainer' 에 추가한다.
                // 'inputContainer' 는 모든 입력 포트를 담는 컨테이너로, 'NodeView' 내에서 입력 포트를 관리하는 역할을 한다.
            }
        }
        // 생성된 입력 포트는 'inputContainer' 에 추가된다.
        
        /*=======================================================================================================*/

        //출력 포트 구성
        private void CreateOutputPorts() {
            if (node is ActionNode) {

            } else if (node is CompositeNode) {
                output = new NodePort(Direction.Output, Port.Capacity.Multi);
            } else if (node is DecoratorNode) {
                output = new NodePort(Direction.Output, Port.Capacity.Single);
            } else if (node is RootNode) {
                output = new NodePort(Direction.Output, Port.Capacity.Single);
            }
            //노드 타입에 따라 적절한 출력 포트를 생성한다. CompositeNode는 Multi 용량의 출력 포트를, DecoratorNode와 RootNode는 Single 용량의 출력 포트를 가진다. 생성된 출력 포트는 outputContainer에 추가된다.


            if (output != null) {
                output.portName = "";
                // 'output' 포트가 null 이 아닌 경우, 마찬가지로 포트의 이름을 빈 문자열로 설정한다.
                output.style.flexDirection = FlexDirection.ColumnReverse;
                // 포트 스타일을 'FlexDirection.ColumnReverse' 로 설정한다.
                // 이 설정은 출력 포트가 'NodeView' 의 하단에 세로로 배열되어 표시되되, 역순으로 정렬되도록 한다.
                // 이는 특히 복수의 출력 포트를 가진 'CompositeNode' 에서 포트들이 하단부터 시작하여 위로 배열되도록 하는 데 유용한다.
                outputContainer.Add(output);
                // 설정된 출력 포트를 'outputContainer' 에 추가한다.
                // 'outputContainer' 는 모든 출력 포트를 담는 컨테이너로, 'NodeView' 내에서 출력 포트들을 효과적으로 관리한다.
            }
        }
        
        /*=======================================================================================================*/
        
        // 사용자가 노드를 드래그할 때, 노드의 새 위치를 설정한다.
        // 이 메서드는 Undo 시스템과 통합되어, 사용자가 노드 위치 변경을 되돌릴 수 있게 한다.
        public override void SetPosition(Rect newPos) {
            base.SetPosition(newPos);
            // 'base.SetPosition(newPos)' 를 호출하여 'NodeView' 의 위치를 새로운 'Rect(newPos)' 로 설정한다.
            Undo.RecordObject(node, "Behaviour Tree (Set Position");
            // 'Undo.RecordObject' 를 사용하여 노드 위치 변겨에 대한 Undo(실행 취소) 작업을 기록한다.
            // 이를 통해 사용자가 위치 변경 작업을 실행 취소할 수 있다.
            node.position.x = newPos.xMin;
            node.position.y = newPos.yMin;
            // 노드의 내부 데이터 모델에 새 위치 정보를 업데이트한다.
            EditorUtility.SetDirty(node);
            // 노드가 변경되었음을 에디터에 알린다. 이는 변경사항이 있을 때 Unity가 해당 객체를 저장해야 함을 의미한다.
        }
        
        /*=======================================================================================================*/
        
        // 노드 뷰가 선택될 때 호출된다. 이 메서드는 'OnNodeSelected' 이벤트를 발생시켜, 노드가 선택되었음을 알린다.
        public override void OnSelected() {
            base.OnSelected();
            // 상위 클래스의 선택 관련 처리를 수행한다.
            if (OnNodeSelected != null) {
                OnNodeSelected.Invoke(this);
            }
            // 'OnNodeSelected' 이벤트가 null 이 아니라면, 즉 구독자가 있다면, 'OnNodeSelected.Invoke(this)' 를 호출하여 노드가 선택되었음을 알리고, 관련된 작업을 실행할 수 있도록 한다.
        }
        
        /*=======================================================================================================*/
        
        // 'CompositeNode' 타입의 노드가 자식 노드를 가지고 있는 경우, 이 메서드는 그 자식 노드들을 가로 위치에 따라 정렬한다.
        public void SortChildren() {
            if (node is CompositeNode composite) {
                composite.children.Sort(SortByHorizontalPosition);
            }
        }
        // 'node' 가 'CompositeNode' 의 인스턴스인지 확인한다. (CompositeNode는 여러 자식을 가질 수 있는 노드 타입이다.)
        // 'composite.children.Sort(SortByHorizontalPosition)' 를 호출하여 'CompositeNode' 의 자식 노드들을 가로 위치(position.x)에 따라 정렬한다.
        // 이는 'SortByHorizontalPosition' 비교 함수를 사용한다.
        
        /*=======================================================================================================*/
        
        // 자식 노드들을 가로 위치(position.x)에 따라 정렬하기 위한 비교 함수이다. 'CompositeNode'와 같이 여러 자식 노드를 가질 수 있는 노드 타입에서 사용된다.
        private int SortByHorizontalPosition(Node left, Node right) {
            return left.position.x < right.position.x ? -1 : 1;
        }
        // 두 노드(left, right)의 가로 위치(position.x)를 비교하여 정렬 순서를 결정한다.
        // 'left' 노드의 'position.x' 가 'right' 보다 작으면 -1 을, 그렇지 않다면 1 을 반환한다.
        // 이 비교 결과는 자식 노드들을 가로 방향으로 정렬하는 데 사용된다.
        
        /*=======================================================================================================*/
        
        // 노드의 실행 상태(Success, Running, Failure)에 따라 CSS 클래스를 동적으로 추가하거나 제거하여, 노드의 상태를 시각적으로 표현한다.
        public void UpdateState() {

            RemoveFromClassList("running");
            RemoveFromClassList("failure");
            RemoveFromClassList("success");
            // 'RemoveFromClassList' 를 사용하여 'running', 'failure', 'success' 클래스를 'NodeView' 에서 제거한다.
            // 이는 상태 업데이트 전에 이전 상태 표시를 초기화한다.

            if (Application.isPlaying) { // 'Application.isPlaying' 을 확인하여 애플리케이션이 실행 중인지 검사한다.
                switch (node.state) {
                    case Node.State.Running:
                        if (node.started) {
                            AddToClassList("running");
                        }
                        break;
                    case Node.State.Failure:
                        AddToClassList("failure");
                        break;
                    case Node.State.Success:
                        AddToClassList("success");
                        break;
                }
            }
            // 'node.state' 에 따라, 노드가 현재 "Running", "Failure", "Success" 중 어떤 상태인지 확인하고 해당 상태를 나타내는 CSS클래스를 'NodeView' 에 추가한다.
            // 예를 들어, 노드의 상태가 'Node.State.Running' 이면, "Running" 클래스를 추가한다.
            // 이러한 클래스는 노드의 현재 상태를 시각적으로 표현하는 데 사용된다.
        }
        
        /*=======================================================================================================*/
    }
}



➔ SplitView.cs

/*
 * Unity의 UI Toolkit을 사용하여 만들어진 'TwoPaneSplitView' 를 확장한다.
 * 'TwoPaneSplitView' 는 UI에서 두 개의 패널을 가진 분할 뷰를 생성하는 데 사용되는 클래스로, 사용자가 뷰의 분할 비율을 조정할 수 있게 한다.
 * 'SplitView' 클래스는 이를 기반으로 하여 추가적인 기능이나 사용자 지정 동작을 포함할 수 있는 확장 포인트를 제공한다
 *
 * 이 코드의 핵심은 'TwoPaneSplitView' 를 확장하여 사용자 정의 분할 뷰를 쉽게 생성하고, UI Toolkit의 UXML을 통해 이 컴포넌트를 UI에 적용할 수 있게 하는 것이다.
 * 'SplitView' 클래스는 현재 구현된 추가 기능이 없지만, 필요에 따라 사용자 정의 속성이나 메서드를 추가하여 확장할 수 있는 기반이 된다.
 */

namespace TheKiwiCoder {
    /// <summary>
    /// 'SplitView' 클래스는 'TwoPaneSplitView' 의 기능을 상속받아, 두 개의 주요 영역이나 패널을 가진 분할 뷰를 UI에 구현하는 데 사용된다.
    /// 이 클래스 자체는 상속받은 기능 외에 추가적인 구현이나 멤버 변수를 정의하지 않고 있다.
    /// 그러나 이 클래스를 사용함으로써, 'TwoPaneSplitView' 에 없는 기능을 추가하거나, 기존 기능을 오버라이드하여 사용자 정의 동작을 구현할 수 있는 기반을 마련할 수 있다.
    /// </summary>
    public class SplitView : TwoPaneSplitView {
        public new class UxmlFactory : UxmlFactory<SplitView, TwoPaneSplitView.UxmlTraits> { }
        
        /*
         * public new class UxmlFactory : UxmlFactory<SplitView, TwoPaneSplitView.UxmlTraits> { }
         * 이 내부 클래스는 'UxmlFactory' 를 상속받아, UI Toolkit의 UXML 파일에서 'SplitView' 요소를 사용할 수 있게 해준다.
         * 여기서 'new' 키워드는 'TwoPaneSplitView.UxmlFactory' 에서 상속받은 'UxmlFactory' 를 숨기고 새로 정의함을 나타낸다.
         * 이를 통해 'SplitView' 요소를 UXML 파일 내에서 직접 사용할 수 있게 되며,
         * UI Toolkit을 사용한 UI 구성 시 'SplitView' 커스텀 컴포넌트를 효과적으로 활용할 수 있다.
         */
    }
}



➔ BehaviorTree.cs

namespace TheKiwiCoder {
    /// <summary>
    /// AI의 행동을 나타내는 루트 노드와 여러 자식 노드들을 관리한다.
    /// 이 클래스는 트리의 상태를 업데이트하고, 노드 간의 관계를 정의하며, 런타임과 에디터 시간에 사용되는 유틸리티 메서드들을 제공한다.
    /// </summary>
    [CreateAssetMenu()] // SO 생성 메뉴 속성 추가
    public class BehaviourTree : ScriptableObject {
        
        /*=======================================================================================================*/
        public Node rootNode; // 노드의 진임점(시작점)
        public Node.State treeState = Node.State.Running; // 행동 트리의 현재 상태를 나타낸다. 초기값 Running
        
        public List<Node> nodes = new List<Node>(); // 행동 트리에 속한 모든 노드의 리스트
        public Blackboard blackboard = new Blackboard(); // 노드들이 접근할 수 있는 공유 데이터를 저장하는 블랙보드

        /*=======================================================================================================*/
        
        // 트리의 루트 노드를 업데이트하고, 트리의 상태를 반환한다.
        public Node.State Update() {
            if (rootNode.state == Node.State.Running) {
                treeState = rootNode.Update(); // 이 호출은 루트 노드와 그 자식 노드들의 상태를 업데이트 한다.
            }
            return treeState;
        }
        
        /*=======================================================================================================*/
        
        // 주어진 부모 노드의 자식 노드들을 리스트로 반환한다.
        public static List<Node> GetChildren(Node parent) {
            List<Node> children = new List<Node>();

            if (parent is DecoratorNode decorator && decorator.child != null) {
                children.Add(decorator.child);
            }

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

            if (parent is CompositeNode composite) {
                return composite.children;
            }

            return children;
        }
        // 'DecoratorNode' 와 'RootNode' 의 경우, 자식 노드가 존재하면 이를 'children' 리스트에 추가한다.
        // 'CompositeNode' 의 경우, 직접적으로 여러 자식 노드를 가지고 있으므로, 해당 노드의 자식 리스트를 그대로 반환한다.
        // 'children' 리스트를 반환한다. 이 리스트는 부모 노드에 직접 연결된 모든 자식 노드들을 포함한다.
        
        /*=======================================================================================================*/
        
        // 트리를 순회하며, 각 노드에 대해 주어진 액션을 실행한다.
        public static void Traverse(Node node, System.Action<Node> visiter) {
            if (node) {
                visiter.Invoke(node);
                var children = GetChildren(node);
                children.ForEach((n) => Traverse(n, visiter));
            }
        }
        // 주어진 'node' 에 대해 'visiter' 액션(함수)을 호출한다.
        // 'GetChildren(node)' 을 호출하여 현재 노드의 모든 자식 노드를 가져온다.
        // 가져온 자식 노드의 리스트에 대해 재귀적으로 'Traverse' 를 호출하여, 트리의 모든 노드를 순회하고 각 노드에 대해 'visiter' 액션을 실행한다.
        
        /*=======================================================================================================*/
        
        // 행동 트리의 복사본을 생성하고 반환한다.
        public BehaviourTree Clone() {
            BehaviourTree tree = Instantiate(this);
            tree.rootNode = tree.rootNode.Clone();
            tree.nodes = new List<Node>();
            Traverse(tree.rootNode, (n) => {
                tree.nodes.Add(n);
            });

            return tree;
        }
        // 현재 행동 트리의 복사본(tree)을 생성한다.
        // 복사본의 루트 노드를 복제하고, 'tree.rootNode' 에 할당한다.
        // 'Traverse' 메서드를 사용하여 복제된 트리의 모든 노드를 순회하고, 순회 중 방문한 노드를 새 트리의 'nodes' 리스트에 추가한다.
        // 완성된 트리 복사본을 반환한다.
        
        /*=======================================================================================================*/

        // 모든 노드에 컨텍스트와 블랙보드를 바인딩한다.
        public void Bind(Context context) {
            Traverse(rootNode, node => {
                node.context = context;
                node.blackboard = blackboard;
            });
        }
        // 'Traverse(rootNode, ...)' 를 호출하여 트리의 모든 노드를 순회한다.
        // 순회하는 동안 각 노드에 대해, 노드의 'context' 와 'blackboard' 속성을 매개변수로 받은 'context' 객체와 트리의 'blackboard' 로 설정한다.
        // 이는 노드가 실행 시 필요한 컨텍스트 정보와 블랙보드 데이터에 접근할 수 있도록 한다.
        
        /*=======================================================================================================*/
        /*=======================================================================================================*/


        #region Editor Compatibility
        /// <summary>
        /// Unity 에디터에서만 작동하는 메서드들을 포함하고 있으며, '#if UUNITY_EDITOR' 프리프로세서 지시문으로 조건부 컴파일된다.
        /// 이 섹션의 메서드들은 주로 에디터에서 행동 트리를 수정하고 관리하기 위한 기능을 제공한다.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
#if UNITY_EDITOR
        
        /*=======================================================================================================*/

        // 생성할 노드의 유형, 주어진 타입의 새 노드를 생성하고, 트리에 추가한다.
        public Node CreateNode(System.Type type) {
            // 스크립트 가능한 객체 (Scriptable Object) 이기 때문에, 유형을 전달하는 인스턴스 생성하고 다시 노드로 캐스팅
            Node node = ScriptableObject.CreateInstance(type) as Node; // 업캐스팅
            node.name = type.Name; // 노드의 이름을 유형과 동일하게 설정 (검사기에 표시)
            node.guid = GUID.Generate().ToString(); // GUID 인스턴스를 생성하고, 그 값을 문자열로 변환
            // GUID(Globally Unique Identifier), 전역 고유 식별자 (에셋 고유 식별자 생성관리에 사용)(에셋의 고유성을 관리하기 위해 사용)
            // 새로운 노드를 생성할 때마다 해당 노드에 고유한 식별자를 부여 (이 식별자는 노드를 유일하게 식별하고 참조하는데 사용)
            // 노드 간의 관계를 관리하거나 특정 노드를 찾는 수행 가능, Unity의 에셋 관리 시스템에서 에셋 간의 연결이나 참조 관리하는 데도 사용.
            // GUID.Generate() : Unity 엔진 내부에서 에셋 관리를 위해 사용, GUID 클래스의 인스턴스 생성
            // GUID.NewGuid() : .NET 환경에서 고유 식별자를 생성하는데 사용
            
            // Undo.RecordObject : 특정 객체에 대한 변경사항을 기록. 두 개의 매개변수를 받음(변경을 기록할 객체, 그 변경에 대한 설명) 
            // this(현재 객체, 노드를 생성하는 클래스의 인스턴스), "Behavior Tree (CreateNode)"(이 변경에 대한 설명)
            // 이 메서드를 호출하면, 이후에 "객체에 대한 변경"을 Undo(되돌리기)할 수 있다. 즉, 객체긔 상태 변경을 되돌리는데 사용.
            Undo.RecordObject(this, "Behaviour Tree (CreateNode)");
            nodes.Add(node); // 새로 생성된 노드를 nodes리스트에 새로운 요소(node)를 추가

            if (!Application.isPlaying) { // !Application.isPlaying : 현재 어플리케이션이 실행중인지 확인(Unity의 프로퍼티)
                AssetDatabase.AddObjectToAsset(node, this);
                // AssetDatabase.AddObjectToAsset : Unity의 에셋 데이터베이스에 객체를 추가하는 메서드
                // 두 개의 매개변수를 받는다. (에셋 데이터베이스에 추가할 객체, 그 객체를 포함할 부모 객체)
                // 새로 생성된 노드(node)를 현재 객체가 포함하고 있는 에셋에 추가하고 있다. 해당 노드는 Unity의 에셋 시스템에서 관리되는 객체가 된다.
            }
            
            // Undo.RegisterCreatedObjectUndo : 새로 생성된 객체에 대한 Undo 작업을 등록. 두 개의 매개변수를 받음(Undo 작업을 등록할 객체, 그 작업에 대한 설명)
            // 새로 생성된 노드에 대한 Undo 작업을 "Behavior Tree (CreatNode)" 라는 설명과 함께 등록
            // 이 메서드를 호출하면, 이후에 이 "객체의 생성"을 Undo(되돌리기)할 수 있다. 즉, 객체의 생성을 취소하는데 사용
            Undo.RegisterCreatedObjectUndo(node, "Behaviour Tree (CreateNode)");
            
            // AssetDatabase.SaveAssets : Unity의 에셋 데이터베이스에 있는 모든 변경 사항을 디스크에 저장. 매개변수를 받지 않음
            // 이 메서드를 호출하면, AssetDatabase.AddObjectToAsset 등을 통해 에셋 데이터베이스에 추가하거나 변경한 모든 객체가 디스크에 저장된다.
            AssetDatabase.SaveAssets();
            return node;
        }
        
        
        
        /*=======================================================================================================*/
        
        // 삭제할 노드를 가져와 노드를 삭제하는 함수, 주어진 노드를 트리에서 삭제한다.
        public void DeleteNode(Node node) {
            Undo.RecordObject(this, "Behaviour Tree (DeleteNode)");
            // 현재 행동트리에 대한 삭제 작업을 Undo 시스템에 기록한다
            // 이를 통해 사용자가 실수로 노드를 삭제한 경우, Undo 기능을 사용해 복원할 수 있다.
            nodes.Remove(node);
            //인자로 받은 'node' 를 행동 트리의 노드 목록에서 삭제한다.

            //AssetDatabase.RemoveObjectFromAsset(node);
            Undo.DestroyObjectImmediate(node);
            // 'node' 객체를 즉시 삭제하고, 이 작업 또한 Undo 시스템에 기록한다.

            AssetDatabase.SaveAssets();
            // 변경사항을 저장한다.
            // 이는 에디터 상에서 행한 변경사항이 디스크에 영구적으로 반영되도록 한다.
        }
        
        /*=======================================================================================================*/
        
        // 주어진 부모 노드에 자식 노드를 추가한다.
        public void AddChild(Node parent, Node child) {
            if (parent is DecoratorNode decorator) {
                Undo.RecordObject(decorator, "Behaviour Tree (AddChild)");
                decorator.child = child;
                EditorUtility.SetDirty(decorator);
            }

            if (parent is RootNode rootNode) {
                Undo.RecordObject(rootNode, "Behaviour Tree (AddChild)");
                rootNode.child = child;
                EditorUtility.SetDirty(rootNode);
            }

            if (parent is CompositeNode composite) {
                Undo.RecordObject(composite, "Behaviour Tree (AddChild)");
                composite.children.Add(child);
                EditorUtility.SetDirty(composite);
            }
        }
        // 'decorator(rootNode).child = child' 를 호출하여 'child' 노드를 'decorator(rootNode)' 의 자식으로 설정한다.
        // 'composite.children.Add(child)' 를 호출하여 'child' 노드를 'composite' 노드의 자식 목록에 추가한다.
        // 각 경우에 대해, 'Undo.RecordObject(parentNode, "Behavior Tree (AddChild)")' 를 호출하여 자식 노드 추가 작업을 Undo 시스템에 기록한다.
        // 'EditorUtility.SetDirty(parentNode)' 를 호출하여 부모 노드에 변경사항이 있음을 Unity 에디터에 알린다.
        // 이는 변경사항이 저장되고, 에디터의 UI가 적절히 갱신되도록 한다.
        
        /*=======================================================================================================*/
        
        // 주어진 부모 노드에서 자식 노드를 제거한다.
        public void RemoveChild(Node parent, Node child) {
            if (parent is DecoratorNode decorator) {
                Undo.RecordObject(decorator, "Behaviour Tree (RemoveChild)");
                decorator.child = null;
                EditorUtility.SetDirty(decorator);
            }

            if (parent is RootNode rootNode) {
                Undo.RecordObject(rootNode, "Behaviour Tree (RemoveChild)");
                rootNode.child = null;
                EditorUtility.SetDirty(rootNode);
            }

            if (parent is CompositeNode composite) {
                Undo.RecordObject(composite, "Behaviour Tree (RemoveChild)");
                composite.children.Remove(child);
                EditorUtility.SetDirty(composite);
            }
        }
        // 부모 노드의 타입에 따라 적절한 처리를 수행한다.
        // 'DecoratorNode' 와 'RootNode' : 자식 노드를 참조 null 로 설정하여, 자식 노드와의 연결을 제거한다.
        // 'CompositeNode' : 'composite.children.Remove(child)' 를 호출하여 'child' 노드를 자식 목록에서 제거한다.
        // 각 경우에 대해, 'Undo.RecordObject(parentNode, "Behavior Tree (RemoveChild)")' 를 호출하여 자식 노드 제거 작업을 Undo 시스템에 기록한다.
        // 'EditorUnity.SetDirty(parentNode)' 를 호출하여 부모 노드에 변경사항이 있음을 Unity 에디터에 알리고, 이를 통해 변경사항이 저장되고 에디터의 UI가 갱신되도록 한다.
        
        /*=======================================================================================================*/
#endif
        #endregion Editor Compatibility
    }
}



➔ NodePort.cs

/*
 * 이 코드는 Unity의 UI Toolkit(구 Unity Editor GUI)를 사용하여 그래프 뷰 내에서 노드 포트를 정의하는 'NodePort' 클래스이다.
 * 'NodePort' 는 'Port' 클래스는 상속받아 구현되며, 노드 간 연결을 생성하는 인터페이스를 제공한다.
 * 또한 연결(Edge)을 관리하는 사용자 지정 리스너(DefaultEdgeConnectorListener)를 포함한다.
 *
 * 'NodePort' 클래스와 'DefaultEdgeConnectorListener' 내부 클래스는 Unity 에디터 내에서 사용자 정의 그래프 뷰를 구현할 때,
 * 노드 간의 연결을 쉽게 만들고 관리할 수 있는 기능을 제공한다.
 */

namespace TheKiwiCoder {
    
    /// <summary>
    /// 노드 간의 연결점을 나타매여, 연결의 방향과 용량을 지정할 수 있다.
    /// 이 클래스는 그래프 뷰 내에서 노드 간의 연결을 생성하고 관리하기 위한 기능을 제공한다.
    /// </summary>
    public class NodePort : Port {
        
        /*=======================================================================================================*/
        
        // 연결 생성 및 삭제 로직을 처리하는 리스너이다. 그래그 앤 드롭을 통해 포트 간 연결을 생성하거나 기존 연결을 삭제할 때 사용된다.
        // GITHUB:UnityCsReference-master\UnityCsReference-master\Modules\GraphViewEditor\Elements\Port.cs
        private class DefaultEdgeConnectorListener : IEdgeConnectorListener {
            private GraphViewChange m_GraphViewChange; // 연결 변경 사항을 저장하는 GraphViewChange' 객체
            private List<Edge> m_EdgesToCreate; // 생성할 연결을 저장하는 리스트
            private List<GraphElement> m_EdgesToDelete; // 삭제할 연결을 저장하는 리스트
            
            
            // DefaultEdgeConnectorListener의 생성자는 이 리스너 객체가 인스턴스화될 때 초기화 작업을 수행한다.
            // 구체적으로, 연결(Edge) 생성과 삭제를 관리하기 위한 리스트를 초기화한다.
            public DefaultEdgeConnectorListener() {
                m_EdgesToCreate = new List<Edge>();
                // 'm_EdgesToCreate' 리스트를 초기화한다.
                // 이 리스트는 사용자가 포트 간에 새로운 연결을 드래그 앤 드롭하여 생성하려고 할 때, 해당 연결 객체들을 임시로 저장하는 데 사용된다.
                m_EdgesToDelete = new List<GraphElement>();
                // 'm_EdgesToDelete' 리스트를 초기화 한다.
                // 사용자가 새 연결을 만들 때 기존의 연결을 대체해야 하는 경우(예 : 포트 용량이 'Single' 인 경우), 삭제될 기존 연결들을 저장하는 리스트이다.

                m_GraphViewChange.edgesToCreate = m_EdgesToCreate;
                // 'm_GraphViewChange.edgesToCreate' 에 'm_EdgesToCreate' 리스트를 할당한다.
                // 이는 새로 생성될 연결들을 추적하는 데 사용된다.
            }
            
            /*=======================================================================================================*/
            
            //외부 포트에 연결이 떨어졌을 때 호출되지만, 여기서는 구현되지 않는다.
            public void OnDropOutsidePort(Edge edge, Vector2 position) { }
            // 이 메서드는 외부 포트에 연결이 떨어졌을 때 호출되지만, 여기서는 구현되지 않는다.
            // 이는 연결이 유효한 포트 바깥에 떨어졌을 때의 처리를 사용자가 커스텀하게 정의할 수 있도록 남겨져 있다.
            //주어진 포인트가 포트의 영역 내에 있는지 확인한다. 이 메서드는 사용자 인터페이스 내에서 포트를 클릭하거나 선택할 때 사용된다.
            public void OnDrop(GraphView graphView, Edge edge) {
                m_EdgesToCreate.Clear();
                // 'm_EdgesToCreate' 리스트를 비운다. 이는 새로운 연결을 위한 준비 단계로, 리스트를 초기 상태로 리셋한다.
                m_EdgesToCreate.Add(edge);
                // 사용자가 드래그하여 생성한 새로운 연결(edge)을 'm_EdgesToCreate' 리스트에 추가한다.

                // We can't just add these edges to delete to the m_GraphViewChange
                // because we want the proper deletion code in GraphView to also
                // be called. Of course, that code (in DeleteElements) also
                // sends a GraphViewChange.
                m_EdgesToDelete.Clear();
                if (edge.input.capacity == Capacity.Single)
                    foreach (Edge edgeToDelete in edge.input.connections)
                        if (edgeToDelete != edge)
                            m_EdgesToDelete.Add(edgeToDelete);
                if (edge.output.capacity == Capacity.Single)
                    foreach (Edge edgeToDelete in edge.output.connections)
                        if (edgeToDelete != edge)
                            m_EdgesToDelete.Add(edgeToDelete);
                // 'm_EdgesToDelete' 리스트를 비우고, 새로운 연결이 'Single' 용량의 포트에 드롭될 경우,
                // 해당 포트에 이미 존재하는 연결들(edge.input.connections 과 edge.output.connections)을 검사하여, 새 연결과 다른 기존 연결들을 'm_EdgesToDelete' 리스트에 추가한다.
                // 이는 새 연결이 기존 연결을 대체할 필요가 있을 때 기존 연결을 삭제하기 위함이다.
                if (m_EdgesToDelete.Count > 0)
                    graphView.DeleteElements(m_EdgesToDelete);
                // 'm_EdgesToDelete' 에 저장된 기존 연결들이 있다면, 'graphView.DeleteElements(m_EdgesToDelete)' 를 호출하여 그래프 뷰에서 이 연결들을 삭제한다.

                var edgesToCreate = m_EdgesToCreate;
                if (graphView.graphViewChanged != null) {
                    edgesToCreate = graphView.graphViewChanged(m_GraphViewChange).edgesToCreate;
                }
                // 'graphView.graphViewChanged' 이벤트 핸들러가 null 이 아니라면, 새로 생성된 연결들에 대한 처리를 이 이벤트를 통해 수행한다.
                // 이는 그래프 뷰의 상태 변경을 처리하고, 필요에 따라 추가적인 로직을 수행할 수 있게 한다.

                foreach (Edge e in edgesToCreate) {
                    graphView.AddElement(e);
                    edge.input.Connect(e);
                    edge.output.Connect(e);
                }
                // 최종적으로, 'm_EdgesToCreate' 에 저장된 새로운 연결들을 그래프 뷰에 추가하고, 각 연결의 입력과 출력 포트를 연결한다.
                // 이 과정은 사용자가 생성한 연결이 시각적으로 표시되고, 노드간의 데이터 흐름을 나타낼 수 있도록 한다.
            }
            /*
             * 리스너 동작 설명
             * 연결 드래그 앤 드롭 동작을 감지하고 처리하기 위해 DefaultEdgeConnectorListener를 사용한다.
             * 사용자가 연결을 드래그하여 포트에 떨어뜨릴 때, OnDrop 메서드가 호출되어 새 연결을 생성한다.
             * 포트의 용량에 따라, 기존 연결을 삭제할 수 있다. 예를 들어, 포트의 용량이 Single인 경우 이미 연결이 존재하면 그 연결을 삭제하고 새 연결을 추가한다.
             * 연결의 생성 및 삭제 후, 그래프 뷰에 변경 사항을 적용하여 연결이 시각적으로 업데이트된다.
             */
            
            /*=======================================================================================================*/
        }
        
        // 'NodePort' 의 생성자는 연결 방향(Direction)과 용량(Capacity)을 인자로 받아 초기화한다.
        // 연결 리스너를 설정하고, 연결 조작기(EdgeConnector)를 포트에 추가한다. 또한, 포트의 너비를 지정한다.
        public NodePort(Direction direction, Capacity capacity) : base(Orientation.Vertical, direction, capacity, typeof(bool)) {
            // 'NodePort'의 생성자는 'base' 생성자를 호출하여 포트의 기본 설정을 초기화한다.
            // 여기서 'Orientation.Vertical' 은 포트의 방향성을 세로로 설정하고, 'direction' 은 포트가 입력인지 출력인지를,
            // 'capacity' 는 포트가 동시에 연결할 수 있는 연결의 수를(Single 또는 Multi),
            // 마지막으로 'typeof(bool)' 은 포트가 연결할 수 있는 데이터 타입을 나타낸다.
            var connectorListener = new DefaultEdgeConnectorListener();
            // 'DefaultEdgeConnectorListener' 인스턴스를 생성하여 'connectorListener' 변수에 할당한다.
            // 이 리스너는 포트 간의 연결을 드래그 앤 드롭으로 생성할 때 발생하는 이벤트를 처리하는 데 사용된다.
            m_EdgeConnector = new EdgeConnector<Edge>(connectorListener);
            // 'EdgeConnector<Edge>' 를 사용하여 'm_EdgeConnector' 를 초기화한다.
            // 이 때, 'connectorListener' 를 생성자에 전달하여, 연결 이벤트를 처리할 리스너를 설정한다.
            this.AddManipulator(m_EdgeConnector);
            // 'AddManipulator(m_EdgeConnector)' 를 호출하여 생성된 'EdgeConnector' 를 현재 포트에 조작기로 추가한다.
            // 이를 통해 사용자는 UI에서 포트를 드래그하여 노드 간 연결을 생성할 수 있게 된다.
            style.width = 100;
            // 'style.width = 100' 을 설정하여 포트의 너비를 100픽셀로 설정한다.
            // 이는 포트의 시각적 표현과 사용자 인터렉션 영역을 정의한다.
        }
        
        /*=======================================================================================================*/
        
        // 주어진 포인트가 포트의 영역 내에 있는지 확인한다.
        // 이 메서드는 사용자 인터페이스 내에서 포트를 클릭하거나 선택할 때 사용된다.
        public override bool ContainsPoint(Vector2 localPoint) {
            Rect rect = new Rect(0, 0, layout.width, layout.height);
            return rect.Contains(localPoint);
        }
        // 'Rect rect = new Rect(0, 0, layout.width, layout.height)' 를 통해 포트의 현재 레이아웃을 기반으로 한 'Rect' 객체를 생성한다.
        // 이 'Rect' 는 포트의 영역을 나타내며, 시작점은 (0, 0) 이고, 너비와 높이는 포트의 'layout' 속성에서 가져온다.
        // 'rect.Contains(localPoint)' 를 호출하여, 주어진 포인트(localPoint)가 포트의 영역 내에 있는지 확인한다.
        // 이 메서드는 포인트가 포트 영역 내에 있을 경우 'true', 그렇지 않는 경우 'false' 를 반환한다.
        // 이 로직은 사용자 인터페이스에서 포트를 클릭하거나 선택할 때, 클릭이 포트 영역 내에서 발생했는지를 판탄하는 데 사용된다.
        // 이를 통해 사용자의 입력을 정확하게 처리하고, 올바른 사용자 인터랙션을 보잘할 수 있다.
        
        /*=======================================================================================================*/
    }
}











📌 다음 단계

이제 분석이 다 끝났으므로 분석을 토대로 내 방식대로 수정을 하며 만들어보려고 한다.












📌 코드 출저

TheKiwiCoder Youtube

profile
No Easy Day

0개의 댓글