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;
};
///