diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index e055d584d..7623c9915 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -1,6 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEngine; @@ -17,26 +19,16 @@ namespace FlaxEditor.Surface.Archetypes public static class BehaviorTree { /// - /// Customized for the Behavior Tree node. + /// Base class for Behavior Tree nodes wrapped inside . /// - internal class Node : SurfaceNode + internal class NodeBase : SurfaceNode { - private const float ConnectionAreaMargin = 12.0f; - private const float ConnectionAreaHeight = 12.0f; - - private ScriptType _type; - private InputBox _input; - private OutputBox _output; + protected ScriptType _type; internal bool _isValueEditing; 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) + protected NodeBase(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { } @@ -48,11 +40,13 @@ namespace FlaxEditor.Surface.Archetypes title = title.Substring(12); if (title.EndsWith("Node")) title = title.Substring(0, title.Length - 4); + if (title.EndsWith("Decorator")) + title = title.Substring(0, title.Length - 9); title = Utilities.Utils.GetPropertyNameUI(title); return title; } - private void UpdateTitle() + protected virtual void UpdateTitle() { string title = null; if (Instance != null) @@ -66,34 +60,18 @@ namespace FlaxEditor.Surface.Archetypes var typeName = (string)Values[0]; title = "Missing Type " + typeName; } - if (title != Title) - { - Title = title; - ResizeAuto(); - } + Title = title; } - /// - public override void OnLoaded() + public override void OnLoaded(SurfaceNodeActions action) { - base.OnLoaded(); - - // Setup boxes - _input = (InputBox)GetBox(0); - _output = (OutputBox)GetBox(1); + base.OnLoaded(action); // 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 { @@ -112,6 +90,259 @@ namespace FlaxEditor.Surface.Archetypes { Instance = null; } + + UpdateTitle(); + } + + public override void OnValuesChanged() + { + base.OnValuesChanged(); + + // Skip updating instance when it's being edited by user via UI + if (!_isValueEditing) + { + 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); + } + } + catch (Exception ex) + { + Editor.LogError("Failed to load Behavior Tree node of type " + _type); + Editor.LogWarning(ex); + } + } + + UpdateTitle(); + } + + public override void OnSpawned(SurfaceNodeActions action) + { + base.OnSpawned(action); + + ResizeAuto(); + } + + public override void OnDestroy() + { + if (IsDisposing) + return; + _type = ScriptType.Null; + Object.Destroy(ref Instance); + + base.OnDestroy(); + } + } + + /// + /// Customized for the Behavior Tree node. + /// + internal class Node : NodeBase + { + private const float ConnectionAreaMargin = 12.0f; + private const float ConnectionAreaHeight = 12.0f; + private const float DecoratorsMarginX = 5.0f; + private const float DecoratorsMarginY = 2.0f; + + private InputBox _input; + private OutputBox _output; + internal List _decorators; + + 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 unsafe List DecoratorIds + { + get + { + var result = new List(); + var ids = Values.Length >= 3 ? Values[2] as byte[] : null; + if (ids != null) + { + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + int count = ids.Length / sizeof(uint); + for (int i = 0; i < count; i++) + result.Add(ptr[i]); + } + } + return result; + } + set + { + var ids = new byte[sizeof(uint) * value.Count]; + if (value != null) + { + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + for (var i = 0; i < value.Count; i++) + ptr[i] = value[i]; + } + } + SetValue(2, ids); + } + } + + public unsafe List Decorators + { + get + { + if (_decorators == null) + { + _decorators = new List(); + var ids = Values.Length >= 3 ? Values[2] as byte[] : null; + if (ids != null) + { + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + int count = ids.Length / sizeof(uint); + for (int i = 0; i < count; i++) + { + var decorator = Surface.FindNode(ptr[i]) as Decorator; + if (decorator != null) + _decorators.Add(decorator); + } + } + } + } + return _decorators; + } + set + { + _decorators = null; + var ids = new byte[sizeof(uint) * value.Count]; + if (value != null) + { + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + for (var i = 0; i < value.Count; i++) + ptr[i] = value[i].ID; + } + } + SetValue(2, ids); + } + } + + public override unsafe SurfaceNode[] SealedNodes + { + get + { + SurfaceNode[] result = null; + var ids = Values.Length >= 3 ? Values[2] as byte[] : null; + if (ids != null) + { + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + int count = ids.Length / sizeof(uint); + result = new SurfaceNode[count]; + for (int i = 0; i < count; i++) + { + var decorator = Surface.FindNode(ptr[i]) as Decorator; + if (decorator != null) + result[i] = decorator; + } + } + } + return result; + } + } + + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + if (!Surface.CanEdit) + return; + + menu.AddSeparator(); + + var nodeTypes = Editor.Instance.CodeEditing.BehaviorTreeNodes.Get(); + + if (_input.Enabled) // Root node cannot have decorators + { + var decorators = menu.AddChildMenu("Add Decorator"); + var decoratorType = new ScriptType(typeof(BehaviorTreeDecorator)); + foreach (var nodeType in nodeTypes) + { + if (nodeType != decoratorType && decoratorType.IsAssignableFrom(nodeType)) + { + var button = decorators.ContextMenu.AddButton(GetTitle(nodeType)); + button.Tag = nodeType; + button.TooltipText = Editor.Instance.CodeDocs.GetTooltip(nodeType); + button.ButtonClicked += OnAddDecoratorButtonClicked; + } + } + } + } + + private void OnAddDecoratorButtonClicked(ContextMenuButton button) + { + var nodeType = (ScriptType)button.Tag; + + // Spawn decorator + var decorator = Context.SpawnNode(19, 3, Location, new object[] + { + nodeType.TypeName, + Utils.GetEmptyArray(), + }); + + // Add decorator to the node + var decorators = Decorators; + decorators.Add((Decorator)decorator); + Decorators = decorators; + } + + public override void OnValuesChanged() + { + // Reject cached value + _decorators = null; + + base.OnValuesChanged(); + + ResizeAuto(); + } + + public override void OnLoaded(SurfaceNodeActions action) + { + base.OnLoaded(action); + + // 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; + 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; + } if (Archetype.Flags != flags) { // Apply custom flags @@ -119,18 +350,60 @@ namespace FlaxEditor.Surface.Archetypes Archetype.Flags = flags; } - UpdateTitle(); - } - - /// - public override void OnSpawned() - { - base.OnSpawned(); - ResizeAuto(); } - /// + public override unsafe void OnPasted(Dictionary idsMapping) + { + base.OnPasted(idsMapping); + + // Update decorators + var ids = Values.Length >= 3 ? Values[2] as byte[] : null; + if (ids != null) + { + _decorators = null; + fixed (byte* data = ids) + { + uint* ptr = (uint*)data; + int count = ids.Length / sizeof(uint); + for (int i = 0; i < count; i++) + { + if (idsMapping.TryGetValue(ptr[i], out var id)) + { + // Fix previous parent node to re-apply layout (in case it was forced by spawned decorator) + var decorator = Surface.FindNode(ptr[i]) as Decorator; + var decoratorNode = decorator?.Node; + if (decoratorNode != null) + decoratorNode.ResizeAuto(); + + // Update mapping to the new node + ptr[i] = id; + } + } + } + Values[2] = ids; + ResizeAuto(); + } + } + + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); + + ResizeAuto(); + Surface.NodeDeleted += OnNodeDeleted; + } + + private void OnNodeDeleted(SurfaceNode node) + { + if (node is Decorator decorator && Decorators.Contains(decorator)) + { + // Decorator was spawned (eg. via undo) + _decorators = null; + ResizeAuto(); + } + } + public override void ResizeAuto() { if (Surface == null) @@ -144,74 +417,134 @@ namespace FlaxEditor.Surface.Archetypes height += ConnectionAreaHeight; if (_output != null && _output.Visible) height += ConnectionAreaHeight; + var decorators = Decorators; + foreach (var decorator in decorators) + { + decorator.ResizeAuto(); + height += decorator.Height + DecoratorsMarginY; + width = Mathf.Max(width, decorator.Width + 2 * DecoratorsMarginX); + } 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); + UpdateRectangles(); } - /// protected override void UpdateRectangles() { - base.UpdateRectangles(); - - // Update boxes placement + Rectangle bounds = Bounds; + if (_input != null && _input.Visible) + { + _input.Bounds = new Rectangle(ConnectionAreaMargin, 0, Width - ConnectionAreaMargin * 2, ConnectionAreaHeight); + bounds.Location.Y += _input.Height; + } + var decorators = Decorators; + var indexInParent = IndexInParent; + foreach (var decorator in decorators) + { + decorator.Bounds = new Rectangle(bounds.Location.X + DecoratorsMarginX, bounds.Location.Y, bounds.Width - 2 * DecoratorsMarginX, decorator.Height); + bounds.Location.Y += decorator.Height + DecoratorsMarginY; + if (decorator.IndexInParent < indexInParent) + decorator.IndexInParent = indexInParent + 1; // Push elements above the node + } 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); + _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerSize); + _closeButtonRect = new Rectangle(bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); + _footerRect = new Rectangle(0, _headerRect.Bottom, bounds.Width, footerSize); + if (_output != null && _output.Visible) + _output.Bounds = new Rectangle(ConnectionAreaMargin, bounds.Height - ConnectionAreaHeight, bounds.Width - ConnectionAreaMargin * 2, ConnectionAreaHeight); } - /// - public override void OnValuesChanged() + protected override void OnLocationChanged() { - base.OnValuesChanged(); + base.OnLocationChanged(); - if (_isValueEditing) - { - // Skip updating instance when it's being edited by user via UI - UpdateTitle(); - return; - } + // Sync attached elements placement + UpdateRectangles(); + } + } - try + /// + /// Customized for the Behavior Tree decorator. + /// + internal class Decorator : NodeBase + { + internal static SurfaceNode Create(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + { + return new Decorator(id, context, nodeArch, groupArch); + } + + internal Decorator(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + public Node Node + { + get { - if (Instance != null) + foreach (var node in Surface.Nodes) { - // 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(); + if (node is Node n && n.DecoratorIds.Contains(ID)) + return n; + } + return null; + } + } + + protected override Color FooterColor => Color.Transparent; + + protected override Float2 CalculateNodeSize(float width, float height) + { + return new Float2(width, height + FlaxEditor.Surface.Constants.NodeHeaderSize); + } + + protected override void UpdateRectangles() + { + base.UpdateRectangles(); + + _footerRect = Rectangle.Empty; + } + + protected override void UpdateTitle() + { + var title = Title; + + base.UpdateTitle(); + + // Update parent node on title change + if (title != Title) + Node?.ResizeAuto(); + } + + public override void Draw() + { + base.Draw(); + + // Outline + if (!_isSelected) + { + var style = Style.Current; + var rect = new Rectangle(Float2.Zero, Size); + Render2D.DrawRectangle(rect, style.BorderHighlighted); + } + } + + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); + + if (action == SurfaceNodeActions.Undo) + { + // Update parent node layout when restoring decorator from undo + var node = Node; + if (node != null) + { + node._decorators = null; + node.ResizeAuto(); } } - 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(); } } @@ -222,13 +555,14 @@ namespace FlaxEditor.Surface.Archetypes { new NodeArchetype { - TypeID = 1, + TypeID = 1, // Task Node Create = Node.Create, Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI, DefaultValues = new object[] { string.Empty, // Type Name Utils.GetEmptyArray(), // Instance Data + null, // List of Decorator Nodes IDs }, Size = new Float2(100, 0), Elements = new[] @@ -239,7 +573,7 @@ namespace FlaxEditor.Surface.Archetypes }, new NodeArchetype { - TypeID = 2, + TypeID = 2, // Root Node Create = Node.Create, Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI, DefaultValues = new object[] @@ -254,6 +588,18 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(0, string.Empty, ScriptType.Void, 1), } }, + new NodeArchetype + { + TypeID = 3, // Decorator Node + Create = Decorator.Create, + Flags = NodeFlags.BehaviorTreeGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoMove, + DefaultValues = new object[] + { + string.Empty, // Type Name + Utils.GetEmptyArray(), // Instance Data + }, + Size = new Float2(100, 0), + }, }; } } diff --git a/Source/Editor/Surface/BehaviorTreeSurface.cs b/Source/Editor/Surface/BehaviorTreeSurface.cs index 512a55638..9b201d3f8 100644 --- a/Source/Editor/Surface/BehaviorTreeSurface.cs +++ b/Source/Editor/Surface/BehaviorTreeSurface.cs @@ -71,6 +71,10 @@ namespace FlaxEditor.Surface scriptType == typeof(BehaviorTreeRootNode)) return; + // Nodes-only + if (new ScriptType(typeof(BehaviorTreeDecorator)).IsAssignableFrom(scriptType)) + return; + // Create group archetype var groupKey = new KeyValuePair("Behavior Tree", 19); if (!cache.TryGetValue(groupKey, out var group)) @@ -171,6 +175,7 @@ namespace FlaxEditor.Surface { typeof(BehaviorTreeSubTreeNode).FullName, FlaxEngine.Json.JsonSerializer.SaveToBytes(instance), + null, }); FlaxEngine.Object.Destroy(instance); } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 8213a4dd7..07c024adf 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -135,7 +135,7 @@ namespace FlaxEditor.Windows.Assets { for (var i = 0; i < nodes.Count; i++) { - if (nodes[i] is Surface.Archetypes.BehaviorTree.Node node && node.IsSelected && node.Instance) + if (nodes[i] is Surface.Archetypes.BehaviorTree.NodeBase node && node.IsSelected && node.Instance) selection.Add(node.Instance); } } @@ -153,7 +153,7 @@ namespace FlaxEditor.Windows.Assets // 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) + if (nodes[j] is Surface.Archetypes.BehaviorTree.NodeBase node && node.Instance == instance) { node._isValueEditing = true; node.SetValue(1, FlaxEngine.Json.JsonSerializer.SaveToBytes(instance)); diff --git a/Source/Engine/AI/BehaviorTree.cpp b/Source/Engine/AI/BehaviorTree.cpp index acc089036..3196084dc 100644 --- a/Source/Engine/AI/BehaviorTree.cpp +++ b/Source/Engine/AI/BehaviorTree.cpp @@ -65,7 +65,7 @@ void BehaviorTreeGraph::Clear() bool BehaviorTreeGraph::onNodeLoaded(Node* n) { - if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2)) + if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2 || n->TypeID == 3)) { // Create node instance object ScriptingTypeHandle type = Scripting::FindScriptingType((StringAnsiView)n->Values[0]); @@ -102,6 +102,26 @@ void BehaviorTreeGraph::LoadRecursive(Node& node) NodesStatesSize += node.Instance->GetStateSize(); NodesCount++; + if (node.TypeID == 1 && node.Values.Count() >= 3) + { + // Load node decorators + const auto& decoratorIds = node.Values[2]; + if (decoratorIds.Type.Type == VariantType::Blob && decoratorIds.AsBlob.Length) + { + const Span ids((uint32*)decoratorIds.AsBlob.Data, decoratorIds.AsBlob.Length / sizeof(uint32)); + for (int32 i = 0; i < ids.Length(); i++) + { + Node* decorator = GetNode(ids[i]); + if (decorator && decorator->Instance) + { + ASSERT_LOW_LAYER(decorator->Instance->Is()); + node.Instance->_decorators.Add((BehaviorTreeDecorator*)decorator->Instance); + decorator->Instance->_parent = node.Instance; + LoadRecursive(*decorator); + } + } + } + } if (auto* nodeCompound = ScriptingObject::Cast(node.Instance)) { auto& children = node.Boxes[1].Connections; @@ -116,6 +136,7 @@ void BehaviorTreeGraph::LoadRecursive(Node& node) if (child && child->Instance) { nodeCompound->Children.Add(child->Instance); + child->Instance->_parent = nodeCompound; LoadRecursive(*child); } } diff --git a/Source/Engine/AI/BehaviorTreeNode.h b/Source/Engine/AI/BehaviorTreeNode.h index 1d435dc6c..845879059 100644 --- a/Source/Engine/AI/BehaviorTreeNode.h +++ b/Source/Engine/AI/BehaviorTreeNode.h @@ -2,6 +2,7 @@ #pragma once +#include "Engine/Core/Collections/Array.h" #include "Engine/Scripting/SerializableScriptingObject.h" #include "BehaviorTypes.h" @@ -21,6 +22,11 @@ protected: API_FIELD(ReadOnly) int32 _memoryOffset = 0; // Execution index of the node within tree. API_FIELD(ReadOnly) int32 _executionIndex = -1; + // Parent node that owns this node (parent composite or decorator attachment node). + API_FIELD(ReadOnly) BehaviorTreeNode* _parent = nullptr; + +private: + Array> _decorators; public: /// @@ -74,6 +80,10 @@ public: // Helper utility to update node with state creation/cleanup depending on node relevancy. BehaviorUpdateResult InvokeUpdate(const BehaviorUpdateContext& context); + // Helper utility to make node relevant and init it state. + void BecomeRelevant(const BehaviorUpdateContext& context); + // Helper utility to make node irrelevant and release its state (including any nested nodes). + virtual void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly = false); // [SerializableScriptingObject] void Serialize(SerializeStream& stream, const void* otherObj) override; @@ -87,8 +97,31 @@ public: ASSERT((int32)sizeof(T) <= GetStateSize()); return reinterpret_cast((byte*)memory + _memoryOffset); } +}; -protected: - virtual void InvokeReleaseState(const BehaviorUpdateContext& context); -}; +/// +/// Base class for Behavior Tree node decorators. Decorators can implement conditional filtering or override node logic and execution flow. +/// +API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeDecorator : public BehaviorTreeNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeDecorator, BehaviorTreeNode); + + /// + /// Checks if the node can be updated (eg. decorator can block it depending on the gameplay conditions or its state). + /// + /// Behavior update context data. + /// True if can update, otherwise false to block it. + API_FUNCTION() virtual bool CanUpdate(BehaviorUpdateContext context) + { + return true; + } + + /// + /// Called after node update to post-process result or perform additional action. + /// + /// Behavior update context data. + /// The node update result. Can be modified by the decorator (eg. to force success). + API_FUNCTION() virtual void PostUpdate(BehaviorUpdateContext context, API_PARAM(ref) BehaviorUpdateResult& result) + { + } }; diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index 97b63a05b..4dedc0606 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -42,26 +42,72 @@ bool IsAssignableFrom(const StringAnsiView& to, const StringAnsiView& from) BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& context) { ASSERT_LOW_LAYER(_executionIndex != -1); - BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; - if (relevantNodes.Get(_executionIndex) == false) + const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes; + + // Check decorators if node can be executed + for (BehaviorTreeDecorator* decorator : _decorators) { - // Node becomes relevant so initialize it's state - relevantNodes.Set(_executionIndex, true); - InitState(context.Behavior, context.Memory); + ASSERT_LOW_LAYER(decorator->_executionIndex != -1); + if (relevantNodes.Get(decorator->_executionIndex) == false) + decorator->BecomeRelevant(context); + if (!decorator->CanUpdate(context)) + { + return BehaviorUpdateResult::Failed; + } } + // Make node relevant before update + if (relevantNodes.Get(_executionIndex) == false) + BecomeRelevant(context); + // Node-specific update - const BehaviorUpdateResult result = Update(context); + for (BehaviorTreeDecorator* decorator : _decorators) + { + decorator->Update(context); + } + BehaviorUpdateResult result = Update(context); + for (BehaviorTreeDecorator* decorator : _decorators) + { + decorator->PostUpdate(context, result); + } // Check if node is not relevant anymore if (result != BehaviorUpdateResult::Running) - { - InvokeReleaseState(context); - } + BecomeIrrelevant(context); return result; } +void BehaviorTreeNode::BecomeRelevant(const BehaviorUpdateContext& context) +{ + // Initialize state + BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; + ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == false); + relevantNodes.Set(_executionIndex, true); + InitState(context.Behavior, context.Memory); +} + +void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) +{ + // Release state + BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; + ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == true); + relevantNodes.Set(_executionIndex, false); + ReleaseState(context.Behavior, context.Memory); + + if (nodeOnly) + return; + + // Release decorators + for (BehaviorTreeDecorator* decorator : _decorators) + { + if (relevantNodes.Get(decorator->_executionIndex) == true) + { + decorator->BecomeIrrelevant(context); + } + } +} + void BehaviorTreeNode::Serialize(SerializeStream& stream, const void* otherObj) { SerializableScriptingObject::Serialize(stream, otherObj); @@ -78,13 +124,6 @@ void BehaviorTreeNode::Deserialize(DeserializeStream& stream, ISerializeModifier DESERIALIZE(Name); } -void BehaviorTreeNode::InvokeReleaseState(const BehaviorUpdateContext& context) -{ - BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; - relevantNodes.Set(_executionIndex, false); - ReleaseState(context.Behavior, context.Memory); -} - void BehaviorTreeCompoundNode::Init(BehaviorTree* tree) { for (BehaviorTreeNode* child : Children) @@ -102,18 +141,19 @@ BehaviorUpdateResult BehaviorTreeCompoundNode::Update(BehaviorUpdateContext cont return result; } -void BehaviorTreeCompoundNode::InvokeReleaseState(const BehaviorUpdateContext& context) +void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) { + // Make any nested nodes irrelevant as well const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes; for (BehaviorTreeNode* child : Children) { if (relevantNodes.Get(child->_executionIndex) == true) { - child->InvokeReleaseState(context); + child->BecomeIrrelevant(context); } } - BehaviorTreeNode::InvokeReleaseState(context); + BehaviorTreeNode::BecomeIrrelevant(context, nodeOnly); } int32 BehaviorTreeSequenceNode::GetStateSize() const diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index 723db818c..ffe477542 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -28,7 +28,7 @@ public: protected: // [BehaviorTreeNode] - void InvokeReleaseState(const BehaviorUpdateContext& context) override; + void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) override; }; ///