diff --git a/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs b/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs
new file mode 100644
index 000000000..33ad0862f
--- /dev/null
+++ b/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using System.IO;
+using FlaxEditor.Content.Thumbnails;
+using FlaxEditor.Windows;
+using FlaxEditor.Windows.Assets;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Content
+{
+ ///
+ /// A asset proxy object.
+ ///
+ ///
+ [ContentContextMenu("New/AI/Behavior Tree")]
+ public class BehaviorTreeProxy : BinaryAssetProxy
+ {
+ ///
+ public override string Name => "Behavior Tree";
+
+ ///
+ public override bool CanReimport(ContentItem item)
+ {
+ return true;
+ }
+
+ ///
+ public override EditorWindow Open(Editor editor, ContentItem item)
+ {
+ return new BehaviorTreeWindow(editor, item as BinaryAssetItem);
+ }
+
+ ///
+ public override Color AccentColor => Color.FromRGB(0x3256A8);
+
+ ///
+ public override Type AssetType => typeof(BehaviorTree);
+
+ ///
+ public override bool CanCreate(ContentFolder targetLocation)
+ {
+ return targetLocation.CanHaveAssets;
+ }
+
+ ///
+ public override void Create(string outputPath, object arg)
+ {
+ if (Editor.CreateAsset(Editor.NewAssetType.BehaviorTree, outputPath))
+ throw new Exception("Failed to create new asset.");
+ }
+
+ ///
+ public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context)
+ {
+ guiRoot.AddChild(new Label
+ {
+ Text = Path.GetFileNameWithoutExtension(request.Asset.Path),
+ Offsets = Margin.Zero,
+ AnchorPreset = AnchorPresets.StretchAll,
+ Wrapping = TextWrapping.WrapWords
+ });
+ }
+ }
+}
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 6f2111e9c..994d94280 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -930,6 +930,11 @@ namespace FlaxEditor
/// The .
///
Animation = 11,
+
+ ///
+ /// The .
+ ///
+ BehaviorTree = 12,
}
///
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index bed9cfb5c..d1abdf8af 100644
--- a/Source/Editor/Managed/ManagedEditor.Internal.cpp
+++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp
@@ -221,6 +221,7 @@ enum class NewAssetType
ParticleEmitterFunction = 9,
AnimationGraphFunction = 10,
Animation = 11,
+ BehaviorTree = 12,
};
DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateAsset(NewAssetType type, MString* outputPathObj)
@@ -264,6 +265,9 @@ DEFINE_INTERNAL_CALL(bool) EditorInternal_CreateAsset(NewAssetType type, MString
case NewAssetType::Animation:
tag = AssetsImportingManager::CreateAnimationTag;
break;
+ case NewAssetType::BehaviorTree:
+ tag = AssetsImportingManager::CreateBehaviorTreeTag;
+ break;
default:
return true;
}
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index 45428b9df..d699883c8 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -1032,6 +1032,7 @@ namespace FlaxEditor.Modules
Proxy.Add(new SkeletonMaskProxy());
Proxy.Add(new GameplayGlobalsProxy());
Proxy.Add(new VisualScriptProxy());
+ Proxy.Add(new BehaviorTreeProxy());
Proxy.Add(new LocalizedStringTableProxy());
Proxy.Add(new FileProxy());
Proxy.Add(new SpawnableJsonAssetProxy());
diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
index c61fad713..d3c2f6bca 100644
--- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
+++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs
@@ -178,6 +178,11 @@ namespace FlaxEditor.Modules.SourceCodeEditing
///
public readonly CachedCustomAnimGraphNodesCollection AnimGraphNodes = new CachedCustomAnimGraphNodesCollection(32, new ScriptType(typeof(AnimationGraph.CustomNodeArchetypeFactoryAttribute)), IsTypeValidScriptingType, HasAssemblyValidScriptingTypes);
+ ///
+ /// The Behavior Tree custom nodes collection.
+ ///
+ public readonly CachedTypesCollection BehaviorTreeNodes = new CachedTypesCollection(64, new ScriptType(typeof(BehaviorTreeNode)), IsTypeValidScriptingType, HasAssemblyValidScriptingTypes);
+
internal CodeEditingModule(Editor editor)
: base(editor)
{
@@ -361,6 +366,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing
Scripts.ClearTypes();
Controls.ClearTypes();
AnimGraphNodes.ClearTypes();
+ BehaviorTreeNodes.ClearTypes();
TypesCleared?.Invoke();
}
diff --git a/Source/Editor/Scripting/TypeUtils.cs b/Source/Editor/Scripting/TypeUtils.cs
index 4f66d6320..570449839 100644
--- a/Source/Editor/Scripting/TypeUtils.cs
+++ b/Source/Editor/Scripting/TypeUtils.cs
@@ -385,7 +385,12 @@ namespace FlaxEngine.Utilities
return type.IsValueType && !type.IsEnum && !type.IsPrimitive;
}
- internal static bool IsDelegate(Type type)
+ ///
+ /// Checks if the input type represents a delegate.
+ ///
+ /// The input type of the object to check.
+ /// Returns true if the input type represents a delegate.
+ public static bool IsDelegate(this Type type)
{
return typeof(MulticastDelegate).IsAssignableFrom(type.BaseType);
}
diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
new file mode 100644
index 000000000..f86bfaecf
--- /dev/null
+++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
@@ -0,0 +1,243 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using FlaxEditor.Scripting;
+using FlaxEditor.Surface.Elements;
+using FlaxEngine;
+using FlaxEngine.GUI;
+using FlaxEngine.Utilities;
+using Object = FlaxEngine.Object;
+
+namespace FlaxEditor.Surface.Archetypes
+{
+ ///
+ /// Contains archetypes for nodes from the Behavior Tree group.
+ ///
+ [HideInEditor]
+ public static class BehaviorTree
+ {
+ ///
+ /// Customized for the Behavior Tree node.
+ ///
+ internal class Node : SurfaceNode
+ {
+ private const float ConnectionAreaMargin = 12.0f;
+ private const float ConnectionAreaHeight = 12.0f;
+
+ private ScriptType _type;
+ private InputBox _input;
+ private OutputBox _output;
+
+ public BehaviorTreeNode Instance;
+
+ internal static SurfaceNode Create(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ {
+ return new Node(id, context, nodeArch, groupArch);
+ }
+
+ internal Node(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ }
+
+ public static string GetTitle(ScriptType scriptType)
+ {
+ var title = scriptType.Name;
+ if (title.StartsWith("BehaviorTree"))
+ title = title.Substring(12);
+ if (title.EndsWith("Node"))
+ title = title.Substring(0, title.Length - 4);
+ title = Utilities.Utils.GetPropertyNameUI(title);
+ return title;
+ }
+
+ private void UpdateTitle()
+ {
+ string title = null;
+ if (Instance != null)
+ {
+ title = Instance.Name;
+ if (string.IsNullOrEmpty(title))
+ title = GetTitle(_type);
+ }
+ else
+ {
+ var typeName = (string)Values[0];
+ title = "Missing Type " + typeName;
+ }
+ if (title != Title)
+ {
+ Title = title;
+ ResizeAuto();
+ }
+ }
+
+ ///
+ public override void OnLoaded()
+ {
+ base.OnLoaded();
+
+ // Setup boxes
+ _input = (InputBox)GetBox(0);
+ _output = (OutputBox)GetBox(1);
+
+ // Setup node type and data
+ var flagsRoot = NodeFlags.NoRemove | NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaPaste;
+ var flags = Archetype.Flags & ~flagsRoot;
+ var typeName = (string)Values[0];
+ _type = TypeUtils.GetType(typeName);
+ if (_type != null)
+ {
+ bool isRoot = _type.Type == typeof(BehaviorTreeRootNode);
+ _input.Enabled = _input.Visible = !isRoot;
+ _output.Enabled = _output.Visible = new ScriptType(typeof(BehaviorTreeCompoundNode)).IsAssignableFrom(_type);
+ if (isRoot)
+ flags |= flagsRoot;
+ TooltipText = Editor.Instance.CodeDocs.GetTooltip(_type);
+ try
+ {
+ // Load node instance from data
+ Instance = (BehaviorTreeNode)_type.CreateInstance();
+ var instanceData = (byte[])Values[1];
+ FlaxEngine.Json.JsonSerializer.LoadFromBytes(Instance, instanceData, Globals.EngineBuildNumber);
+ }
+ catch (Exception ex)
+ {
+ Editor.LogError("Failed to load Behavior Tree node of type " + typeName);
+ Editor.LogWarning(ex);
+ }
+ }
+ else
+ {
+ Instance = null;
+ }
+ if (Archetype.Flags != flags)
+ {
+ // Apply custom flags
+ Archetype = (NodeArchetype)Archetype.Clone();
+ Archetype.Flags = flags;
+ }
+
+ UpdateTitle();
+ }
+
+ ///
+ public override void ResizeAuto()
+ {
+ if (Surface == null)
+ return;
+ var width = 0.0f;
+ var height = 0.0f;
+ var titleLabelFont = Style.Current.FontLarge;
+ width = Mathf.Max(width, 100.0f);
+ width = Mathf.Max(width, titleLabelFont.MeasureText(Title).X + 30);
+ if (_input != null && _input.Visible)
+ height += ConnectionAreaHeight;
+ if (_output != null && _output.Visible)
+ height += ConnectionAreaHeight;
+ Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize);
+ if (_input != null && _input.Visible)
+ _input.Bounds = new Rectangle(ConnectionAreaMargin, 0, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
+ if (_output != null && _output.Visible)
+ _output.Bounds = new Rectangle(ConnectionAreaMargin, Height - ConnectionAreaHeight, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
+ }
+
+ ///
+ protected override void UpdateRectangles()
+ {
+ base.UpdateRectangles();
+
+ // Update boxes placement
+ const float footerSize = FlaxEditor.Surface.Constants.NodeFooterSize;
+ const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderSize;
+ const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin;
+ const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize;
+ _headerRect = new Rectangle(0, 0, Width, headerSize);
+ if (_input != null && _input.Visible)
+ _headerRect.Y += ConnectionAreaHeight;
+ _footerRect = new Rectangle(0, _headerRect.Bottom, Width, footerSize);
+ _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize);
+ }
+
+ ///
+ public override void OnValuesChanged()
+ {
+ base.OnValuesChanged();
+
+ try
+ {
+ if (Instance != null)
+ {
+ // Reload node instance from data
+ var instanceData = (byte[])Values[1];
+ if (instanceData == null || instanceData.Length == 0)
+ {
+ // Recreate instance data to default state if previous state was empty
+ var defaultInstance = (BehaviorTreeNode)_type.CreateInstance(); // TODO: use default instance from native ScriptingType
+ instanceData = FlaxEngine.Json.JsonSerializer.SaveToBytes(defaultInstance);
+ }
+ FlaxEngine.Json.JsonSerializer.LoadFromBytes(Instance, instanceData, Globals.EngineBuildNumber);
+ UpdateTitle();
+ }
+ }
+ catch (Exception ex)
+ {
+ Editor.LogError("Failed to load Behavior Tree node of type " + _type);
+ Editor.LogWarning(ex);
+ }
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ if (IsDisposing)
+ return;
+ _type = ScriptType.Null;
+ Object.Destroy(ref Instance);
+
+ base.OnDestroy();
+ }
+ }
+
+ ///
+ /// The nodes for that group.
+ ///
+ public static NodeArchetype[] Nodes =
+ {
+ new NodeArchetype
+ {
+ TypeID = 1,
+ Create = Node.Create,
+ Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI,
+ DefaultValues = new object[]
+ {
+ string.Empty, // Type Name
+ Utils.GetEmptyArray(), // Instance Data
+ },
+ Size = new Float2(100, 0),
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, ScriptType.Void, 0),
+ NodeElementArchetype.Factory.Output(0, string.Empty, ScriptType.Void, 1),
+ }
+ },
+ new NodeArchetype
+ {
+ TypeID = 2,
+ Create = Node.Create,
+ Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI,
+ DefaultValues = new object[]
+ {
+ typeof(BehaviorTreeRootNode).FullName, // Root node
+ Utils.GetEmptyArray(), // Instance Data
+ },
+ Size = new Float2(100, 0),
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, ScriptType.Void, 0),
+ NodeElementArchetype.Factory.Output(0, string.Empty, ScriptType.Void, 1),
+ }
+ },
+ };
+ }
+}
diff --git a/Source/Editor/Surface/BehaviorTreeSurface.cs b/Source/Editor/Surface/BehaviorTreeSurface.cs
new file mode 100644
index 000000000..30b548a5d
--- /dev/null
+++ b/Source/Editor/Surface/BehaviorTreeSurface.cs
@@ -0,0 +1,158 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using FlaxEditor.Scripting;
+using FlaxEditor.Surface.ContextMenu;
+using FlaxEditor.Surface.Elements;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Surface
+{
+ ///
+ /// The Visject Surface implementation for the Behavior Tree graphs.
+ ///
+ ///
+ [HideInEditor]
+ public class BehaviorTreeSurface : VisjectSurface
+ {
+ private static NodesCache _nodesCache = new NodesCache(IterateNodesCache);
+
+ ///
+ public BehaviorTreeSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo)
+ : base(owner, onSave, undo, CreateStyle())
+ {
+ }
+
+ private static SurfaceStyle CreateStyle()
+ {
+ var editor = Editor.Instance;
+ var style = SurfaceStyle.CreateStyleHandler(editor);
+ style.DrawBox = DrawBox;
+ style.DrawConnection = DrawConnection;
+ return style;
+ }
+
+ private static void DrawBox(Box box)
+ {
+ var rect = new Rectangle(Float2.Zero, box.Size);
+ const float minBoxSize = 5.0f;
+ if (rect.Size.LengthSquared < minBoxSize * minBoxSize)
+ return;
+
+ var style = FlaxEngine.GUI.Style.Current;
+ var color = style.LightBackground;
+ if (box.IsMouseOver)
+ color *= 1.2f;
+ Render2D.FillRectangle(rect, color);
+ }
+
+ private static void DrawConnection(Float2 start, Float2 end, Color color, float thickness)
+ {
+ Archetypes.Animation.StateMachineStateBase.DrawConnection(ref start, ref end, ref color);
+ }
+
+ private void OnActiveContextMenuVisibleChanged(Control activeCM)
+ {
+ _nodesCache.Wait();
+ }
+
+ private static void IterateNodesCache(ScriptType scriptType, Dictionary, GroupArchetype> cache, int version)
+ {
+ // Filter by BT node types only
+ if (!new ScriptType(typeof(BehaviorTreeNode)).IsAssignableFrom(scriptType))
+ return;
+
+ // Skip in-built types
+ if (scriptType == typeof(BehaviorTreeNode) ||
+ scriptType == typeof(BehaviorTreeCompoundNode) ||
+ scriptType == typeof(BehaviorTreeRootNode))
+ return;
+
+ // Create group archetype
+ var groupKey = new KeyValuePair("Behavior Tree", 19);
+ if (!cache.TryGetValue(groupKey, out var group))
+ {
+ group = new GroupArchetype
+ {
+ GroupID = groupKey.Value,
+ Name = groupKey.Key,
+ Color = new Color(70, 220, 181),
+ Tag = version,
+ Archetypes = new List(),
+ };
+ cache.Add(groupKey, group);
+ }
+
+ // Create node archetype
+ var node = (NodeArchetype)Archetypes.BehaviorTree.Nodes[0].Clone();
+ node.DefaultValues[0] = scriptType.TypeName;
+ node.Flags &= ~NodeFlags.NoSpawnViaGUI;
+ node.Title = Archetypes.BehaviorTree.Node.GetTitle(scriptType);
+ node.Description = Editor.Instance.CodeDocs.GetTooltip(scriptType);
+ ((IList)group.Archetypes).Add(node);
+ }
+
+ ///
+ protected override void OnShowPrimaryMenu(VisjectCM activeCM, Float2 location, Box startBox)
+ {
+ activeCM.ShowExpanded = true;
+ _nodesCache.Get(activeCM);
+
+ base.OnShowPrimaryMenu(activeCM, location, startBox);
+
+ activeCM.VisibleChanged += OnActiveContextMenuVisibleChanged;
+ }
+
+ ///
+ public override string GetTypeName(ScriptType type)
+ {
+ if (type == ScriptType.Void)
+ return string.Empty; // Remove `Void` tooltip from connection areas
+ return base.GetTypeName(type);
+ }
+
+ ///
+ public override bool Load()
+ {
+ if (base.Load())
+ return true;
+
+ // Ensure to have Root node created (UI blocks spawning of it)
+ if (RootContext.FindNode(19, 2) == null)
+ {
+ RootContext.SpawnNode(19, 2, Float2.Zero);
+ }
+
+ return false;
+ }
+
+ ///
+ public override bool CanUseNodeType(GroupArchetype groupArchetype, NodeArchetype nodeArchetype)
+ {
+ // Comments
+ if (groupArchetype.GroupID == 7 && nodeArchetype.TypeID == 11)
+ return true;
+
+ // Single root node
+ if (groupArchetype.GroupID == 19 && nodeArchetype.TypeID == 2 && RootContext.FindNode(19, 2) != null)
+ return false;
+
+ // Behavior Tree nodes only
+ return (nodeArchetype.Flags & NodeFlags.BehaviorTreeGraph) != 0 &&
+ groupArchetype.GroupID == 19 &&
+ base.CanUseNodeType(groupArchetype, nodeArchetype);
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ if (IsDisposing)
+ return;
+ _nodesCache.Wait();
+
+ base.OnDestroy();
+ }
+ }
+}
diff --git a/Source/Editor/Surface/NodeFactory.cs b/Source/Editor/Surface/NodeFactory.cs
index 27c850258..0c956f640 100644
--- a/Source/Editor/Surface/NodeFactory.cs
+++ b/Source/Editor/Surface/NodeFactory.cs
@@ -175,6 +175,13 @@ namespace FlaxEditor.Surface
Color = new Color(110, 180, 81),
Archetypes = Archetypes.Collections.Nodes
},
+ new GroupArchetype
+ {
+ GroupID = 19,
+ Name = "Behavior Tree",
+ Color = new Color(70, 220, 181),
+ Archetypes = Archetypes.BehaviorTree.Nodes
+ },
};
///
diff --git a/Source/Editor/Surface/NodeFlags.cs b/Source/Editor/Surface/NodeFlags.cs
index f90154806..52590bf2e 100644
--- a/Source/Editor/Surface/NodeFlags.cs
+++ b/Source/Editor/Surface/NodeFlags.cs
@@ -68,9 +68,14 @@ namespace FlaxEditor.Surface
///
NoSpawnViaPaste = 512,
+ ///
+ /// Node can be used in the Behavior Tree graphs.
+ ///
+ BehaviorTreeGraph = 1024,
+
///
/// Node can be used in the all visual graphs.
///
- AllGraphs = MaterialGraph | ParticleEmitterGraph | AnimGraph | VisualScriptGraph,
+ AllGraphs = MaterialGraph | ParticleEmitterGraph | AnimGraph | VisualScriptGraph | BehaviorTreeGraph,
}
}
diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
new file mode 100644
index 000000000..f2dde24a8
--- /dev/null
+++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
@@ -0,0 +1,433 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Xml;
+using FlaxEditor.Content;
+using FlaxEditor.CustomEditors;
+using FlaxEditor.GUI;
+using FlaxEditor.Scripting;
+using FlaxEditor.Surface;
+using FlaxEngine;
+using FlaxEngine.GUI;
+using FlaxEngine.Utilities;
+
+namespace FlaxEditor.Windows.Assets
+{
+ ///
+ /// Behavior Tree window allows to view and edit asset.
+ ///
+ ///
+ ///
+ public sealed class BehaviorTreeWindow : AssetEditorWindowBase, IVisjectSurfaceWindow
+ {
+ private readonly SplitPanel _split1;
+ private readonly SplitPanel _split2;
+ private CustomEditorPresenter _nodePropertiesEditor;
+ private CustomEditorPresenter _knowledgePropertiesEditor;
+ private BehaviorTreeSurface _surface;
+ private Undo _undo;
+ private readonly ToolStripButton _saveButton;
+ private readonly ToolStripButton _undoButton;
+ private readonly ToolStripButton _redoButton;
+ private bool _showWholeGraphOnLoad = true;
+ private bool _isWaitingForSurfaceLoad;
+ private bool _canEdit = true;
+
+ ///
+ /// Gets the Visject Surface.
+ ///
+ public BehaviorTreeSurface Surface => _surface;
+
+ ///
+ /// Gets the undo history context for this window.
+ ///
+ public Undo Undo => _undo;
+
+ ///
+ public BehaviorTreeWindow(Editor editor, BinaryAssetItem item)
+ : base(editor, item)
+ {
+ var isPlayMode = Editor.IsPlayMode;
+
+ // Undo
+ _undo = new Undo();
+ _undo.UndoDone += OnUndoRedo;
+ _undo.RedoDone += OnUndoRedo;
+ _undo.ActionDone += OnUndoRedo;
+
+ // Split Panels
+ _split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None)
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
+ SplitterValue = 0.7f,
+ Parent = this
+ };
+ _split2 = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical, ScrollBars.Vertical)
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = Margin.Zero,
+ SplitterValue = 0.5f,
+ Parent = _split1.Panel2
+ };
+
+ // Surface
+ _surface = new BehaviorTreeSurface(this, Save, _undo)
+ {
+ Parent = _split1.Panel1,
+ Enabled = false
+ };
+ _surface.SelectionChanged += OnNodeSelectionChanged;
+
+ // Properties editors
+ _nodePropertiesEditor = new CustomEditorPresenter(null); // Surface handles undo for nodes editing
+ _nodePropertiesEditor.Features = FeatureFlags.UseDefault;
+ _nodePropertiesEditor.Panel.Parent = _split2.Panel1;
+ _nodePropertiesEditor.Modified += OnNodePropertyEdited;
+ _knowledgePropertiesEditor = new CustomEditorPresenter(null, "No blackboard type assigned"); // No undo for knowledge editing
+ _knowledgePropertiesEditor.Features = FeatureFlags.None;
+ _knowledgePropertiesEditor.Panel.Parent = _split2.Panel2;
+
+ // Toolstrip
+ _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
+ _toolstrip.AddSeparator();
+ _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
+ _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _toolstrip.AddSeparator();
+ _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)");
+ _toolstrip.AddButton(editor.Icons.CenterView64, _surface.ShowWholeGraph).LinkTooltip("Show whole graph");
+
+ // Setup input actions
+ InputActions.Add(options => options.Undo, _undo.PerformUndo);
+ InputActions.Add(options => options.Redo, _undo.PerformRedo);
+ InputActions.Add(options => options.Search, Editor.ContentFinding.ShowSearch);
+
+ SetCanEdit(!isPlayMode);
+ }
+
+ private void OnUndoRedo(IUndoAction action)
+ {
+ MarkAsEdited();
+ UpdateToolstrip();
+ _nodePropertiesEditor.BuildLayoutOnUpdate();
+ }
+
+ private void OnNodeSelectionChanged()
+ {
+ // Select node instances to view/edit
+ var selection = new List