From 18b47257fdbbbd77e74bff8fc86df498419c94a7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 16 Aug 2023 13:26:33 +0200 Subject: [PATCH] Add **Behavior Tree** asset type and editing --- .../Editor/Content/Proxy/BehaviorTreeProxy.cs | 66 +++ Source/Editor/Editor.cs | 5 + .../Editor/Managed/ManagedEditor.Internal.cpp | 4 + .../Editor/Modules/ContentDatabaseModule.cs | 1 + .../SourceCodeEditing/CodeEditingModule.cs | 6 + Source/Editor/Scripting/TypeUtils.cs | 7 +- .../Editor/Surface/Archetypes/BehaviorTree.cs | 243 ++++++++++ Source/Editor/Surface/BehaviorTreeSurface.cs | 158 +++++++ Source/Editor/Surface/NodeFactory.cs | 7 + Source/Editor/Surface/NodeFlags.cs | 7 +- .../Windows/Assets/BehaviorTreeWindow.cs | 433 ++++++++++++++++++ Source/Engine/AI/BehaviorKnowledge.h | 18 + Source/Engine/AI/BehaviorTree.cpp | 161 +++++++ Source/Engine/AI/BehaviorTree.cs | 42 ++ Source/Engine/AI/BehaviorTree.h | 91 ++++ Source/Engine/AI/BehaviorTreeNode.h | 29 ++ Source/Engine/AI/BehaviorTreeNodes.cpp | 20 + Source/Engine/AI/BehaviorTreeNodes.h | 41 ++ Source/Engine/AI/BehaviorTypes.h | 35 ++ .../AssetsImportingManager.cpp | 3 + .../ContentImporters/AssetsImportingManager.h | 5 + .../ContentImporters/CreateBehaviorTree.h | 36 ++ Source/Engine/Visject/VisjectGraph.h | 2 +- 23 files changed, 1417 insertions(+), 3 deletions(-) create mode 100644 Source/Editor/Content/Proxy/BehaviorTreeProxy.cs create mode 100644 Source/Editor/Surface/Archetypes/BehaviorTree.cs create mode 100644 Source/Editor/Surface/BehaviorTreeSurface.cs create mode 100644 Source/Editor/Windows/Assets/BehaviorTreeWindow.cs create mode 100644 Source/Engine/AI/BehaviorKnowledge.h create mode 100644 Source/Engine/AI/BehaviorTree.cpp create mode 100644 Source/Engine/AI/BehaviorTree.cs create mode 100644 Source/Engine/AI/BehaviorTree.h create mode 100644 Source/Engine/AI/BehaviorTreeNode.h create mode 100644 Source/Engine/AI/BehaviorTreeNodes.cpp create mode 100644 Source/Engine/AI/BehaviorTreeNodes.h create mode 100644 Source/Engine/AI/BehaviorTypes.h create mode 100644 Source/Engine/ContentImporters/CreateBehaviorTree.h 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(); + var nodes = _surface.Nodes; + if (nodes != null) + { + for (var i = 0; i < nodes.Count; i++) + { + if (nodes[i] is Surface.Archetypes.BehaviorTree.Node node && node.IsSelected && node.Instance) + selection.Add(node.Instance); + } + } + _nodePropertiesEditor.Select(selection); + } + + private void OnNodePropertyEdited() + { + _surface.MarkAsEdited(); + var nodes = _surface.Nodes; + for (var i = 0; i < _nodePropertiesEditor.Selection.Count; i++) + { + if (_nodePropertiesEditor.Selection[i] is BehaviorTreeNode instance) + { + // Sync instance data with surface node value storage + for (var j = 0; j < nodes.Count; j++) + { + if (nodes[j] is Surface.Archetypes.BehaviorTree.Node node && node.Instance == instance) + { + node.SetValue(1, FlaxEngine.Json.JsonSerializer.SaveToBytes(instance)); + break; + } + } + } + } + } + + private void UpdateKnowledge() + { + var rootNode = _surface.FindNode(19, 2) as Surface.Archetypes.BehaviorTree.Node; + if (rootNode != null) + rootNode.ValuesChanged += UpdateKnowledge; + var rootInstance = rootNode?.Instance as BehaviorTreeRootNode; + var blackboardType = TypeUtils.GetType(rootInstance?.BlackboardType); + if (blackboardType) + { + var blackboardInstance = blackboardType.CreateInstance(); + _knowledgePropertiesEditor.Select(blackboardInstance); + } + else + { + _knowledgePropertiesEditor.Deselect(); + } + } + + /// + public override void Save() + { + // Early check + if (!IsEdited || _asset == null || _isWaitingForSurfaceLoad) + return; + + // Check if surface has been edited + if (_surface.IsEdited) + { + if (SaveSurface()) + return; + } + + ClearEditedFlag(); + OnSurfaceEditedChanged(); + _item.RefreshThumbnail(); + } + + /// + protected override void UpdateToolstrip() + { + _saveButton.Enabled = _canEdit && IsEdited; + _undoButton.Enabled = _canEdit && _undo.CanUndo; + _redoButton.Enabled = _canEdit && _undo.CanRedo; + + base.UpdateToolstrip(); + } + + /// + protected override void OnAssetLinked() + { + _isWaitingForSurfaceLoad = true; + + base.OnAssetLinked(); + } + + /// + /// Focuses the node. + /// + /// The node. + public void ShowNode(SurfaceNode node) + { + SelectTab(); + RootWindow.Focus(); + Surface.Focus(); + Surface.FocusNode(node); + } + + /// + public Asset SurfaceAsset => Asset; + + /// + public string SurfaceName => "Behavior Tree"; + + /// + public byte[] SurfaceData + { + get => _asset.LoadSurface(); + set + { + // Save data to the asset + if (_asset.SaveSurface(value)) + { + _surface.MarkAsEdited(); + Editor.LogError("Failed to save surface data"); + } + _asset.Reload(); + } + } + + /// + public VisjectSurfaceContext ParentContext => null; + + /// + public void OnContextCreated(VisjectSurfaceContext context) + { + } + + /// + public void OnSurfaceEditedChanged() + { + if (_surface.IsEdited) + MarkAsEdited(); + } + + /// + public void OnSurfaceGraphEdited() + { + } + + /// + public void OnSurfaceClose() + { + Close(); + } + + /// + protected override void UnlinkItem() + { + _isWaitingForSurfaceLoad = false; + + base.UnlinkItem(); + } + + private bool LoadSurface() + { + if (_surface.Load()) + { + Editor.LogError("Failed to load Behavior Tree surface."); + return true; + } + return false; + } + + private bool SaveSurface() + { + _surface.Save(); + return false; + } + + private void SetCanEdit(bool canEdit) + { + if (_canEdit == canEdit) + return; + _canEdit = canEdit; + _undo.Enabled = canEdit; + _surface.CanEdit = canEdit; + _nodePropertiesEditor.ReadOnly = !_canEdit; + _knowledgePropertiesEditor.ReadOnly = true; + UpdateToolstrip(); + } + + /// + public override void OnPlayBegin() + { + base.OnPlayBegin(); + + SetCanEdit(false); + } + + /// + public override void OnPlayEnd() + { + SetCanEdit(true); + + base.OnPlayEnd(); + } + + /// + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (_isWaitingForSurfaceLoad && _asset.IsLoaded) + { + _isWaitingForSurfaceLoad = false; + + if (LoadSurface()) + { + Close(); + return; + } + + // Setup + _undo.Clear(); + _surface.Enabled = true; + _nodePropertiesEditor.BuildLayout(); + _knowledgePropertiesEditor.BuildLayout(); + ClearEditedFlag(); + if (_showWholeGraphOnLoad) + { + _showWholeGraphOnLoad = false; + _surface.ShowWholeGraph(); + } + SurfaceLoaded?.Invoke(); + _knowledgePropertiesEditor.ReadOnly = true; + UpdateKnowledge(); + } + } + + /// + public override bool UseLayoutData => true; + + /// + public override void OnLayoutSerialize(XmlWriter writer) + { + LayoutSerializeSplitter(writer, "Split1", _split1); + LayoutSerializeSplitter(writer, "Split2", _split1); + } + + /// + public override void OnLayoutDeserialize(XmlElement node) + { + LayoutDeserializeSplitter(node, "Split1", _split1); + LayoutDeserializeSplitter(node, "Split2", _split2); + } + + /// + public override void OnLayoutDeserialize() + { + _split1.SplitterValue = 0.7f; + _split2.SplitterValue = 0.5f; + } + + /// + public override void OnDestroy() + { + if (IsDisposing) + return; + _undo.Enabled = false; + _nodePropertiesEditor.Deselect(); + _knowledgePropertiesEditor.Deselect(); + _undo.Clear(); + + base.OnDestroy(); + } + + /// + public IEnumerable NewParameterTypes => Editor.CodeEditing.VisualScriptPropertyTypes.Get(); + + /// + public event Action SurfaceLoaded; + + /// + public void OnParamRenameUndo() + { + } + + /// + public void OnParamEditAttributesUndo() + { + } + + /// + public void OnParamAddUndo() + { + } + + /// + public void OnParamRemoveUndo() + { + } + + /// + public object GetParameter(int index) + { + throw new NotSupportedException(); + } + + /// + public void SetParameter(int index, object value) + { + throw new NotSupportedException(); + } + + /// + public Asset VisjectAsset => Asset; + + /// + public VisjectSurface VisjectSurface => _surface; + } +} diff --git a/Source/Engine/AI/BehaviorKnowledge.h b/Source/Engine/AI/BehaviorKnowledge.h new file mode 100644 index 000000000..95917fb45 --- /dev/null +++ b/Source/Engine/AI/BehaviorKnowledge.h @@ -0,0 +1,18 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Scripting/ScriptingObject.h" + +/// +/// Behavior logic component knowledge data container. Contains blackboard values, sensors data and goals storage for Behavior Tree execution. +/// +API_CLASS() class FLAXENGINE_API BehaviorKnowledge : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorKnowledge, ScriptingObject); + + // TODO: blackboard + // TODO: sensors data + // TODO: goals + // TODO: GetGoal/HasGoal +}; diff --git a/Source/Engine/AI/BehaviorTree.cpp b/Source/Engine/AI/BehaviorTree.cpp new file mode 100644 index 000000000..aea8105a9 --- /dev/null +++ b/Source/Engine/AI/BehaviorTree.cpp @@ -0,0 +1,161 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "BehaviorTree.h" +#include "BehaviorTreeNode.h" +#include "BehaviorTreeNodes.h" +#include "Engine/Content/Factories/BinaryAssetFactory.h" +#include "Engine/Scripting/Scripting.h" +#include "Engine/Serialization/JsonSerializer.h" +#include "Engine/Serialization/MemoryReadStream.h" +#include "Engine/Threading/Threading.h" +#include "FlaxEngine.Gen.h" + +REGISTER_BINARY_ASSET(BehaviorTree, "FlaxEngine.BehaviorTree", false); + +BehaviorTreeGraphNode::~BehaviorTreeGraphNode() +{ + SAFE_DELETE(Instance); +} + +bool BehaviorTreeGraph::Load(ReadStream* stream, bool loadMeta) +{ + if (VisjectGraph::Load(stream, loadMeta)) + return true; + + // Build node instances hierarchy + for (Node& node : Nodes) + { + if (auto* nodeCompound = ScriptingObject::Cast(node.Instance)) + { + for (const GraphBox* childBox : node.Boxes[1].Connections) + { + const Node* child = childBox ? (Node*)childBox->Parent : nullptr; + if (child && child->Instance) + { + nodeCompound->Children.Add(child->Instance); + } + } + } + } + + return false; +} + +void BehaviorTreeGraph::Clear() +{ + VisjectGraph::Clear(); + + Root = nullptr; +} + +bool BehaviorTreeGraph::onNodeLoaded(Node* n) +{ + if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2)) + { + // Create node instance object + ScriptingTypeHandle type = Scripting::FindScriptingType((StringAnsiView)n->Values[0]); + if (!type) + type = Scripting::FindScriptingType(StringAnsi((StringView)n->Values[0])); + if (type) + { + n->Instance = (BehaviorTreeNode*)Scripting::NewObject(type); + const Variant& data = n->Values[1]; + if (data.Type == VariantType::Blob) + JsonSerializer::LoadFromBytes(n->Instance, Span((byte*)data.AsBlob.Data, data.AsBlob.Length), FLAXENGINE_VERSION_BUILD); + + // Find root node + if (!Root && n->Instance && BehaviorTreeRootNode::TypeInitializer == type) + Root = (BehaviorTreeRootNode*)n->Instance; + } + else + { + const String name = n->Values[0].ToString(); + if (name.HasChars()) + LOG(Error, "Missing type '{0}'", name); + } + } + + return VisjectGraph::onNodeLoaded(n); +} + +BehaviorTree::BehaviorTree(const SpawnParams& params, const AssetInfo* info) + : BinaryAsset(params, info) +{ +} + +BytesContainer BehaviorTree::LoadSurface() +{ + if (WaitForLoaded()) + return BytesContainer(); + ScopeLock lock(Locker); + if (!LoadChunks(GET_CHUNK_FLAG(0))) + { + const auto data = GetChunk(0); + BytesContainer result; + result.Copy(data->Data); + return result; + } + LOG(Warning, "\'{0}\' surface data is missing.", ToString()); + return BytesContainer(); +} + +#if USE_EDITOR + +bool BehaviorTree::SaveSurface(const BytesContainer& data) +{ + // Wait for asset to be loaded or don't if last load failed + if (LastLoadFailed()) + { + LOG(Warning, "Saving asset that failed to load."); + } + else if (WaitForLoaded()) + { + LOG(Error, "Asset loading failed. Cannot save it."); + return true; + } + + ScopeLock lock(Locker); + + // Set Visject Surface data + GetOrCreateChunk(0)->Data.Copy(data); + + // Save + AssetInitData assetData; + assetData.SerializedVersion = 1; + if (SaveAsset(assetData)) + { + LOG(Error, "Cannot save \'{0}\'", ToString()); + return true; + } + + return false; +} + +#endif + +Asset::LoadResult BehaviorTree::load() +{ + // Load graph + const auto surfaceChunk = GetChunk(0); + if (surfaceChunk == nullptr) + return LoadResult::MissingDataChunk; + MemoryReadStream surfaceStream(surfaceChunk->Get(), surfaceChunk->Size()); + if (Graph.Load(&surfaceStream, true)) + { + LOG(Warning, "Failed to load graph \'{0}\'", ToString()); + return LoadResult::Failed; + } + + return LoadResult::Ok; +} + +void BehaviorTree::unload(bool isReloading) +{ + // Clear resources + Graph.Clear(); +} + +AssetChunksFlag BehaviorTree::getChunksToPreload() const +{ + return GET_CHUNK_FLAG(0); +} diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs new file mode 100644 index 000000000..fcaf22846 --- /dev/null +++ b/Source/Engine/AI/BehaviorTree.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#if FLAX_EDITOR +using System; +using FlaxEngine.Utilities; +using FlaxEditor.Scripting; +using FlaxEngine.GUI; +#endif + +namespace FlaxEngine +{ + partial class BehaviorTreeRootNode + { +#if FLAX_EDITOR + private static bool IsValidBlackboardType(ScriptType type) + { + if (ScriptType.FlaxObject.IsAssignableFrom(type)) + return false; + if (type.Type != null) + { + if (type.Type.IsDelegate()) + return false; + if (typeof(Control).IsAssignableFrom(type.Type)) + return false; + if (typeof(Attribute).IsAssignableFrom(type.Type)) + return false; + if (type.Type.FullName.StartsWith("FlaxEditor.", StringComparison.Ordinal)) + return false; + } + return !type.IsGenericType && + !type.IsInterface && + !type.IsStatic && + !type.IsAbstract && + !type.IsArray && + !type.IsVoid && + (type.IsClass || type.IsStructure) && + type.IsPublic && + type.CanCreateInstance; + } +#endif + } +} diff --git a/Source/Engine/AI/BehaviorTree.h b/Source/Engine/AI/BehaviorTree.h new file mode 100644 index 000000000..d3af415e4 --- /dev/null +++ b/Source/Engine/AI/BehaviorTree.h @@ -0,0 +1,91 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Content/BinaryAsset.h" +#include "Engine/Visject/VisjectGraph.h" + +class BehaviorTreeNode; +class BehaviorTreeRootNode; + +/// +/// Behavior Tree graph node. +/// +class BehaviorTreeGraphNode : public VisjectGraphNode<> +{ +public: + // Instance of the graph node. + BehaviorTreeNode* Instance = nullptr; + + ~BehaviorTreeGraphNode(); +}; + +/// +/// Behavior Tree graph. +/// +class BehaviorTreeGraph : public VisjectGraph +{ +public: + // Instance of the graph root node. + BehaviorTreeRootNode* Root = nullptr; + + // [VisjectGraph] + bool Load(ReadStream* stream, bool loadMeta) override; + void Clear() override; + bool onNodeLoaded(Node* n) override; +}; + +/// +/// Behavior Tree graph executor runtime. +/// +class BehaviorTreeExecutor : public VisjectExecutor +{ +}; + +/// +/// Behavior Tree asset with AI logic graph. +/// +/// +API_CLASS(NoSpawn, Sealed) class FLAXENGINE_API BehaviorTree : public BinaryAsset +{ + DECLARE_BINARY_ASSET_HEADER(BehaviorTree, 1); + +public: + /// + /// The Behavior Tree graph. + /// + BehaviorTreeGraph Graph; + + /// + /// Tries to load surface graph from the asset. + /// + /// The surface data or empty if failed to load it. + API_FUNCTION() BytesContainer LoadSurface(); + +#if USE_EDITOR + /// + /// Updates the graph surface (save new one, discard cached data, reload asset). + /// + /// Stream with graph data. + /// True if cannot save it, otherwise false. + API_FUNCTION() bool SaveSurface(const BytesContainer& data); +#endif + +public: + // [BinaryAsset] +#if USE_EDITOR + void GetReferences(Array& output) const override + { + // Base + BinaryAsset::GetReferences(output); + + Graph.GetReferences(output); + } +#endif + +protected: + // [BinaryAsset] + LoadResult load() override; + void unload(bool isReloading) override; + AssetChunksFlag getChunksToPreload() const override; +}; diff --git a/Source/Engine/AI/BehaviorTreeNode.h b/Source/Engine/AI/BehaviorTreeNode.h new file mode 100644 index 000000000..6e3e29495 --- /dev/null +++ b/Source/Engine/AI/BehaviorTreeNode.h @@ -0,0 +1,29 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Scripting/SerializableScriptingObject.h" +#include "BehaviorTypes.h" + +/// +/// Base class for Behavior Tree nodes. +/// +API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeNode : public SerializableScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeNode, SerializableScriptingObject); + +public: + /// + /// Node user name (eg. Follow Enemy, or Pick up Weapon). + /// + API_FIELD() String Name; + + // TODO: decorators/conditionals + // TODO: init methods + // TODO: instance data ctor/dtor + // TODO: start/stop methods + // TODO: update method + + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; +}; diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp new file mode 100644 index 000000000..8cfcb4899 --- /dev/null +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -0,0 +1,20 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "BehaviorTreeNodes.h" +#include "Engine/Serialization/Serialization.h" + +void BehaviorTreeNode::Serialize(SerializeStream& stream, const void* otherObj) +{ + SerializableScriptingObject::Serialize(stream, otherObj); + + SERIALIZE_GET_OTHER_OBJ(BehaviorTreeNode); + SERIALIZE(Name); +} + +void BehaviorTreeNode::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + SerializableScriptingObject::Deserialize(stream, modifier); + + Name.Clear(); // Missing Name is assumes as unnamed node + DESERIALIZE(Name); +} diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h new file mode 100644 index 000000000..7a8561a45 --- /dev/null +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -0,0 +1,41 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "BehaviorTreeNode.h" +#include "Engine/Core/Collections/Array.h" + +/// +/// Base class for compound Behavior Tree nodes that composite child nodes. +/// +API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeCompoundNode : public BehaviorTreeNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeCompoundNode, BehaviorTreeNode); + +public: + /// + /// List with all child nodes. + /// + API_FIELD(Readonly) Array> Children; +}; + +/// +/// Sequence node updates all its children as long as they return success. If any child fails, the sequence is failed. +/// +API_CLASS() class FLAXENGINE_API BehaviorTreeSequenceNode : public BehaviorTreeCompoundNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeSequenceNode, BehaviorTreeCompoundNode); +}; + +/// +/// Root node of the behavior tree. Contains logic properties and definitions for the runtime. +/// +API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTreeCompoundNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeRootNode, BehaviorTreeCompoundNode); + API_AUTO_SERIALIZATION(); + + // Full typename of the blackboard data type (structure or class). Spawned for each instance of the behavior. + API_FIELD(Attributes="EditorOrder(0), TypeReference(\"\", \"IsValidBlackboardType\"), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")") + StringAnsi BlackboardType; +}; diff --git a/Source/Engine/AI/BehaviorTypes.h b/Source/Engine/AI/BehaviorTypes.h new file mode 100644 index 000000000..5887460e4 --- /dev/null +++ b/Source/Engine/AI/BehaviorTypes.h @@ -0,0 +1,35 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Scripting/ScriptingType.h" + +class BehaviorTree; +class BehaviorTreeNode; +class BehaviorKnowledge; + +/// +/// Behavior update context state. +/// +API_STRUCT() struct FLAXENGINE_API BehaviorUpdateContext +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(BehaviorUpdateContext); + + /// + /// Simulation time delta (in seconds) since the last update. + /// + float DeltaTime; +}; + +/// +/// Behavior update result. +/// +API_ENUM() enum class BehaviorUpdateResult +{ + // Action completed successfully. + Success, + // Action is still running and active. + Running, + // Action failed. + Failed, +}; diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 52fa8105f..cb366ffb3 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -33,6 +33,7 @@ #include "CreateAnimationGraphFunction.h" #include "CreateVisualScript.h" #include "CreateAnimation.h" +#include "CreateBehaviorTree.h" #include "CreateJson.h" // Tags used to detect asset creation mode @@ -54,6 +55,7 @@ const String AssetsImportingManager::CreateMaterialFunctionTag(TEXT("MaterialFun const String AssetsImportingManager::CreateParticleEmitterFunctionTag(TEXT("ParticleEmitterFunction")); const String AssetsImportingManager::CreateAnimationGraphFunctionTag(TEXT("AnimationGraphFunction")); const String AssetsImportingManager::CreateAnimationTag(TEXT("Animation")); +const String AssetsImportingManager::CreateBehaviorTreeTag(TEXT("BehaviorTree")); const String AssetsImportingManager::CreateVisualScriptTag(TEXT("VisualScript")); class AssetsImportingManagerService : public EngineService @@ -485,6 +487,7 @@ bool AssetsImportingManagerService::Init() { AssetsImportingManager::CreateParticleEmitterFunctionTag, CreateParticleEmitterFunction::Create }, { AssetsImportingManager::CreateAnimationGraphFunctionTag, CreateAnimationGraphFunction::Create }, { AssetsImportingManager::CreateAnimationTag, CreateAnimation::Create }, + { AssetsImportingManager::CreateBehaviorTreeTag, CreateBehaviorTree::Create }, { AssetsImportingManager::CreateVisualScriptTag, CreateVisualScript::Create }, }; AssetsImportingManager::Creators.Add(InBuildCreators, ARRAY_COUNT(InBuildCreators)); diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.h b/Source/Engine/ContentImporters/AssetsImportingManager.h index 2930f8979..eb5058650 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.h +++ b/Source/Engine/ContentImporters/AssetsImportingManager.h @@ -118,6 +118,11 @@ public: /// static const String CreateAnimationTag; + /// + /// The create Behavior Tree asset tag. + /// + static const String CreateBehaviorTreeTag; + /// /// The create visual script asset tag. /// diff --git a/Source/Engine/ContentImporters/CreateBehaviorTree.h b/Source/Engine/ContentImporters/CreateBehaviorTree.h new file mode 100644 index 000000000..feea85d34 --- /dev/null +++ b/Source/Engine/ContentImporters/CreateBehaviorTree.h @@ -0,0 +1,36 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Types.h" + +#if COMPILE_WITH_ASSETS_IMPORTER + +#include "Engine/AI/BehaviorTree.h" + +/// +/// Creating animation utility +/// +class CreateBehaviorTree +{ +public: + static CreateAssetResult Create(CreateAssetContext& context) + { + // Base + IMPORT_SETUP(BehaviorTree, 1); + + // Chunk 0 - Visject Surface + if (context.AllocateChunk(0)) + return CreateAssetResult::CannotAllocateChunk; + { + const BehaviorTreeGraph graph; + MemoryWriteStream stream(64); + graph.Save(&stream, true); + context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); + } + + return CreateAssetResult::Ok; + } +}; + +#endif diff --git a/Source/Engine/Visject/VisjectGraph.h b/Source/Engine/Visject/VisjectGraph.h index 0d841f3d4..5836afb2f 100644 --- a/Source/Engine/Visject/VisjectGraph.h +++ b/Source/Engine/Visject/VisjectGraph.h @@ -216,7 +216,7 @@ public: typedef void (VisjectExecutor::*ProcessBoxHandler)(Box*, Node*, Value&); protected: - ProcessBoxHandler _perGroupProcessCall[19]; + ProcessBoxHandler _perGroupProcessCall[20]; public: ///