/*
* 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)' 를 호출하여 프로젝트 뷰에서 새로 생성된 트리를 강조 표시한다.
//
}
/*=======================================================================================================*/
}
/// <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' 클래스는 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)이 그래픽에 반영되도록 한다.
/*=======================================================================================================*/
}
}
/*
* 이 클래스는 '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)' 를 사용하여 선택 목록에 추가한다.
// 이렇게 함으로써, 클릭된 노드의 모든 자식 노드들이 에디터에서 자동으로 선택되어, 사용자가 루트 요소를 포함한 전체 하위 구조를 쉽게 이동할 수 있게 한다.
}
/*=======================================================================================================*/
}
}