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

이준호·2024년 2월 11일
0

📌 Unity 최종 프로젝트



📌 Behavior Tree GraphView Editor 코드 심화 분석

➔ 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;
        // treeObject : 현재 편집중인 'BehaviorTree' 객체를 'SerializedObject' 로 감싸, 인스펙터에서 수정할 수 있게 하는 객체이다.
        // Unity 에디터에서 객체의 프로퍼티를 시각적으로 편집하기 위해 사용된다.
        SerializedProperty blackboardProperty;
        // blackboardProperty : 'treeObject' 에서 'blackboard' 프로퍼티를 참조하는 'SerializedProperty' 이다.
        // 이를 통해 'blackboard' 의 세부 사항을 인스펙터에서 직접 수정할 수 있다.
        
        /*=======================================================================================================*/

        // 행동 트리 에디터 윈도우를 열기 위한 정적 메서드이다.
        [MenuItem("TheKiwiCoder/BehaviourTreeEditor ...")]
        // Unity 메뉴에 "TheKiwiCoder/BehaviourTreeEditor ..." 항목을 추가하고, 이를 선택했을 때 'BehaviorTreeEditor' 윈도우를 열도록 한다.
        public static void OpenWindow() {
            BehaviourTreeEditor wnd = GetWindow<BehaviourTreeEditor>();
            // 'GetWindow<BehaviourTreeEditor>()' 함수는 'BehaviorTreeEditor' 타입의 에디터 윈도우 인스턴스를 반환하거나, 없으면 새로 생성한다.
            wnd.titleContent = new GUIContent("BehaviourTreeEditor");
            wnd.minSize = new Vector2(800, 600);
            // 'titleContent' 과 'minSize' 를 설정하여, 윈도우의 제목과 최소 크기를 지정한다.
        }
        
        /*=======================================================================================================*/
        
        // 행동 트리 에셋을 더블 클릭할 때 해당 에디터 윈도우를 자동으로 여는 기능을 구현한다.
        [OnOpenAsset]
        // '[OnOpenAsset]' 어트리뷰를 사용하여, 프로젝트 뷰에서 행동 트리 에셋을 더블 클릭했을 때 자동으로, 'BehaviorTreeEditor' 윈도우를 열도록 한다.
        public static bool OnOpenAsset(int instanceId, int line) 
        {
            if (Selection.activeObject is BehaviourTree) 
            {
                OpenWindow();
                return true;
            }
            return false;
        }
        // 'Selection.activeObject' 가 'BehaviorTree' 타입인지 검사하여, 해당하는 뎡우 'OpenWindow()' 를 호출해 데이터 윈도우를 연다.
        // 이는 윈도우를 자동으로 열어 편집을 시작할 수 있게 해준다.
        
        /*=======================================================================================================*/
        
        // 지정된 타입의 모든 에셋을 로드하는 유틸리티 메서드이다.
        List<T> LoadAssets<T>() where T : UnityEngine.Object
        // 제네릭 'T' 에 해당하는 모든 에셋을 로드하는 유틸리티 메서드이다.
        // 여기서 'T' 는 'UnityEngine.Object' 를 상속받는 모든 타입이 될 수 있다.
        {
            string[] assetIds = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
            // 'AssetDatabase.FindAssets($"t:{typeof(T).Name}")' 을 사용하여 지정된 타입의 모든 에셋의 GDUI를 찾는다.
            List<T> assets = new List<T>();
            foreach (var assetId in assetIds) 
            {
                string path = AssetDatabase.GUIDToAssetPath(assetId);
                // 각 GDUI에 대해 'AssetDatabase.GUIDToAssetPath(assetId)' 를 호출하여 에셋의 경로를 얻고,
                T asset = AssetDatabase.LoadAssetAtPath<T>(path);
                // 'AssetDatabase.LoadAssetAtPath<T>(path)' 로 실제 에셋을 로드한다.
                assets.Add(asset);
                // 로드된 에셋을 'List<T>' 에 추가한다.
            }
            return assets;
            // 로드된 에셋들을 반환한다.
        }
        // 이 메서드는 행동 트리 에디터에서 사용할 에셋을 동적으로 로드할 때 사용될 수 있다.
        
        /*=======================================================================================================*/
        
        // 에디터 윈도우의 GUI를 생성하고 초기화하는 메서드이다. ('BehaviorTreeEditor' 의 핵심적인 부분)
        public void CreateGUI() {
            
            // 설정 로드
            settings = BehaviourTreeSettings.GetOrCreateSettings();
            // 'BehaviourTreeSettings.GetOrCreateSettings()' 를 호출하여 'BehaviorTreeEditor' 의 설정을 로드하거나, 없으면 생성한다.
            // 이 설정에는 UI 구성, 스타일, 사용할 XML 등이 포함된다.
            
            // 루트 'VisualElement' 설정
            // Each editor window contains a root VisualElement object
            VisualElement root = rootVisualElement;
            // 애디터 윈도우의 'rootVisualElement' 객체에 접근하여 UI의 기본 컨테이너로 사용한다.
            
            // UXML 가져오기
            // Import UXML
            var visualTree = settings.behaviourTreeXml;
            visualTree.CloneTree(root);
            // 설정에서 정의된 'behaviorTreeXml' 을 사용하여 UXML로부터 UI구조를 가져오고,
            // 'CloneTree' 메서드를 통해 현재 루트 'VisualEnelemt' 에 복재하여 UI 구조를 구성한다.

            // 스타일 시트 추가
            // 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);
            // 'setting.behaviorTreeStyle' 에서 가져온 스타일 시트를
            // 루트 'VisualElement' 에 추가한다. 이 스타일은 UI의 모양과 느낌을 정의한다.

            // 매인 트리뷰 생성
            // Main treeview
            treeView = root.Q<BehaviourTreeView>();
            treeView.OnNodeSelected = OnNodeSelectionChanged;
            // 'root.Q<BehaviorTreeView>()' 를 호출하여 메인 트리뷰를 찾고, 'OnNodeSelected' 이벤트 핸들러를 설정한다.
            // 이 트리뷰는 행동 트리의 노드들을 시각적으로 표시하고 조작할 수 있는 영역이다.
            
            // 인스펙터 뷰 설정
            // Inspector View
            inspectorView = root.Q<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();
                }
            };
            // 'IMGUIContainer' 를 사용하여 블랙보드 뷰를 설정한다.
            // 이 컨테이너 내에서, 'treeObject' 와 'blackboardProperty' 를 사용하여 블랙보드의 프로퍼티를 인스펙터에서 편집할 수 있게 한다.

            // 툴바 에셋 메뉴 구성
            // 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"));
            // 'LoadAssets<BehaviorTree>()' 를 호출하여 모든 'BehaviorTree' 에셋을 로드하고, 이를 툴바 메뉴에 추가한다.
            // 이를 통해 사용자는 기존 행동 트리를 선택하거나 새로운 트리를 생성할 수 있는 옵션을 갖게 된다.
            
            // 새 트리 대화상자 구성
            // 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);
            // 새 행동 트리를 생성하기 위한 입력 필드(treeNameField, locationPathField)와 버튼(creatNewTreeButton)을 구성한다.
            // 사용자가 버튼을 클릭하면 'CreateNewTree' 메서드가 호출되어, 입력된 이름으로 새 트리를 생성한다.
            
            // 선택된 트리 처리
            if (tree == null) {
                OnSelectionChange();
            } else {
                SelectTree(tree);
            }
            // 만약 현재 선택된 트리(tree)가 없다면, 'OnSelectionChange()' 메서드를 호출하여 선택된 트리를 처리한다.
            // 이미 선택된 트리가 있다면, 'SelectTree(tree)' 를 호출하여 해당 트리를 에디터에 표시한다.
        }
        
        /*=======================================================================================================*/
        
        // OnEnable, OnDisable : 윈도우가 활성화/비황성화 될 때, 호출되어 플레이 모드 상태 변경 이벤트에 대한 구독을 관리한다.
        private void OnEnable() { // 윈도우 에디터가 활성화될 때 호출
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }
        // 'EditorApplication.playModeStateChanged' 이벤트에 대한 구독을 먼저 해제한 후 다시 구독한다.
        // 이는 중복 구독을 방지하기 위함이다. 이 이벤트는 플레이 모드 상태가 변경될 때마다 발생한다.
        private void OnDisable() { // 윈도우 에디터가 비활성화될 때 호출
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
        }
        // 'EditorApplication.playModeStateChanged' 이벤트에 대한 구독을 해제하여, 불필요한 이벤트 호출을 방지한다.
        
        /*=======================================================================================================*/
        
        // 플레이 모드 상태가 변경될 때 호출되는 메서드이다.
        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;
            }
        }
        // 'EnteredEditMode' : 에디터가 플레이 모드에서 나와 에디트 모드로 진입될 때 호출된다.
        // 이 시점에 'OnSelectionChange' 메서드를 호출하여 현재 선택된 행동 트리를 에디터에 표시한다.
        // 'ExitingEditMode' : 에디터가 에디트 모드에서 나가 플레이 모드로 진입하기 직전에 호출된다. 여기서는 별도의 동작을 정의하지 않는다.
        // 'EnteredPlayMode' : 에디터가 에디트 모드에서 플레이 모드로 진입한 직후에 호출된다. 이 시점에서도 'OnSelectionChange' 를 호출한다.
        // 'ExitingPlayMode' : 에디터가 플레이 모드에서 나와 에디트 모드로 돌아가기 직전에 호출된다. 이 상태에서도 별도의 동작은 정의하지 않는다.
        
        /*=======================================================================================================*/
        
        // 에디터에서 다른 객체가 선택될 때 호출되어, 선택된 행동 트리를 에디터에 표시한다.
        // 현재 선택된 객체가 'BehaviorTedd' 인지 혹은, 'BehaviorTreeRunner' 컴포넌트를 가진 게임오브젝트인지를 확인하고, 해당 트리 에디터에 표시한다.
        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);
            };
        }
        // 'EditorApplication.delayCall' 을 사용하여, 현재 실행 중인 모든 처리가 완료된 후에 코드블록을 실행하도록 한다.
        // 이는 데이터의 상태가 안정된 후에 선택된 트리를 처리하기 위함이다.
        // 'Selection.activeObjet' 를 확인하여 현재 선택된 객체가 'BehaviorTree' 타입인지 검사한다.
        // 그렇지 않고 선택된 게임오브젝트가 있을 경우, 해당 오브젝트에서 'BehaviorTreeRunner' 컴포넌트를 찾아 관련된 트리를 확인한다.
        // 확인된 행동 트리를 'SelectTree' 메서드에 전달하여, 에디터에서 해당 트리를 표시하고 작업할 수 있게 한다.
        
        /*=======================================================================================================*/
        
        // 주어진 행동 트리를 선택하고 에디터에 표시하는 메서드이다.
        void SelectTree(BehaviourTree newTree) {

            if (treeView == null) {
                return;
            }

            if (!newTree) {
                return;
            }
            // 'treeView' 가 'null' 이 아니고, 'newTree' 도 'null' 이 아닌 경우에만 작업을 진행한다.

            this.tree = newTree;
            // 'tree' 필드에 'newTree' 를 할당하여 현재 선택된 행동 트리를 업데이트한다.

            overlay.style.visibility = Visibility.Hidden;
            // 'overlay' 의 스타일 속성 중 'visibility' 를 'Hidden' 으로 설정하여, 어떤 오버레이 UI 요소도 숨긴다.

            if (Application.isPlaying) {
                treeView.PopulateView(tree);
            } else {
                treeView.PopulateView(tree);
            }
            // 'treeView.PopulateView(tree)' 를 호출하여 'treeView' 내부에 'newTree' 의 내용을 채운다.
            // 이는 플레이 모드 상태 여부와 관계없이 동일하게 수행된다.

            
            treeObject = new SerializedObject(tree);
            // 'treeObject' 에 'new SerializedObject(tree)' 를 할당하여, 현재 행동 트리에 대한 직렬화된 객체를 생성한다.
            // 이는 인스펙터에서 트리 객체를 수정할 수 있게 한다.
            blackboardProperty = treeObject.FindProperty("blackboard");
            // 'blackboardProperty' 에 'treeObject' 에서 'blackboard' 프로퍼티를 찾아 할당한다.

            EditorApplication.delayCall += () => {
                treeView.FrameAll();
            };
            // 'EditorApplication.delayCall' 을 사용하여 'treeView.FrameAll' 호출을 지연시킨다.
            // 이는 트리 뷰가 모든 노드를 프레임에 맞추도록 한다.
        }
        
        /*=======================================================================================================*/
        
        // 노드가 선택될 때 호출되어, 인스펙터 뷰를 (해당 노드에 대한 정보로)업데이트한다.
        void OnNodeSelectionChanged(NodeView node) {
            inspectorView.UpdateSelection(node);
        }
        // inspectorView.UpdateSelection(node)를 호출하여, 선택된 노드의 세부 정보를 표시한다.
        
        /*=======================================================================================================*/

        // 인스펙터가 업데이트될 필요가 있을 때 주기적으로 호출되는 메서드이다.
        private void OnInspectorUpdate() {
            treeView?.UpdateNodeStates();
        }
        // treeView?.UpdateNodeStates()를 호출하여, 트리 뷰 내 모든 노드의 상태를 업데이트한다.
        // 이는 트리 뷰가 null이 아닐 때만 실행된다.
        
        /*=======================================================================================================*/
        
        // 사용자가 입력한 이름으로 새 행동 트리를 생성하고 저장하는 메서드이다.
        void CreateNewTree(string assetName) {
            string path = System.IO.Path.Combine(locationPathField.value, $"{assetName}.asset");
            // 'System.IO.Path.Combine' 을 사용하여 저장할 경로를 구성한다.
            BehaviourTree tree = ScriptableObject.CreateInstance<BehaviourTree>();
            // 'ScriptableObject.CreateInstance<BehaviourTree>()' 를 호출하여 새 'BehaviorTree' 인스턴스를 생성한다.
            tree.name = treeNameField.ToString();
            // 생성된 트리의 이름을 'treeNameField' 의 값으로 설정한다.
            AssetDatabase.CreateAsset(tree, path);
            // ' AssetDatabase.CreateAsset' 을 사용하여 새 행동 트리를 프로젝트의 에셋으로 저장한다.
            AssetDatabase.SaveAssets();
            // 'AssetDatabase.SaveAssets()' 를 호출하여 변경사항을 저장한다.
            Selection.activeObject = tree;
            // 'Selection.activeObject' 에 새로 생성된 트리를 할당하여, 사용자가 이를 에디터에서 바로 볼 수 있도록 한다.
            EditorGUIUtility.PingObject(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);
        }
    }
    // 프로젝트 내에서 'BehaviorTreeSettings' 타입의 설정 파일을 검색하여 반환한다.
    // 'AssetDatabase.FindAssets' 를 사용하여 탐색하며, 여러 개의 설정 파일이 발견되면 첫 번째 파일을 사용하고,
    // 이에 대한 경고를 로그로 출력한다. 설정 파일이 없을 경우 'null' 을 반환한다.
    
    /*=======================================================================================================*/

    // 성정 파일을 찾아 반환하며, 없을 경우 새로 생성하고 저장한다.
    internal static BehaviourTreeSettings GetOrCreateSettings() {
        var settings = FindSettings();
        if (settings == null) {
            settings = ScriptableObject.CreateInstance<BehaviourTreeSettings>();
            AssetDatabase.CreateAsset(settings, "Assets");
            AssetDatabase.SaveAssets();
        }
        return settings;
    }
    // 'FindSettings' 를 호출하여 설정 파일을 찾는다.
    // 설정 파일이 없을 경우 새로운 'BehaviorTreeSettings' 인스턴스를 생성하고, "Assets" 디텍토리에 저장한 뒤, 이를 반환한다.
    // 이 메서드는 설정 파일이 항상 사용 가능하도록 보장한다.
    
    /*=======================================================================================================*/
    
    // 'GetOrCreateSettungs()' 를 통해 얻은 성정 객체를 'SerializedObject' 로 감싸서 반환한다.
    // 이는 Unity의 인스펙터에서 설정을 수정할 수 있게 해준다.
    internal static SerializedObject GetSerializedSettings() {
        return new SerializedObject(GetOrCreateSettings());
    }
    // "GetOrCreateSettings" 메서드를 통해 얻은 설정 객체를 'SerializedObject' 로 감싸서 반환한다.
    // 이는 Unity의 인스펙터에서 'BehaviorTreeSettings' 객체의 필드를 시각적으로 수정할 수 있게 해준다.
    // 'SerializedObject' 는 Unity 에디터 스크립팅에서 객체의 프로퍼티를 인스펙터 UI와 동기화할 때 사용되는 래퍼(wrapper) 클래스이다.
    
    /*=======================================================================================================*/
}

/*=======================================================================================================*/

/*
 * Unity의 프로젝트 설정 창에 사용자 정의 설정을 추가하기 위한 클래스이다.
 * 이 클래스는 'SettingProvider' 를 사용해 Unity 설정 창에 새로운 섹션을 추가한다.
 *
 * Unity의 'SettingsProvider' API를 활용하여 "Project Settings" 창에 "Behavior Tree" 라는 새로운 설정 섹션을 추가한다.
 * 이 섹션은 'BehaviorTreeSettings' 의 설정을 편집할 수 있는 UI를 제공하며, UIElements 프레임워크를 사용하여 UI를 구성한다.
 */
// 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() {
        // 'SettingsProvider' 생성
        // 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",
            // "Project/MyCustomUIElementsSettings" 경로에 "BehaviorTree" 라벨을 가진 새 'SettingProvider' 인스턴스를 생성한다.
            // 이 설정은 프로젝트 스코프에서만 나타난다.
            
            // activateHandler 설정
            // activateHandler is called when the user clicks on the Settings item in the Settings window.
            activateHandler = (searchContext, rootElement) => {
                var settings = BehaviourTreeSettings.GetSerializedSettings();
                // 사용자가 설정 항목을 클릭할 때 호출될 핸들러를 정의한다.
                // 이 핸들러 내부에서 'BehaviorTreeSettings' 의 직렬화된 설정 객체를 로드하고, UI 구성요소를 동적으로 생성하여 설정 창의 'rootElement' 에 추가한다.
                
                // 타이틀 추가
                // 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);
                // "Behavior Tree Settings" 라는 텍스트를 가진 'Label' 을 생성하고, 스타일 클래스 "tittle" 을 추가한 뒤, 'rootElement' 에 이 타이틀을 추가한다.
                
                // 속성 리스트 컨테이너 추가
                var properties = new VisualElement() {
                    style =
                    {
                        flexDirection = FlexDirection.Column
                    }
                };
                properties.AddToClassList("property-list");
                rootElement.Add(properties);
                // 설정 항목들을 담을 'VisualElement' 컨테이너를 생성하고, 스타일을 설정한 후 'rootElement' 에 추가한다.
                // 이 컨테이너는 'flexDirection' 스타일을 'Column' 으로 설정하여 속성들이 세로로 나열되도록 한다.
                
                // 인스펙터 요소 추가
                properties.Add(new InspectorElement(settings));
                // 'InspectorElement' 를 생성하여 'BehaviorTreeSettings' 의 성정을 시각적으로 표현하고, 이를 속성 리스트 컨테이너에 추가한다.
                // 'InspectorElement' 는 'SerializedObject' 를 받아 해당 객체의 프로퍼티를 UIElement 기반의 인스펙터로 표시할 수 있다.
                
                // 데이터 바인딩
                rootElement.Bind(settings);
                // 'rootElement.Bind(settings)' 를 호출하여, 'rootElement' 와 'settings' 간의 데이터 바인딩을 설정한다.
                // 이를 통해 설정 객체의 변경사항이 UI에 자동으로 반영되고, UI에서의 변경사항이 설정 객체에 저장된다.
            },
        };

        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' 구조체는 스크립트 파일 생성 시 사용될 템플릿 정보를 담는다.
        // 이 구조체는 템플릿 파일(TextAsset), 생성될 파일의 기본 이름(defaultFileName), 파일이 저장될 하위 폴더 경로(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" },
        };
        // 'scriptFileAsset' 배열은 'ScriptTemplate' 구조체의 인스턴스를 여러 개 저장한다.
        // 이 배열은 각각 ActionNode, CompositeNode, DecorateNode 생성 시 사용될 스크립트 템플릿 파일과 그에 대한 기본 파알이름, 저장될 하위 폴더 경로를 정의한다.
        // 이 배열을 통해 사용자가 노드 유형별로 새 스크립트를 쉽게 생성할 수 있도록 한다.
        
        /*=======================================================================================================*/
        
        /// <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;
        }
        // 'BehaviorTreeView' 의 생성자는 행동 트리 에디터의 주요 뷰를 초기화하고, 필요한 조작기(Mainpulator)와 스타일시트를 추가한다.
        // 여기에는 콘텐츠를 확대/축소할 수 있는 'ContentZoomer', 내용을 드래그할 수 있는 'ContentDragger', 더블 클릭으로 특정 동작을 수행하는 'DoubleClickSelection'
        // 선택된 요소를 드래그하는 'SelectionDragger', 영역 선택을 가능하게 하는 'RectangleSelector' 등이 포함된다.
        // 또한 'Undo/Redo' 기능을 위한 이벤트 리스너도 설정된다.
        
        /*=======================================================================================================*/
        
        // Undo나 Redo 작업이 수행될 때, 호출되어 트리 뷰를 새로운 상태로  업데이트 하고, 변경 사항을 저장한다.
        private void OnUndoRedo() {
            PopulateView(tree);
            AssetDatabase.SaveAssets();
        }
        // 'OnUndoRedo' 메서드는 사용자가 Undo나 Redo 작업을 수행할 때 호출된다.
        // 이 메서드는 현재 행동 트리를 'PopulateView' 메서드를 통해 새로운 상태로 업데이트하고, 'AssetDatabase.SaveAssets()' 를 호출하여 변경 사항을 저장한다.
        
        /*=======================================================================================================*/
        
        // 주어진 'Node' 객체에 해당하는 'NodeView'를 찾아 반환한다. 이는 노드의 GUID를 사용하여 검색한다.
        public NodeView FindNodeView(Node node) {
            return GetNodeByGuid(node.guid) as NodeView;
        }
        // 'FindView' 메서드는 주어진 'Node' 객채에 해당하는 'NodeView' 를 찾아 반환한다.
        // 이는 노드의 고유 식별자(GUID)를 사용하여 검색을 수행한다.
        
        /*=======================================================================================================*/
        
        // 주어진 'BehaviorTree' 객체를 사용해 그래프뷰를 채운다.
        // 먼저 기존의 그래프 요소를 모두 삭제한 후, 트리의 모든 노드에 대해 노드 뷰를 생성하고, 노드 간의 연결(Edge)를 생성한다.
        internal void PopulateView(BehaviourTree tree) {
            // 트리 할당 
            this.tree = tree;
            // 매개변수로 받은 'tree' 객체를 'BehaviorTreeView' 의 현재 트리(this.tree)로 설정한다.

            // 그래프 변경 리스너 관리
            // 기존 요소 삭제
            graphViewChanged -= OnGraphViewChanged;
            DeleteElements(graphElements.ToList());
            graphViewChanged += OnGraphViewChanged;
            // 'graphViewChanged' 이벤트에서 'OnGraphViewChanged' 메서드를 해제하고 다시 추가함으로써, 그래프 뷰가 변경될 때마다 적절한 처리가 이루어지도록 한다.
            // 이는 그래프 뷰가 업데이트될 때 발생할 수 있는 이벤트를 관리하기 위한 준비 작업이다.
            // 'DeleteElements' 를 호출하여 'graphElements.ToList()' 로 변환된 현재 그래프 뷰 내의 모든 요소를 삭제한다.
            // 이는 새로운 트리를 표시하기 전에 기존의 모든 노드와 연결을 제거하기 위함이다.

            // 루트 노드 생성
            if (tree.rootNode == null) {
                tree.rootNode = tree.CreateNode(typeof(RootNode))as RootNode;   // <- 다운 캐스팅  (알아두면 좋다. : .GetType() : 실제 자신을 반환)
                EditorUtility.SetDirty(tree);
                AssetDatabase.SaveAssets();
            }
            // 만약 'tree.rootNode' 가 'null' 인 경우, 'tree.CreateNode(typeof(RootNode))' 를 호출하여 새로운 'RootNode' 를 생성하고, 트리의 루트 노드로 설정한다.
            // 이는 행동 트리가 최소 하나의 루트 노드를 가지고 있도록 보장한다.

            // 노드 뷰 생성
            // Creates node view
            tree.nodes.ForEach(n => CreateNodeView(n));
            // 'tree.nodes.ForEach' 를 사용하여 트리에 포함된 모든 노드에 대해 'CreateNodeView' 메서드를 호출한다.
            // 이 과정에서 각 노드를 시각적으로 표현하는 'NodeView' 객체가 생성된다.

            // 연결 생성
            // 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);
                });
            });
            // 각 노드에 대해 'BehaviorTree.GetChildren' 메서드를 호출하여 자식 노드 목록을 얻고, 'ForEach' 루프를 사용하여 각 자식 노드에 대한 연결(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();
        }
        // 연결을 시작하는 포트와 반대 방향(direction)의 포트만 연결 대상으로 고려한다.
        // 이는 입력 포트는 출력 포트와만 연결되고, 출력 포트는 입력 포트와만 연결될 수 있음을 의미한다.
        // 동일한 노드에 속한 포트끼리는 연결되지 않는다.
        // 이는 노드가 자기 자신과 연결되는 것을 방지한다.
        
        /*=======================================================================================================*/
        
        // 그래프 뷰에 변경사항이 발생했을 때 이를 처리한다. 주로 노드 또는 연결선(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);
                    }
                });
            }
            // 'elementsToRemove' 가 null 이 아니면, 각 요소에 대해 타입을 검사하여 'NodeView' 인 경우 트리에서 노드를 삭제하고,
            // '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);
                });
            }
            // 'edgesToCreate' 가 null 이 아니면, 각 'Edge' 에 대해 연결된 노드 간의 부모-자식 관계를 추가한다.
            // 이는 새로운 연결이 그래프에 추가될 때마다 해당 연결을 통해 노드간의 관계가 정의됨을 의미한다.

            // 노드의 자식 정렬
            nodes.ForEach((n) => {
                NodeView view = n as NodeView;
                view.SortChildren();
            });
            // 모든 노드에 대해 'SortChildren' 메서드를 호출하여, 자식 노드들을 정렬한다.
            // 이는 노드들이 그래픽 인터페이스에서 일관되고 직관적인 순서로 표시되도록 한다.

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

            //base.BuildContextualMenu(evt);
            // "base.BuildContextualMenu(evt)" 가 주석 처리된 이유는 'BuildContextualMenu' 메서드를 오버라이드 하면서
            // 기본 구현(base 클래스의 구현)을 호출하지 않기로 결정했기 때문이다.
            
            // 새 스크립트 파일 생성 옵션 추가
            // 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();
            // 'evt.menu.AppendAction' 을 사용하여 "Create Script.../New Action Node, Composite Node, Decorator Node" 등의 메뉴 항목을 추가한다.
            // 각 항목은 'CreateNewScript' 메서드를 호출하며, 이는 'scriptFileAssets' 배열에서 해당 스크립트 템플릿 정보를 사용하여 새 스크립트 파일을 생성한다.

            // 노드 생성 위치 결정
            Vector2 nodePosition = this.ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition);
            // 생성자 메뉴를 호출한 위치를 기반으로 노드를 생성하기 위해, 'ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition)' 를 사용하여 마우스 클릭 위치를 그래프 뷰 내의 좌표로 변환한다.
            // 이 좌표는 'CreateNode' 메서드에 전달되어, 생성된 노드가 사용자가 클릭한 위치에 배치된다.
            {
                // 노드 타입별 메뉴 항목 동적 생성
                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));
                }
            }
            // 메뉴에 노드 추가 옵션을 동적으로 생성하기 위해, 'TypeCache.GetTypesDerivedFrom<ActionNode, CompositeNode, DecoratorNode>' 를 사용하여 각각의 노트 타입을 검색한다.
            // 검색된 각 노드 타입에 대해, 해당 타입의 이름을 메뉴 항목으로 추가하고, 메뉴 항목이 선택될 때 'CreateNode' 메서드를 호출하여 해당 타입의 노드를 생성하고 그래프 뷰에 추가한다.
        }
        
        /*=======================================================================================================*/

        // 주어진 경로의 폴더를 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);
            // 주어진 'path' 문자열의 마지막에 '/' 문자가 있는지 검사한다. 만약 있으면, 이를 제거하여 경로를 정제한다.
            // 이는 'AssetDatabase.LoadAssetAtPath' 메서드에 올바른 경로 형식을 제공하기 위한 사전 준비 작업이다.

            // 객체 로드
            // Load object
            UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object));
            // 'AssetDatabase.LoadAssetAtPath' 메서드를 사용하여 주어진 경로에 해당하는 'UnityEngine.Object' 를 로드한다.
            // 이 메서드는 폴더를 포함한 모든 에셋 타입에 사용될 수 있으며, 여기서는 특히 폴더 객체를 로드하는 데 사용된다.

            // 폴더 선택 및 강조 표시
            // Select the object in the project folder
            Selection.activeObject = obj;
            // Selection.activeObject' 에 로드한 객체를 할당함으로써, 프로젝트 뷰에서 해당 객체(폴더)를 선택 상태로 만든다.
            // 이는 스크립트를 통해 에디터 내에서 사용자의 선택을 변경할 수 있게 해준다.

            // Also flash the folder yellow to highlight it
            EditorGUIUtility.PingObject(obj);
            // 'EditorGUIUtility.PingObject' 메서드를 호출하여, 선택된 객체(폴더)를 프로젝트 뷰에서 강조 표시한다.
            // 이는 폴더가 잠깐동안 노란색으로 깜빡이며, 어디에 위치해 있는지 쉽게 찾을 수 있게 해준다.
        }
        
        /*=======================================================================================================*/
        
        // 사용자가 새 스크립트 파일을 생성할 수 있게 한다.
        // 선택된 템플릿에 따라 스크립트 파일을 생성하고, 해당 파일이 저장될 폴더를 선택한다.
        void CreateNewScript(ScriptTemplate template) {
            SelectFolder($"{settings.newNodeBasePath}/{template.subFolder}");
            // 'SelectFolder' 를 호춣하여 스크립트 파일이 저장된 폴더를 선택하고 강조 표시한다.
            // 이 폴더의 경로는 'settings.newNodeBasePath' 와 'template.subFolder' 를 결합하여 결졍된다.
            var templatePath = AssetDatabase.GetAssetPath(template.templateFile);
            // AssetDatabase.GetAssetPath' 를 사용하여 템플릿 파일의 경로를 얻는다.
            ProjectWindowUtil.CreateScriptAssetFromTemplateFile(templatePath, template.defaultFileName);
            // 'ProjectWindowUtil.CreateScriptAssetFromTemplateFile' 을 호출하여 템플릿 파일을 기반으로 새 스크립트 파일을 생성하고, 이를 지정된 폴더에 저장한다.
            // 파일명은 'template.defaultFileName' 에 지정된 값으로 설정된다.
        }
        
        /*=======================================================================================================*/
        
        // 사용자가 지정한 타입의 새 노드를 생성하고, 이를 그래프 뷰에 추가한다.
        // 노드의 위치는 사용자가 그래프상에서 클릭한 위치에 기반한다.
        void CreateNode(System.Type type, Vector2 position) {
            Node node = tree.CreateNode(type);
            // tree.CreateNode' 를 호출하여 지정된 타입의 새 노드를 생성한다.
            node.position = position;
            // 생성된 노드의 'position' 속성을 사용자가 그래프 상에서 클릭한 위치로 설정한다.
            CreateNodeView(node);
            // 'CreateNodeView' 를 호출하여 생성된 노드에 대한 노드 뷰를 생성하고, 이를 그래프 뷰에 추가한다.
        }
        
        /*=======================================================================================================*/
        
        // 주어진 'Node' 객체를 기반으로 'NodeView' 객체를 생성하고, 이를 'GraphView'에 추가한다. (이 메서드는 노드가 생성)
        void CreateNodeView(Node node) {
            NodeView nodeView = new NodeView(node);
            // 'NodeView' 인스턴스를 생성하고, 생성자에 'node' 객체를 전달한다.
            nodeView.OnNodeSelected = OnNodeSelected;
            // 생성된 'NodeView' 인스턴스의 'OnNodeSelected' 이벤트 핸들러를 현재 클래스의 'OnNodeSelected' 메서드로 설정한다.
            AddElement(nodeView);
            // 'AddElement' 를 호출하여 생성된 'NodeView' 를 그래프 뷰에 추가한다.
        }
        
        /*=======================================================================================================*/
        
        // 그래프 내의 모든 노드 뷰의 상태를 최신 상태로 업데이트한다. 이는 주로 노드의 시각적 표현을 최신 상태로 유지하기 위해 사용된다.
        public void UpdateNodeStates() {
            nodes.ForEach(n => {
                NodeView view = n as NodeView;
                view.UpdateState();
            });
        }
        // 'nodes' 컬렉션에 포함된 모든 노드에 대해 반복하며, 각 노드를 'NodeView' 로 캐스팅한다.
        // 각 'NodeView' 의 'UpdateState' 메서드를 호출하여, 노드 뷰의 시각적 표현을 최신 상태로 업데이트한다.
        // 이는 노드의 상태 변경(예:Running, Success, Failure)이 그래픽에 반영되도록 한다.
        
        /*=======================================================================================================*/
    }
}



➔ 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;
        }
        // 객체가 생성될 때, 'EditorApplication.timeSinceStartup' 을 사용하여 형재 시간을 'time' 필드에 초기화한다.
        // 'EditorApplication.timeSinceStartup' 은 에디터가 시작된 이후로 경과한 시간(초 단위)을 나타낸다.
        // 이 시간은 첫 번째 마우스 다운 이벤트의 타임스팸프로 사용된다.
        
        /*=======================================================================================================*/
        
        // 조작기가 대상(BehaviorTreeView)에 연결될 때 호출된다.
        // 'MouseDownEvent' 에 대한 콜백(OnMouseDown)을 등록하여, 마우스 다운 이벤트를 처리할 수 있게 한다.
        protected override void RegisterCallbacksOnTarget() {
            target.RegisterCallback<MouseDownEvent>(OnMouseDown);
        }
        // 'MouseDownEvent' 에 대한 콜백(OnMouseDown)을 등록하여, 마우스 다운 이벤트가 발생했을 때, 'OnMousedown' 메서드가 호출되도록 한다.
        // 이를 통해 마우스 다운 이벤트를 처리할 수 있게 된다.
        
        /*=======================================================================================================*/
        
        // 조작기가 대상에서 분리될 때 호출된다. 'MouseDownEvent' 에 대한 콜백을 해제한다.
        protected override void UnregisterCallbacksFromTarget() {

            target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
        }
        // 'MouseDownEvent' 에 대한 콜백을 해제하여, 더 이상 'OnMouseDown' 메서드가 마우스 다운 이벤트에 반응하지 않도록 한다.
        
        /*=======================================================================================================*/
        
        // 마우스 다운 이벤트가 발생했을 때 호출된다. 이 메서드는 마지막 마우스 다운 이벤트 이후 시간 간격을 계산하고,
        // 이 간격이 'doubleClickDuration' 보다 작을 경우 'SelectChildren' 메서드를 호출하여 자식 노드들을 선택한다.
        // 그리고 현재 시간을 'time' 필드에 다시 기록한다.
        private void OnMouseDown(MouseDownEvent evt) {
            var graphView = target as BehaviourTreeView;
            if (graphView == null)
                return;
            // 'target' 을 'BehaviorTreeView' 로 캐스팅하여 이벤트가 발생한 대상이 'BehaviorTreeView' 인지 확인한다.
            // 캐스팅에 실패하면 메서드는 실행을 중단한다.

            double duration = EditorApplication.timeSinceStartup - time;
            // 마우스 마지막 다운 이벤트 이후의 시간 간격을 계산하기 위해 현재 시간(EditorApplication.timeSinceStartup)에서 마지막으로 기록된 'time' 값을 뺀다.
            if (duration < doubleClickDuration) {
                SelectChildren(evt);
            }
            // 계산된 시간 간격이 'doubleClickDuration' 보다 작다면, 이는 더블 클릭으로 간주되며, 'SelectChildren' 메서드를 호출아혀 현재 노드의 자식 노드들을 선택한다.

            time = EditorApplication.timeSinceStartup;
            // 마지막으로, 현재 시간을 'time' 필드에 다시 기록하여, 다음 마우스 다운 이벤트를 위한 타임스탬프로 사용한다.
        }
        
        /*=======================================================================================================*/

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

            // 대상 확인
            var graphView = target as BehaviourTreeView;
            if (graphView == null)
                return;
            // 'target' 을 'BehaviorTreeView' 로 캐스팅하여, 이벤트가 발생한 대상이 올바른 타입의 뷰인지 확인한다.
            // 캐스팅에 실패하면 메서드는 실행을 중단한다.

            // 조작 중단 가능성 확인
            if (!CanStopManipulation(evt))
                return;
            // 'CanStopManipulation(evt)' 를 호출하여, 현재 이벤트가 조작을 중단할 수 있는 상태인지 확인한다.
            // 만약 조작을 중단할 수 없다면 메서드 실행을 중단한다. 이는 특정 상황에서 불필요한 이벤트 처리를 방지하기 위함이다.

            // 클릭된 노드의 'NodeView' 확인
            NodeView clickedElement = evt.target as NodeView;
            if (clickedElement == null) {
                var ve = evt.target as VisualElement;
                clickedElement = ve.GetFirstAncestorOfType<NodeView>();
                if (clickedElement == null)
                    return;
            }
            // 마우스 다운 이벤트의 대상(evt.target)을 'NodeView' 로 캐스팅하여, 사용자가 클릭한 노드 뷰를 확인한다.
            // 만약 직접적으로 'NodeView' 를 찾을 수 없다면, 'evt.target'을 'VisualElement' 로 캐스팅하고,
            // 이를 통해 'GetFirstAncestorOfType<NodeView>()' 를 호출하여, 클릭된 요소의 조상 중 'NodeView' 를 찾는다.
            // 이는 클릭된 요소가 노드 뷰의 자식 요소일 경우를 처리하기 위함이다.
            // 클릭된 요소가 'NodeView' 타입이 아니면 메서드 실행을 중단한다.

            // 자식 노드 선택
            // Add children to selection so the root element can be moved
            BehaviourTree.Traverse(clickedElement.node, node => {
                var view = graphView.FindNodeView(node);
                graphView.AddToSelection(view);
            });
            // 'BehaviourTree.Traverse' 메서드를 사용하여, 클릭된 노드(clickedElement.node)부터 시작하여 트리를 순회한다.
            // 이 메서드는 클릭된 노드와 가 자식 노드들을 재귀적으로 방문하며, 각 노드에 대해 지정된 람다 함수를 실행한다.
            // 순회 과정에서 방문하는 각 노드에 대해, 'graphView.FindNodeView(node)' 를 호출하여 해당 노드에 대응하는 노드 뷰를 찾는다.
            // 찾아낸 노드 뷰를 'graphView.AddToSelection(view)' 를 사용하여 선택 목록에 추가한다.
            // 이렇게 함으로써, 클릭된 노드의 모든 자식 노드들이 에디터에서 자동으로 선택되어, 사용자가 루트 요소를 포함한 전체 하위 구조를 쉽게 이동할 수 있게 한다.
        }
        
        /*=======================================================================================================*/
    }
}











📌 코드 출저

TheKiwiCoder Youtube

profile
No Easy Day

0개의 댓글