Unity Editor에서 행동 트리를 시각적으로 편집하고 관리할 수 있는 커스텀 에디터 환경을 구성한다.
각각의 스크립트는 특정 기능을 담당하며, 서로 상호작용하여 사용자가 행동 트리를 쉽게 생성, 편집, 관리할 수 있게 한다.
AI의 행동 로직을 시각적으로 구성하고, 게임 내에서 다양한 AI 행동을 쉽게 구현할 수 있다.
GraphView, UXML 등을 이용한 Custom Editor는 아예 처음 접해보는 새로운 것이었다. 그래서 코드를 보면 처음보는 유니티 제공 클래스들 그 클래스의 내장 메서드들 모르는 것들 투성이었다. 그래서 내가 제대로 이해하고 사용하여 수정하면서 쓰기 위해 정확히 어떤식으로작동하는지 내부 로직을 이해하기 위해서 간단하게 분석하였다.
다음 날은 더 세부적으로 분석할 예정이다.
BehaviorTreeEditor : 행동 트리를 편집할 수 있는 메인 에디터 윈도우를 제공한다. 사용자가 행동 트리를 생성, 선택, 편집할 수 있는 UI를 포함한다.
BehaviorTreeSettings : 행동 트리 에디터의 설정을 저장한다. 에디터의 UI 구성, 스타일, 노드 템플릿 등을 정의한다.
BehaviorTreeView : 행동 트리의 구조를 시각적으로 표현하는 뷰이다. 노드 간 연결을 만들고, 노드를 배치하는 등의 작업을 할 수 있다.
DoubleClickSelection : 노드를 더블 클릭했을 때 특정 동작을 수행하는 마우스 조작기이다. 주로 노드와 그 자식 노드들을 선택하는 데 사용된다.
InspectorView : 선택된 노드의 세부 정보를 표시하고 편집할 수 있는 인스펙터 뷰를 제공한다.
NodeView : 개별 노드를 시각적으로 표현한다. 노드의 타입, 상태, 연결 포트 등을 표시한다.
SplitView : 두 개의 패널을 가진 분할 뷰를 제공한다. 예를 들어, 하나의 패널에서 행동 트리를 표시하고, 다른 패널에서 세부 정보를 표시하는 데 사용될 수 있다.
BehaviorTree : 행동 트리의 데이터 구조를 정의한다. 루트 노드와 모든 자식 노드들, 그리고 블랙보드를 포함한다.
NodePort : 노드 간 연결을 생성할 때 사용되는 포트를 정의한다. 연결의 방향과 용량을 지정할 수 있다.
BehahvorTreeEditor는 사용자의 행동에 반응하여 BehaviorTree 객체를 생성하거나 선택하고, 이를 BehaviorTreeView에 표시한다. 또한, BehaviorTreeSettings를 참조하여 에디터의 설정을 불러온다.
BehaviorTreeView는 NodeView 인스턴스를 사용하여 트리의 각 노드를 시각적으로 표현하며, DoubleClickSelection 및 NodePort를 사용하여 노드 간 연결을 관리한다.
사용자가 노드를 선택하면, InpectorView가 활설화되어 선택된 노드의 세부 정보를 NodeView에서 가져와 표시하고 편집할 수 있게 한다.
SplitView는 필요에 따라 BehaviorTreeEditor 내에서 다양한 뷰(예:BehaviorTreeView 와 InpectorView)를 동시에 표시하는 데 사영된다.
NodePort는 NodeView 내에서 정의되어, 노드 간의 연결을 생성하고 관리하는 인터페이스를 제공한다. 이 때, DefauleEdgeConnectorListener와 같은 리스너가 연결의 생성 및 삭제 로직을 처리한다.
/*
* 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);
}
}
}
/// <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' 클래스는 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();
});
}
}
}
/*
* 이 클래스는 '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);
});
}
}
}
/*
* 이 코드는 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' 클래스를 정의하고 있으며, 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;
}
}
}
}
}
/*
* 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' 커스텀 컴포넌트를 효과적으로 활용할 수 있다.
*/
}
}
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
}
}
/*
* 이 코드는 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);
}
}
}