Unity 최종 프로젝트 - 20 (GraphView Custom Editor 분석)

이준호·2024년 2월 10일
0

📌 Unity 최종 프로젝트



📌 Behavior Tree GraphView Editor 분석

Unity Editor에서 행동 트리를 시각적으로 편집하고 관리할 수 있는 커스텀 에디터 환경을 구성한다.
각각의 스크립트는 특정 기능을 담당하며, 서로 상호작용하여 사용자가 행동 트리를 쉽게 생성, 편집, 관리할 수 있게 한다.
AI의 행동 로직을 시각적으로 구성하고, 게임 내에서 다양한 AI 행동을 쉽게 구현할 수 있다.

➔ 분석하는 이유

  • GraphView, UXML 등을 이용한 Custom Editor는 아예 처음 접해보는 새로운 것이었다. 그래서 코드를 보면 처음보는 유니티 제공 클래스들 그 클래스의 내장 메서드들 모르는 것들 투성이었다. 그래서 내가 제대로 이해하고 사용하여 수정하면서 쓰기 위해 정확히 어떤식으로작동하는지 내부 로직을 이해하기 위해서 간단하게 분석하였다.

  • 다음 날은 더 세부적으로 분석할 예정이다.




➔ 각 스크립트의 기능 요약

  • BehaviorTreeEditor : 행동 트리를 편집할 수 있는 메인 에디터 윈도우를 제공한다. 사용자가 행동 트리를 생성, 선택, 편집할 수 있는 UI를 포함한다.

  • BehaviorTreeSettings : 행동 트리 에디터의 설정을 저장한다. 에디터의 UI 구성, 스타일, 노드 템플릿 등을 정의한다.

  • BehaviorTreeView : 행동 트리의 구조를 시각적으로 표현하는 뷰이다. 노드 간 연결을 만들고, 노드를 배치하는 등의 작업을 할 수 있다.

  • DoubleClickSelection : 노드를 더블 클릭했을 때 특정 동작을 수행하는 마우스 조작기이다. 주로 노드와 그 자식 노드들을 선택하는 데 사용된다.

  • InspectorView : 선택된 노드의 세부 정보를 표시하고 편집할 수 있는 인스펙터 뷰를 제공한다.

  • NodeView : 개별 노드를 시각적으로 표현한다. 노드의 타입, 상태, 연결 포트 등을 표시한다.

  • SplitView : 두 개의 패널을 가진 분할 뷰를 제공한다. 예를 들어, 하나의 패널에서 행동 트리를 표시하고, 다른 패널에서 세부 정보를 표시하는 데 사용될 수 있다.

  • BehaviorTree : 행동 트리의 데이터 구조를 정의한다. 루트 노드와 모든 자식 노드들, 그리고 블랙보드를 포함한다.

  • NodePort : 노드 간 연결을 생성할 때 사용되는 포트를 정의한다. 연결의 방향과 용량을 지정할 수 있다.




➔ 상호작용 방식

  • BehahvorTreeEditor는 사용자의 행동에 반응하여 BehaviorTree 객체를 생성하거나 선택하고, 이를 BehaviorTreeView에 표시한다. 또한, BehaviorTreeSettings를 참조하여 에디터의 설정을 불러온다.

  • BehaviorTreeViewNodeView 인스턴스를 사용하여 트리의 각 노드를 시각적으로 표현하며, DoubleClickSelectionNodePort를 사용하여 노드 간 연결을 관리한다.

  • 사용자가 노드를 선택하면, InpectorView가 활설화되어 선택된 노드의 세부 정보를 NodeView에서 가져와 표시하고 편집할 수 있게 한다.

  • SplitView는 필요에 따라 BehaviorTreeEditor 내에서 다양한 뷰(예:BehaviorTreeView 와 InpectorView)를 동시에 표시하는 데 사영된다.

  • NodePortNodeView 내에서 정의되어, 노드 간의 연결을 생성하고 관리하는 인터페이스를 제공한다. 이 때, DefauleEdgeConnectorListener와 같은 리스너가 연결의 생성 및 삭제 로직을 처리한다.












📌 코드 분석

➔ BehaviorTreeEditor.cs

/*
 * Unity Editor 내에서 행동 트리를 시각적으로 편집할 수 있는 에디터 윈도우를 구현한다.
 * 이 클래스는 'EditorWindow' 를 상속받아 구현되며, 행동 트리의 구성, 검사, 및 블랙보드의 편집 기능을 제공한다.
 * 행동 트리 기반의 게임 AI 개발을 위한 강력한 에디터 도구를 제공하며, 개발자가 시각적으로 행동 트리를 구성하고 관리할 수 있게 한다.
*/
namespace TheKiwiCoder {
    /// <summary>
    /// 사용자가 Unity 에디터 내에서 행동 트리를 생성, 편집, 저장할 수 있게 하는 사용자 인터페이스(UI)를 제공한다.
    /// 이 클래스를 통해 개발자는 행동 트리의 노드를 추가, 삭제, 연결할 수 있고, 노드의 세부 사항을 검사하며, 행동 트리의 블랙보드를 관리할 수 있다.
    /// </summary>
    public class BehaviourTreeEditor : EditorWindow {

        BehaviourTreeView treeView; // 행동 트리의 노드들을 시각적으로 표시하고 편집하는 뷰이다.
        BehaviourTree tree; // 현재 편집 중인 행동 트리 객체이더.
        InspectorView inspectorView; // 선택된 노드의 세부 사항을 표시하고 편집하는 뷰이다.
        IMGUIContainer blackboardView; // 행동 트리의 블랙보드를 표시하고 편집하는 컨테이너이다.
        ToolbarMenu toolbarMenu; // 행동 트리 에디터의 도구 모음 메뉴이다.
        TextField treeNameField; // treeNameField, locationPathField : 새 행동 트리를 생성할 때 사용되는 트리 이름과 저장 위치를 입력하는 필드이다.
        TextField locationPathField;
        Button createNewTreeButton; // 새 행동 트리를 생성하는 버튼이다.
        VisualElement overlay; // 트리 생성 대화 상자 등의 오버레이 요소이다.
        BehaviourTreeSettings settings; // 행동 트리 에디터의 설정을 저장하는 객체이다.
        
        // 편집 중인 행동 트리와 그 블랙보드를 직렬화하는 객체이다.
        SerializedObject treeObject;
        SerializedProperty blackboardProperty;

        // 행동 트리 에디터 윈도우를 열기 위한 정적 메서드이다.
        [MenuItem("TheKiwiCoder/BehaviourTreeEditor ...")]
        public static void OpenWindow() {
            BehaviourTreeEditor wnd = GetWindow<BehaviourTreeEditor>();
            wnd.titleContent = new GUIContent("BehaviourTreeEditor");
            wnd.minSize = new Vector2(800, 600);
        }
        
        // 행동 트리 에셋을 더블 클릭할 때 해당 에디터 윈도우를 자동으로 여는 기능을 구현한다.
        [OnOpenAsset]
        public static bool OnOpenAsset(int instanceId, int line) {
            if (Selection.activeObject is BehaviourTree) {
                OpenWindow();
                return true;
            }
            return false;
        }
        
        // 지정된 타입의 모든 에셋을 로드하는 유틸리티 메서드이다.
        List<T> LoadAssets<T>() where T : UnityEngine.Object {
            string[] assetIds = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
            List<T> assets = new List<T>();
            foreach (var assetId in assetIds) {
                string path = AssetDatabase.GUIDToAssetPath(assetId);
                T asset = AssetDatabase.LoadAssetAtPath<T>(path);
                assets.Add(asset);
            }
            return assets;
        }
        
        // 에디터 윈도우의 GUI를 생성하고 초기화하는 메서드이다.
        public void CreateGUI() {

            settings = BehaviourTreeSettings.GetOrCreateSettings();

            // Each editor window contains a root VisualElement object
            VisualElement root = rootVisualElement;

            // Import UXML
            var visualTree = settings.behaviourTreeXml;
            visualTree.CloneTree(root);

            // A stylesheet can be added to a VisualElement.
            // The style will be applied to the VisualElement and all of its children.
            var styleSheet = settings.behaviourTreeStyle;
            root.styleSheets.Add(styleSheet);

            // Main treeview
            treeView = root.Q<BehaviourTreeView>();
            treeView.OnNodeSelected = OnNodeSelectionChanged;

            // Inspector View
            inspectorView = root.Q<InspectorView>();

            // Blackboard view
            blackboardView = root.Q<IMGUIContainer>();
            blackboardView.onGUIHandler = () => {
                if (treeObject != null && treeObject.targetObject != null) {
                    treeObject.Update();
                    EditorGUILayout.PropertyField(blackboardProperty);
                    treeObject.ApplyModifiedProperties();
                }
            };

            // Toolbar assets menu
            toolbarMenu = root.Q<ToolbarMenu>();
            var behaviourTrees = LoadAssets<BehaviourTree>();
            behaviourTrees.ForEach(tree => {
                toolbarMenu.menu.AppendAction($"{tree.name}", (a) => {
                    Selection.activeObject = tree;
                });
            });
            toolbarMenu.menu.AppendSeparator();
            toolbarMenu.menu.AppendAction("New Tree...", (a) => CreateNewTree("NewBehaviourTree"));

            // New Tree Dialog
            treeNameField = root.Q<TextField>("TreeName");
            locationPathField = root.Q<TextField>("LocationPath");
            overlay = root.Q<VisualElement>("Overlay");
            createNewTreeButton = root.Q<Button>("CreateButton");
            createNewTreeButton.clicked += () => CreateNewTree(treeNameField.value);

            if (tree == null) {
                OnSelectionChange();
            } else {
                SelectTree(tree);
            }
        }
        
        // OnEnable, OnDisable : 윈도우가 활성화/비황성화 될 때, 호출되어 플레이 모드 상태 변경 이벤트에 대한 구독을 관리한다.
        private void OnEnable() {
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }

        private void OnDisable() {
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
        }
        
        // 플레이 모드 상태가 변경될 때 호출되는 메서드이다.
        private void OnPlayModeStateChanged(PlayModeStateChange obj) {
            switch (obj) {
                case PlayModeStateChange.EnteredEditMode:
                    OnSelectionChange();
                    break;
                case PlayModeStateChange.ExitingEditMode:
                    break;
                case PlayModeStateChange.EnteredPlayMode:
                    OnSelectionChange();
                    break;
                case PlayModeStateChange.ExitingPlayMode:
                    break;
            }
        }
        
        // 에디터에서 다른 객체가 선택될 때 호출되어, 선택된 행동 트리를 에디터에 표시한다.
        private void OnSelectionChange() {
            EditorApplication.delayCall += () => {
                BehaviourTree tree = Selection.activeObject as BehaviourTree;
                if (!tree) {
                    if (Selection.activeGameObject) {
                        BehaviourTreeRunner runner = Selection.activeGameObject.GetComponent<BehaviourTreeRunner>();
                        if (runner) {
                            tree = runner.tree;
                        }
                    }
                }

                SelectTree(tree);
            };
        }
        
        // 주어진 행동 트리를 선택하고 에디터에 표시하는 메서드이다.
        void SelectTree(BehaviourTree newTree) {

            if (treeView == null) {
                return;
            }

            if (!newTree) {
                return;
            }

            this.tree = newTree;

            overlay.style.visibility = Visibility.Hidden;

            if (Application.isPlaying) {
                treeView.PopulateView(tree);
            } else {
                treeView.PopulateView(tree);
            }

            
            treeObject = new SerializedObject(tree);
            blackboardProperty = treeObject.FindProperty("blackboard");

            EditorApplication.delayCall += () => {
                treeView.FrameAll();
            };
        }
        
        // 노드가 선택될 때 호출되어, 인스펙터 뷰를 업데이트한다.
        void OnNodeSelectionChanged(NodeView node) {
            inspectorView.UpdateSelection(node);
        }

        // 인스펙터가 업데이트될 필요가 있을 때 주기적으로 호출되는 메서드이다.
        private void OnInspectorUpdate() {
            treeView?.UpdateNodeStates();
        }
        
        // 사용자가 입력한 이름으로 새 행동 트리를 생성하고 저장하는 메서드이다.
        void CreateNewTree(string assetName) {
            string path = System.IO.Path.Combine(locationPathField.value, $"{assetName}.asset");
            BehaviourTree tree = ScriptableObject.CreateInstance<BehaviourTree>();
            tree.name = treeNameField.ToString();
            AssetDatabase.CreateAsset(tree, path);
            AssetDatabase.SaveAssets();
            Selection.activeObject = tree;
            EditorGUIUtility.PingObject(tree);
        }
    }
}



➔ BehaviorTreeSettings.cs

/// <summary>
/// 이 코드는 Unity 프로젝트 내에서 행동 트리 에디터의 설정을 관리하는 'BehaviorTreeSettings'클래스와
/// 이 설정을 Unity의 프로젝트 설정 창에 통합하기 위한 'MyCustomSettingsUIElementsRegister' 정적 클래스를 정의한다.
/// 'BehaviorTreeSettings' 는 'ScriptableObject' 를 상속받아, 에디터 환경에 필요한 다양한 리소스와 설정 값을 저장한다.
///
/// 이 코드의 주된 목적은 행동 트리 에디터의 사용자 설정을 관리하고, 이를 Unity의 프로젝트 설정 UI에 통합하는 것이다.
/// 개발자는 이 설정을 통해 행동 트리 에디터의 동작을 사용자의 요구에 맞게 조정할 수 있다.
/// 'BehaviorTreeSettings' 클래스는 필요한 모든 설정 데이터를 저장하며,
/// 'MyCustomSettingUIElementsRegister' 클래스는 이 설정을 Unity의 프로젝트 설정 창에 사용자 친화적인 방식으로 통합한다.
/// </summary>

// Create a new type of Settings Asset.
class BehaviourTreeSettings : ScriptableObject {
    // behaviourTreeXml, nodeXml : 행동 트리와 노드를 구성하는 데 사용되는 UI 구조를 정의하는 'VisualTreeAsset' 이다.
    public VisualTreeAsset behaviourTreeXml; 
    public StyleSheet behaviourTreeStyle; // 행동 트리 에디터와 그 요소들에 적용될 스타일을 정의하는 'StyleSheet' 이다.
    public VisualTreeAsset nodeXml;
    public TextAsset scriptTemplateActionNode; // scriptTemplateActionNode, scriptTemplateCompositeNode, scriptTemplateDecoratorNode
    public TextAsset scriptTemplateCompositeNode; // : 노드 생성 스크립트 템플릿을 저장하는 'TextAsset' 이다.
    public TextAsset scriptTemplateDecoratorNode;
    public string newNodeBasePath = "Assets/"; // 새 노드 스크립트 파일이 저장될 기본 경로이다.

    // 프로젝트 내에서 'BehaviorTreeSettings' 타입의 설정 파일을 찾아 반환한다.
    static BehaviourTreeSettings FindSettings(){
        var guids = AssetDatabase.FindAssets("t:BehaviourTreeSettings");
        if (guids.Length > 1) {
            Debug.LogWarning($"Found multiple settings files, using the first.");
        }

        switch (guids.Length) {
            case 0:
                return null;
            default:
                var path = AssetDatabase.GUIDToAssetPath(guids[0]);
                return AssetDatabase.LoadAssetAtPath<BehaviourTreeSettings>(path);
        }
    }

    // 성정 파일을 찾아 반환하며, 없을 경우 새로 생성하고 저장한다.
    internal static BehaviourTreeSettings GetOrCreateSettings() {
        var settings = FindSettings();
        if (settings == null) {
            settings = ScriptableObject.CreateInstance<BehaviourTreeSettings>();
            AssetDatabase.CreateAsset(settings, "Assets");
            AssetDatabase.SaveAssets();
        }
        return settings;
    }
    
    // 'GetOrCreateSettungs()' 를 통해 얻은 성정 객체를 'SerializedObject' 로 감싸서 반환한다.
    // 이는 Unity의 인스펙터에서 설정을 수정할 수 있게 해준다.
    internal static SerializedObject GetSerializedSettings() {
        return new SerializedObject(GetOrCreateSettings());
    }
}

/*
 * Unity의 프로젝트 설정 창에 사용자 정의 설정을 추가하기 위한 클래스이다.
 * 이 클래스는 'SettingProvider' 를 사용해 Unity 설정 창에 새로운 섹션을 추가한다.
 */
// Register a SettingsProvider using UIElements for the drawing framework:
static class MyCustomSettingsUIElementsRegister {
    // Unity의 "Project Settings" 창에 "BehaviorTree"라는 새 섹션을 추가한다.
    // 이 섹션은 'BehaviorTreeSettings' 의 설정을 편집할 수 있는 UI를 제공한다.
    // 'activateHandler'는 설정 섹션이 활성화될 때 실행되는 람다 함수로, 설정 객체를 UI와 바인딩 하고,
    // 설정을 시각적으로 표시하는 UIElements 구성요소를 생성한다.
    [SettingsProvider]
    public static SettingsProvider CreateMyCustomSettingsProvider() {
        // First parameter is the path in the Settings window.
        // Second parameter is the scope of this setting: it only appears in the Settings window for the Project scope.
        var provider = new SettingsProvider("Project/MyCustomUIElementsSettings", SettingsScope.Project) {
            label = "BehaviourTree",
            // activateHandler is called when the user clicks on the Settings item in the Settings window.
            activateHandler = (searchContext, rootElement) => {
                var settings = BehaviourTreeSettings.GetSerializedSettings();

                // rootElement is a VisualElement. If you add any children to it, the OnGUI function
                // isn't called because the SettingsProvider uses the UIElements drawing framework.
                var title = new Label() {
                    text = "Behaviour Tree Settings"
                };
                title.AddToClassList("title");
                rootElement.Add(title);

                var properties = new VisualElement() {
                    style =
                    {
                        flexDirection = FlexDirection.Column
                    }
                };
                properties.AddToClassList("property-list");
                rootElement.Add(properties);

                properties.Add(new InspectorElement(settings));

                rootElement.Bind(settings);
            },
        };

        return provider;
    }
}



➔ BehaviorTreeView.cs

/*
 * 'BehaviorTreeView' 클래스는 Unity 에디터 내에서 행동 트리를 시각적으로 생성, 편집, 관리할 수 있는 사용자 인터페이스를 제공한다.
 * 이 클래스는 'GrahpView'를 상속받아 노드 기반의 인터렉티브 그래프를 구현하며, 사용자는 노드를 추가, 삭제, 연결할 수 있다.
 * 또한, 컨텍스트 메뉴를 통해 새 스크립트 파일을 생성하고, 특정 타입의 노드를 그래프에 동적으로 추가하는 기능을 포함한다.
 * 이를 통해 개발자는 복잡한 AI 행동 로직을 시각적으로 구성하고 관리할 수 있다.
 */
namespace TheKiwiCoder {
    
    /// <summary>
    /// Unity Editor에서 사용할 수 있는 행동 트리(Behavior Tree) 시각화 및 편집 도구를 구현하는 클래스 (BehaviorTreeView)
    /// Unity의 GraphView를 상속받아 사용자가 행동 트리를 그래픽 인터페이스를 통해 쉽게 만들고 수정할 수 있게 해준다.
    /// 행동트리를 시각화하고, 편집하는 에디터 확장 기능을 제공
    /// </summary>
    public class BehaviourTreeView : GraphView {

        // 'NodeView' 객체가 선택될 때, 호출될 액션(delegate) 외부에서 이벤트 핸들러에 등록하여 노드가 선택될 때, 특정 행동을 정의할 수 있다.
        public Action<NodeView> OnNodeSelected;
        // UxmlFactory : 'GraphView'의 'UxmlFactory' 를 상속받는 내부 클래스로, UXML을 통해 'BehaviorTreeView'를 인스턴스화 할 때 사용된다.
        public new class UxmlFactory : UxmlFactory<BehaviourTreeView, GraphView.UxmlTraits> { }
        // 현재 에디터에서 작업중인 'BehaviorTree' 객체, 이 트리는 노드의 집합과 그들 사이의 연결 관계를 포함한다.
        BehaviourTree tree;
        // 행동 트리 에디터의 설정을 저장하는 'BehaviorTreeSettings' 객체, 스타일 시트나 노드 생성 템플릿 등의 설정 정보를 포함한다.
        BehaviourTreeSettings settings;
        
        // 스크립트 파일 생성 시 사용할 템플릿 정보를 담는 구조체. 템플릿 파일, 기본 파일 이름, 하위 폴더 경로를 포함
        public struct ScriptTemplate {
            public TextAsset templateFile;
            public string defaultFileName;
            public string subFolder;
        }
        
        // 스크립트 생성에 사용될 템플릿 파일 정보를 담는 'ScriptTemplate' 배열.
        // 이 배열은 ActionNode, CompositeNode, DecorateNode 생성 시 사용될 템플릿 파일과 기본 파일 이름, 저장될 하위 폴더 경로를 정의한다.
        public ScriptTemplate[] scriptFileAssets = {
            
            new ScriptTemplate{ templateFile=BehaviourTreeSettings.GetOrCreateSettings().scriptTemplateActionNode, defaultFileName="NewActionNode.cs", subFolder="Actions" },
            new ScriptTemplate{ templateFile=BehaviourTreeSettings.GetOrCreateSettings().scriptTemplateCompositeNode, defaultFileName="NewCompositeNode.cs", subFolder="Composites" },
            new ScriptTemplate{ templateFile=BehaviourTreeSettings.GetOrCreateSettings().scriptTemplateDecoratorNode, defaultFileName="NewDecoratorNode.cs", subFolder="Decorators" },
        };
        
        /// <summary>
        /// 생성자에서 설정을 로드하고, 'GraphView'에 필요한 조작기(mainiplators)와 UI 요소를 추가한다.
        /// 이는 사용자가 트리를 쉽게 조작할 수 있게 해준다.
        /// 조작기에는 ContentZoomer, ContentDragger, DoubleClickSelection, SelectionDragger, RectangleSelector 등이 포함되었다.
        /// 또한 스타일시트를 추가하여 뷰의 외관을 정의한다.
        /// Undo/Redo 기능을 위한 Event 리스너를 설정한다.
        /// </summary>
        public BehaviourTreeView() {
            settings = BehaviourTreeSettings.GetOrCreateSettings();

            Insert(0, new GridBackground());

            this.AddManipulator(new ContentZoomer());
            this.AddManipulator(new ContentDragger());
            this.AddManipulator(new DoubleClickSelection());
            this.AddManipulator(new SelectionDragger());
            this.AddManipulator(new RectangleSelector());

            var styleSheet = settings.behaviourTreeStyle;
            styleSheets.Add(styleSheet);

            Undo.undoRedoPerformed += OnUndoRedo;
        }
        
        // Undo나 Redo 작업이 수행될 때, 호출되어 트리 뷰를 새로운 상태로  업데이트 하고, 변경 사항을 저장한다.
        private void OnUndoRedo() {
            PopulateView(tree);
            AssetDatabase.SaveAssets();
        }
        
        // 주어진 'Node' 객체에 해당하는 'NodeView'를 찾아 반환한다. 이는 노드의 GUID를 사용하여 검색한다.
        public NodeView FindNodeView(Node node) {
            return GetNodeByGuid(node.guid) as NodeView;
        }
        
        // 주어진 'BehaviorTree' 객체를 사용해 그래프뷰를 채운다.
        // 먼저 기존의 그래프 요소를 모두 삭제한 후, 트리의 모든 노드에 대해 노드 뷰를 생성하고, 노드 간의 연결(Edge)를 생성한다.
        internal void PopulateView(BehaviourTree tree) {
            this.tree = tree;

            graphViewChanged -= OnGraphViewChanged;
            DeleteElements(graphElements.ToList());
            graphViewChanged += OnGraphViewChanged;

            if (tree.rootNode == null) {
                tree.rootNode = tree.CreateNode(typeof(RootNode))as RootNode;   // <- 다운 캐스팅  (알아두면 좋다. : .GetType() : 실제 자신을 반환)
                EditorUtility.SetDirty(tree);
                AssetDatabase.SaveAssets();
            }

            // Creates node view
            tree.nodes.ForEach(n => CreateNodeView(n));

            // Create edges
            tree.nodes.ForEach(n => {
                var children = BehaviourTree.GetChildren(n);
                children.ForEach(c => {
                    NodeView parentView = FindNodeView(n);
                    NodeView childView = FindNodeView(c);

                    Edge edge = parentView.output.ConnectTo(childView.input);
                    AddElement(edge);
                });
            });
        }
        
        // 연결을 시작하는 포트와 연결이 가능한 다른 포트들을 찾는다. 규칙은 아래와 같다.
        // 연결을 시작하는 포트 'startPort' 와 반대 방향의 포트만 연결 대산으로 고려한다.
        // 동일한 노드에 속한 포트끼리는 연결되지 않는다.
        // 결과적으로, 이 메서드는 사용자가 노드 간 연결을 만들 수 있는 유효한 포트들만을 선택할 수 있도록 도와준다.
        public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) {
            return ports.ToList().Where(endPort =>
            endPort.direction != startPort.direction &&
            endPort.node != startPort.node).ToList();
        }
        
        // 그래프 뷰에 변경사항이 발생했을 때 이를 처리한다. 주로 노드 또는 연결선(Edge)의 추가 및 제거와 관련된 작업을 수행한다.
        // 제거할 요소가 있으면, 해당 요소가 노드인 경우 트리에서 노드를 삭제하고, Edge인 경우 연결된 노드 간의 관계를 제거한다.
        // 생성할 Edge가 있으면, 새로운 Edge로 연결된 노드 간의 부모-자식 관계를 추가한다.
        // 모든 노드의 자식을 정렬한다.
        private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange) {
            if (graphViewChange.elementsToRemove != null) {
                graphViewChange.elementsToRemove.ForEach(elem => {
                    NodeView nodeView = elem as NodeView;
                    if (nodeView != null) {
                        tree.DeleteNode(nodeView.node);
                    }

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

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

            nodes.ForEach((n) => {
                NodeView view = n as NodeView;
                view.SortChildren();
            });

            return graphViewChange;
        }
        
        // 그래프 뷰에서 마우스 오른쪽 버튼을 클릭할 때 나타나는 컨텍스트 메뉴를 구성한다.
        // 새 스크립트 파일 생성 옵션을 메뉴에 추가한다. (액션 노드, 컴포지트 노드, 데코레이터 노드)
        // 다양한 노드 타입(액션, 컴포지트, 데코레이터)을 추가할 수 있는 메뉴 항목을 동적으로 생성한다.
        // 사용자가 메뉴 옵션을 선택하면, 해당 타입의 노드가 그래프 뷰에 추가된다.
        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) {

            //base.BuildContextualMenu(evt);

            // New script functions
            evt.menu.AppendAction($"Create Script.../New Action Node", (a) => CreateNewScript(scriptFileAssets[0]));
            evt.menu.AppendAction($"Create Script.../New Composite Node", (a) => CreateNewScript(scriptFileAssets[1]));
            evt.menu.AppendAction($"Create Script.../New Decorator Node", (a) => CreateNewScript(scriptFileAssets[2]));
            evt.menu.AppendSeparator();

            Vector2 nodePosition = this.ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition);
            {

                var types = TypeCache.GetTypesDerivedFrom<ActionNode>();
                foreach (var type in types) {
                    evt.menu.AppendAction($"[Action]/{type.Name}", (a) => CreateNode(type, nodePosition));
                }
            }

            {
                var types = TypeCache.GetTypesDerivedFrom<CompositeNode>();
                foreach (var type in types) {
                    evt.menu.AppendAction($"[Composite]/{type.Name}", (a) => CreateNode(type, nodePosition));
                }
            }

            {
                var types = TypeCache.GetTypesDerivedFrom<DecoratorNode>();
                foreach (var type in types) {
                    evt.menu.AppendAction($"[Decorator]/{type.Name}", (a) => CreateNode(type, nodePosition));
                }
            }
        }

        // 주어진 경로의 폴더를 Unity 에디터의 프로젝트 뷰에서 선택하고 강조 표시한다.
        // 경로의 마지막에 '/' 가 있으면 제거한다.
        // 주어진 경로에 해당하는 'UnityEngine.Object'를 로드한다.
        // 로드한 객체를 프로젝트 뷰에서 선택하고 강조 표시한다.
        void SelectFolder(string path) {
            // https://forum.unity.com/threads/selecting-a-folder-in-the-project-via-button-in-editor-window.355357/
            // Check the path has no '/' at the end, if it does remove it,
            // Obviously in this example it doesn't but it might
            // if your getting the path some other way.

            if (path[path.Length - 1] == '/')
                path = path.Substring(0, path.Length - 1);

            // Load object
            UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object));

            // Select the object in the project folder
            Selection.activeObject = obj;

            // Also flash the folder yellow to highlight it
            EditorGUIUtility.PingObject(obj);
        }
        
        // 사용자가 새 스크립트 파일을 생성할 수 있게 한다.
        // 선택된 템플릿에 따라 스크립트 파일을 생성하고, 해당 파일이 저장될 폴더를 선택한다.
        void CreateNewScript(ScriptTemplate template) {
            SelectFolder($"{settings.newNodeBasePath}/{template.subFolder}");
            var templatePath = AssetDatabase.GetAssetPath(template.templateFile);
            ProjectWindowUtil.CreateScriptAssetFromTemplateFile(templatePath, template.defaultFileName);
        }
        
        // 사용자가 지정한 타입의 새 노드를 생성하고, 이를 그래프 뷰에 추가한다.
        // 노드의 위치는 사용자가 그래프상에서 클릭한 위치에 기반한다.
        void CreateNode(System.Type type, Vector2 position) {
            Node node = tree.CreateNode(type);
            node.position = position;
            CreateNodeView(node);
        }
        
        // 주어진 'Node' 객체를 기반으로 'NodeView' 객체를 생성하고, 이를 'GraphView'에 추가한다. (이 메서드는 노드가 생성)
        void CreateNodeView(Node node) {
            NodeView nodeView = new NodeView(node);
            nodeView.OnNodeSelected = OnNodeSelected;
            AddElement(nodeView);
        }
        
        // 그래프 내의 모든 노드 뷰의 상태를 최신 상태로 업데이트한다. 이는 주로 노드의 시각적 표현을 최신 상태로 유지하기 위해 사용된다.
        public void UpdateNodeStates() {
            nodes.ForEach(n => {
                NodeView view = n as NodeView;
                view.UpdateState();
            });
        }
    }
}



➔ DoubleClickSelection.cs

/*
 * 이 클래스는 'MouseManipulator' 를 상속받아, 마우스 이벤트를 기반으로 특정 조작을 수행하는 사용자 지정 마우스 조작기를 구현한다.
 * 특히, 이 클래스는 사용자가 노드를 더블 클릭할 때 해당 노드와 그 자식 노드들을 선택하는 기능을 제공한다.
 *
 * 이 클래스는 사용자가 'BehaviorTreeView' 내에서 노드를 더블 클릭하는 사용자 경험을 향상시키는 데 중요한 역할을 한다.
 * 이를 통해 사용자는 노드와 그 자식 노드들을 빠르게 선택하고, 이들 간의 관계를 시각적으로 확인할 수 있다.
 */

namespace TheKiwiCoder {
    /// <summary>
    /// 'DoubleClickSelection' 클래스는 Unity의 GraphView API를 확장하며, 사용자가 'BehaviorTreeView' 내의 노드를 더블 클릭된 노드와 그 자식 노드들이 선택된다.
    /// 이 기능은 트리 구조 내에서 부모 노드와 관련된 자식 노드들을 함께 선택하고자 할 때 유용하다.
    /// </summary>
    public class DoubleClickSelection : MouseManipulator {
        double time; // 마지막 마우스 다운 이벤트가 발생한 시간을 기록한다.
        double doubleClickDuration = 0.3;  // 더블 클릭으로 간주되는 최대 시간 간격을 정의한다. 기본값은 0.3
        
        // 생성자에서 객체가 생성될 때의 시간(EditorApplication.timeSinceStartup)을 'time' 필드에 초기화한다.
        // 이 시간은 첫 번째 마우스 다운 이벤트의 타임스탬프로 사용된다.
        public DoubleClickSelection() {
            time = EditorApplication.timeSinceStartup;
        }
        
        // 조작기가 대상(BehaviorTreeView)에 연결될 때 호출된다.
        // 'MouseDownEvent' 에 대한 콜백(OnMouseDown)을 등록하여, 마우스 다운 이벤트를 처리할 수 있게 한다.
        protected override void RegisterCallbacksOnTarget() {
            target.RegisterCallback<MouseDownEvent>(OnMouseDown);
        }
        
        // 조작기가 대상에서 분리될 때 호출된다. 'MouseDownEvent' 에 대한 콜백을 해제한다.
        protected override void UnregisterCallbacksFromTarget() {

            target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
        }
        
        // 마우스 다운 이벤트가 발생했을 때 호출된다. 이 메서드는 마지막 마우스 다운 이벤트 이후 시간 간격을 계산하고,
        // 이 간격이 'doubleClickDuration' 보다 작을 경우 'SelectChildren' 메서드를 호출하여 자식 노드들을 선택한다.
        // 그리고 현재 시간을 'time' 필드에 다시 기록한다.
        private void OnMouseDown(MouseDownEvent evt) {
            var graphView = target as BehaviourTreeView;
            if (graphView == null)
                return;

            double duration = EditorApplication.timeSinceStartup - time;
            if (duration < doubleClickDuration) {
                SelectChildren(evt);
            }

            time = EditorApplication.timeSinceStartup;
        }

        // 더블 클릭된 노드와 그 자식 노드들을 선택한다.
        // 이 메서드는 더블 클릭된 노드를 식별하고, 'BehaviorTree.Traverse' 메서드를 사용하여 해당 노드와 그 자식 노드들을 순회하며,
        // 각 노드에 대응하는 'NodeView'를 'BehaviorTreeView' 의 선택 목록에 추가한다.
        void SelectChildren(MouseDownEvent evt) {

            var graphView = target as BehaviourTreeView;
            if (graphView == null)
                return;

            if (!CanStopManipulation(evt))
                return;

            NodeView clickedElement = evt.target as NodeView;
            if (clickedElement == null) {
                var ve = evt.target as VisualElement;
                clickedElement = ve.GetFirstAncestorOfType<NodeView>();
                if (clickedElement == null)
                    return;
            }

            // Add children to selection so the root element can be moved
            BehaviourTree.Traverse(clickedElement.node, node => {
                var view = graphView.FindNodeView(node);
                graphView.AddToSelection(view);
            });
        }
    }
}



➔ 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> { }
        
        // 선택된 노드를 위한 'Editor' 인스턴스이다. 이 'Editor' 객체는 선택된 노드의 속성을 인스펙터 UI에 표시하고 편집할 수 있게 해준다.
        Editor editor;
        
        // 'InspectorView' 의 생성자는 기본적으로 비어있다.
        // 이 클래스의 인스턴스가 생성될 때 특별한 초기화 작업은 필요하지 않으며, 주된 기능은 선택된 노드를 인스펙터 뷰에 표시하는 것이다.
        public InspectorView() {

        }
        
        // 사용자가 행동 트리 에디터 내에서 다른 노드를 선택했을 때 호출된다.
        internal void UpdateSelection(NodeView nodeView) {
            Clear();

            UnityEngine.Object.DestroyImmediate(editor);

            editor = Editor.CreateEditor(nodeView.node);
            IMGUIContainer container = new IMGUIContainer(() => {
                if (editor && editor.target) {
                    editor.OnInspectorGUI();
                }
            });
            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;
            this.node.name = node.GetType().Name;
            this.title = node.name.Replace("(Clone)", "").Replace("Node", "");
            this.viewDataKey = node.guid;

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

            CreateInputPorts();
            CreateOutputPorts();
            SetupClasses();
            SetupDataBinding();
        }
        
        // 노드의 속성(예:설명)을 UI 요소에 바인딩하는 메서드이다. 이를 통해 노드의 데이터가 UI에 동적으로 반영된다.
        private void SetupDataBinding() {
            Label descriptionLabel = this.Q<Label>("description");
            descriptionLabel.bindingPath = "description";
            descriptionLabel.Bind(new SerializedObject(node));
        }
        
        // 노드 타입(Action, Composite, Decorate)에 따라 CSS 클래스를 동적으로 추가하는 메서드이다. 노드의 시각적 스타일을 결정한다.
        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");
            }
        }
        
        // 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) {

            }

            if (input != null) {
                input.portName = "";
                input.style.flexDirection = FlexDirection.Column;
                inputContainer.Add(input);
            }
        }

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

            if (output != null) {
                output.portName = "";
                output.style.flexDirection = FlexDirection.ColumnReverse;
                outputContainer.Add(output);
            }
        }
        
        // 사용자가 노드를 드래그할 때, 노드의 새 위치를 설정한다.
        // 이 메서드는 Undo 시스템과 통합되어, 사용자가 노드 위치 변경을 되돌릴 수 있게 한다.
        public override void SetPosition(Rect newPos) {
            base.SetPosition(newPos);
            Undo.RecordObject(node, "Behaviour Tree (Set Position");
            node.position.x = newPos.xMin;
            node.position.y = newPos.yMin;
            EditorUtility.SetDirty(node);
        }
        
        // 노드 뷰가 선택될 때 호출된다. 이 메서드는 'OnNodeSelected' 이벤트를 발생시켜, 노드가 선택되었음을 알린다.
        public override void OnSelected() {
            base.OnSelected();
            if (OnNodeSelected != null) {
                OnNodeSelected.Invoke(this);
            }
        }
        
        // 'CompositeNode' 타입의 노드가 자식 노드를 가지고 있는 경우, 이 메서드는 그 자식 노드들을 가로 위치에 따라 정렬한다.
        public void SortChildren() {
            if (node is CompositeNode composite) {
                composite.children.Sort(SortByHorizontalPosition);
            }
        }
        
        // 자식 노드들을 가로 위치(position.x)에 따라 정렬하기 위한 비교 함수이다. 'CompositeNode'와 같이 여러 자식 노드를 가질 수 있는 노드 타입에서 사용된다.
        private int SortByHorizontalPosition(Node left, Node right) {
            return left.position.x < right.position.x ? -1 : 1;
        }
        
        // 노드의 실행 상태(Success, Running, Failure)에 따라 CSS 클래스를 동적으로 추가하거나 제거하여, 노드의 상태를 시각적으로 표현한다.
        public void UpdateState() {

            RemoveFromClassList("running");
            RemoveFromClassList("failure");
            RemoveFromClassList("success");

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



➔ 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;
        }
        
        // 트리를 순회하며, 각 노드에 대해 주어진 액션을 실행한다.
        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));
            }
        }
        
        // 행동 트리의 복사본을 생성하고 반환한다.
        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;
        }

        // 모든 노드에 컨텍스트와 블랙보드를 바인딩한다.
        public void Bind(Context context) {
            Traverse(rootNode, node => {
                node.context = context;
                node.blackboard = 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)");
            nodes.Remove(node);

            //AssetDatabase.RemoveObjectFromAsset(node);
            Undo.DestroyObjectImmediate(node);

            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);
            }
        }
        
        // 주어진 부모 노드에서 자식 노드를 제거한다.
        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);
            }
        }
#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>();
                // 사용자가 새 연결을 만들 때 기존의 연결을 대체해야 하는 경우(예 : 포트 용량이 'Single' 인 경우), 삭제될 기존 연결들을 저장하는 리스트이다.
                m_EdgesToDelete = new List<GraphElement>();

                m_GraphViewChange.edgesToCreate = m_EdgesToCreate;
            }
            
            // 외부 포트에 연결이 떨어졌을 때 호출되지만, 여기서는 구현되지 않는다.
            public void OnDropOutsidePort(Edge edge, Vector2 position) { }
            // 주어진 포인트가 포트의 영역 내에 있는지 확인한다. 이 메서드는 사용자 인터페이스 내에서 포트를 클릭하거나 선택할 때 사용된다.
            public void OnDrop(GraphView graphView, Edge edge) {
                m_EdgesToCreate.Clear();
                m_EdgesToCreate.Add(edge);

                // 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);
                if (m_EdgesToDelete.Count > 0)
                    graphView.DeleteElements(m_EdgesToDelete);

                var edgesToCreate = m_EdgesToCreate;
                if (graphView.graphViewChanged != null) {
                    edgesToCreate = graphView.graphViewChanged(m_GraphViewChange).edgesToCreate;
                }

                foreach (Edge e in edgesToCreate) {
                    graphView.AddElement(e);
                    edge.input.Connect(e);
                    edge.output.Connect(e);
                }
            }
            /*
             * 리스너 동작 설명
             * 연결 드래그 앤 드롭 동작을 감지하고 처리하기 위해 DefaultEdgeConnectorListener를 사용한다.
             * 사용자가 연결을 드래그하여 포트에 떨어뜨릴 때, OnDrop 메서드가 호출되어 새 연결을 생성한다.
             * 포트의 용량에 따라, 기존 연결을 삭제할 수 있다. 예를 들어, 포트의 용량이 Single인 경우 이미 연결이 존재하면 그 연결을 삭제하고 새 연결을 추가한다.
             * 연결의 생성 및 삭제 후, 그래프 뷰에 변경 사항을 적용하여 연결이 시각적으로 업데이트된다.
             */
        }
        
        // 'NodePort' 의 생성자는 연결 방향(Direction)과 용량(Capacity)을 인자로 받아 초기화한다.
        // 연결 리스너를 설정하고, 연결 조작기(EdgeConnector)를 포트에 추가한다. 또한, 포트의 너비를 지정한다.
        public NodePort(Direction direction, Capacity capacity) : base(Orientation.Vertical, direction, capacity, typeof(bool)) {
            var connectorListener = new DefaultEdgeConnectorListener();
            m_EdgeConnector = new EdgeConnector<Edge>(connectorListener);
            this.AddManipulator(m_EdgeConnector);
            style.width = 100;
        }
        
        // 주어진 포인트가 포트의 영역 내에 있는지 확인한다.
        // 이 메서드는 사용자 인터페이스 내에서 포트를 클릭하거나 선택할 때 사용된다.
        public override bool ContainsPoint(Vector2 localPoint) {
            Rect rect = new Rect(0, 0, layout.width, layout.height);
            return rect.Contains(localPoint);
        }
    }
}











📌 코드 출저

TheKiwiCoder Youtube

profile
No Easy Day

0개의 댓글