diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
index 27fb62bcd..15f154617 100644
--- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs
+++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
@@ -29,6 +29,8 @@ namespace FlaxEditor.Surface.Archetypes
protected const float DecoratorsMarginX = 5.0f;
protected const float DecoratorsMarginY = 2.0f;
+ protected string _debugInfo;
+ protected Float2 _debugInfoSize;
protected ScriptType _type;
internal bool _isValueEditing;
@@ -52,6 +54,21 @@ namespace FlaxEditor.Surface.Archetypes
return title;
}
+ public virtual void UpdateDebug(Behavior behavior)
+ {
+ BehaviorTreeNode instance = null;
+ if (behavior)
+ {
+ // Try to use instance from the currently debugged behavior
+ // TODO: support nodes from nested trees
+ instance = behavior.Tree.GetNodeInstance(ID);
+ }
+ var size = _debugInfoSize;
+ UpdateDebugInfo(instance, behavior);
+ if (size != _debugInfoSize)
+ ResizeAuto();
+ }
+
protected virtual void UpdateTitle()
{
string title = null;
@@ -69,6 +86,21 @@ namespace FlaxEditor.Surface.Archetypes
Title = title;
}
+ protected virtual void UpdateDebugInfo(BehaviorTreeNode instance = null, Behavior behavior = null)
+ {
+ _debugInfo = null;
+ _debugInfoSize = Float2.Zero;
+ if (!instance)
+ instance = Instance;
+ if (instance)
+ {
+ // Get debug description for the node based on the current settings
+ _debugInfo = Behavior.GetNodeDebugInfo(instance, behavior);
+ if (!string.IsNullOrEmpty(_debugInfo))
+ _debugInfoSize = Style.Current.FontSmall.MeasureText(_debugInfo);
+ }
+ }
+
public override void OnLoaded(SurfaceNodeActions action)
{
base.OnLoaded(action);
@@ -97,6 +129,7 @@ namespace FlaxEditor.Surface.Archetypes
Instance = null;
}
+ UpdateDebugInfo();
UpdateTitle();
}
@@ -129,6 +162,7 @@ namespace FlaxEditor.Surface.Archetypes
}
}
+ UpdateDebugInfo();
UpdateTitle();
}
@@ -139,10 +173,23 @@ namespace FlaxEditor.Surface.Archetypes
ResizeAuto();
}
+ public override void Draw()
+ {
+ base.Draw();
+
+ // Debug Info
+ if (!string.IsNullOrEmpty(_debugInfo))
+ {
+ var style = Style.Current;
+ Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 4, _debugInfoSize), style.Foreground);
+ }
+ }
+
public override void OnDestroy()
{
if (IsDisposing)
return;
+ _debugInfo = null;
_type = ScriptType.Null;
FlaxEngine.Object.Destroy(ref Instance);
@@ -258,6 +305,7 @@ namespace FlaxEditor.Surface.Archetypes
{
get
{
+ // Return decorator nodes attached to this node to be moved/copied/pasted as a one
SurfaceNode[] result = null;
var ids = Values.Length >= 3 ? Values[2] as byte[] : null;
if (ids != null)
@@ -425,6 +473,11 @@ namespace FlaxEditor.Surface.Archetypes
var titleLabelFont = Style.Current.FontLarge;
width = Mathf.Max(width, 100.0f);
width = Mathf.Max(width, titleLabelFont.MeasureText(Title).X + 30);
+ if (_debugInfoSize.X > 0)
+ {
+ width = Mathf.Max(width, _debugInfoSize.X + 8.0f);
+ height += _debugInfoSize.Y + 8.0f;
+ }
if (_input != null && _input.Visible)
height += ConnectionAreaHeight;
if (_output != null && _output.Visible)
@@ -463,9 +516,12 @@ namespace FlaxEditor.Surface.Archetypes
const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize;
_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);
+ _footerRect = new Rectangle(0, bounds.Height - footerSize, bounds.Width, footerSize);
if (_output != null && _output.Visible)
+ {
+ _footerRect.Y -= ConnectionAreaHeight;
_output.Bounds = new Rectangle(ConnectionAreaMargin, bounds.Height - ConnectionAreaHeight, bounds.Width - ConnectionAreaMargin * 2, ConnectionAreaHeight);
+ }
}
protected override void OnLocationChanged()
@@ -589,7 +645,12 @@ namespace FlaxEditor.Surface.Archetypes
protected override Float2 CalculateNodeSize(float width, float height)
{
- return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize + 2 * DecoratorsMarginX, height + FlaxEditor.Surface.Constants.NodeHeaderSize);
+ if (_debugInfoSize.X > 0)
+ {
+ width = Mathf.Max(width, _debugInfoSize.X + 8.0f);
+ height += _debugInfoSize.Y + 8.0f;
+ }
+ return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderSize);
}
protected override void UpdateRectangles()
@@ -603,15 +664,22 @@ namespace FlaxEditor.Surface.Archetypes
protected override void UpdateTitle()
{
- var title = Title;
-
- base.UpdateTitle();
-
// Update parent node on title change
+ var title = Title;
+ base.UpdateTitle();
if (title != Title)
Node?.ResizeAuto();
}
+ protected override void UpdateDebugInfo(BehaviorTreeNode instance, Behavior behavior)
+ {
+ // Update parent node on debug text change
+ var debugInfoSize = _debugInfoSize;
+ base.UpdateDebugInfo(instance, behavior);
+ if (debugInfoSize != _debugInfoSize)
+ Node?.ResizeAuto();
+ }
+
public override void OnLoaded(SurfaceNodeActions action)
{
// Add drag button to reorder decorator
diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
index 3f0694bc6..2f466be17 100644
--- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
+++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs
@@ -449,6 +449,18 @@ namespace FlaxEditor.Windows.Assets
UpdateKnowledge();
}
+ private void UpdateDebugInfos(bool playMode)
+ {
+ var behavior = playMode ? (Behavior)_behaviorPicker.Value : null;
+ if (!behavior)
+ behavior = null;
+ foreach (var e in _surface.Nodes)
+ {
+ if (e is Surface.Archetypes.BehaviorTree.NodeBase node)
+ node.UpdateDebug(behavior);
+ }
+ }
+
///
public override void OnPlayBegin()
{
@@ -461,6 +473,7 @@ namespace FlaxEditor.Windows.Assets
public override void OnPlayEnd()
{
SetCanEdit(true);
+ UpdateDebugInfos(false);
base.OnPlayEnd();
}
@@ -520,6 +533,12 @@ namespace FlaxEditor.Windows.Assets
// Update behavior debugging
SetCanDebug(Editor.IsPlayMode && _behaviorPicker.Value);
+ // Update debug info texts on all nodes
+ if (Editor.IsPlayMode)
+ {
+ UpdateDebugInfos(true);
+ }
+
base.Update(deltaTime);
}
diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp
index 19102063e..74d48ddb0 100644
--- a/Source/Engine/AI/Behavior.cpp
+++ b/Source/Engine/AI/Behavior.cpp
@@ -166,3 +166,25 @@ void Behavior::OnDisable()
{
BehaviorServiceInstance.UpdateList.Remove(this);
}
+
+#if USE_EDITOR
+
+String Behavior::GetNodeDebugInfo(BehaviorTreeNode* node, Behavior* behavior)
+{
+ if (!node)
+ return String::Empty;
+ BehaviorUpdateContext context;
+ Platform::MemoryClear(&context, sizeof(context));
+ if (behavior && behavior->_knowledge.RelevantNodes.Get(node->_executionIndex))
+ {
+ // Pass behavior and knowledge data only for relevant nodes to properly access it
+ context.Behavior = behavior;
+ context.Knowledge = &behavior->_knowledge;
+ context.Memory = behavior->_knowledge.Memory;
+ context.RelevantNodes = &behavior->_knowledge.RelevantNodes;
+ context.Time = behavior->_totalTime;
+ }
+ return node->GetDebugInfo(context);
+}
+
+#endif
diff --git a/Source/Engine/AI/Behavior.h b/Source/Engine/AI/Behavior.h
index 37b9c1c44..c061fcb5a 100644
--- a/Source/Engine/AI/Behavior.h
+++ b/Source/Engine/AI/Behavior.h
@@ -27,7 +27,6 @@ private:
float _accumulatedTime = 0.0f;
float _totalTime = 0.0f;
BehaviorUpdateResult _result = BehaviorUpdateResult::Success;
- void* _memory = nullptr;
void UpdateAsync();
@@ -91,4 +90,10 @@ public:
// [Script]
void OnEnable() override;
void OnDisable() override;
+
+private:
+#if USE_EDITOR
+ // Editor-only utility to debug nodes state.
+ API_FUNCTION(Internal) static String GetNodeDebugInfo(BehaviorTreeNode* node, Behavior* behavior);
+#endif
};
diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp
index 05eb84596..a33738d31 100644
--- a/Source/Engine/AI/BehaviorKnowledge.cpp
+++ b/Source/Engine/AI/BehaviorKnowledge.cpp
@@ -119,12 +119,12 @@ bool AccessBehaviorKnowledge(BehaviorKnowledge* knowledge, const StringAnsiView&
return false;
}
-bool BehaviorKnowledgeSelectorAny::Set(BehaviorKnowledge* knowledge, const Variant& value)
+bool BehaviorKnowledgeSelectorAny::Set(BehaviorKnowledge* knowledge, const Variant& value) const
{
return knowledge && knowledge->Set(Path, value);
}
-Variant BehaviorKnowledgeSelectorAny::Get(BehaviorKnowledge* knowledge)
+Variant BehaviorKnowledgeSelectorAny::Get(const BehaviorKnowledge* knowledge) const
{
Variant value;
if (knowledge)
@@ -132,7 +132,7 @@ Variant BehaviorKnowledgeSelectorAny::Get(BehaviorKnowledge* knowledge)
return value;
}
-bool BehaviorKnowledgeSelectorAny::TryGet(BehaviorKnowledge* knowledge, Variant& value)
+bool BehaviorKnowledgeSelectorAny::TryGet(const BehaviorKnowledge* knowledge, Variant& value) const
{
return knowledge && knowledge->Get(Path, value);
}
@@ -182,9 +182,9 @@ void BehaviorKnowledge::FreeMemory()
Tree = nullptr;
}
-bool BehaviorKnowledge::Get(const StringAnsiView& path, Variant& value)
+bool BehaviorKnowledge::Get(const StringAnsiView& path, Variant& value) const
{
- return AccessBehaviorKnowledge(this, path, value, false);
+ return AccessBehaviorKnowledge(const_cast(this), path, value, false);
}
bool BehaviorKnowledge::Set(const StringAnsiView& path, const Variant& value)
diff --git a/Source/Engine/AI/BehaviorKnowledge.h b/Source/Engine/AI/BehaviorKnowledge.h
index 42d391347..eb1bd0f3d 100644
--- a/Source/Engine/AI/BehaviorKnowledge.h
+++ b/Source/Engine/AI/BehaviorKnowledge.h
@@ -67,7 +67,7 @@ public:
/// Selector path.
/// Result value (valid only when returned true).
/// True if got value, otherwise false.
- API_FUNCTION() bool Get(const StringAnsiView& path, API_PARAM(Out) Variant& value);
+ API_FUNCTION() bool Get(const StringAnsiView& path, API_PARAM(Out) Variant& value) const;
///
/// Sets the knowledge item value via selector path.
diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.h b/Source/Engine/AI/BehaviorKnowledgeSelector.h
index d27f5e963..2ce4623f5 100644
--- a/Source/Engine/AI/BehaviorKnowledgeSelector.h
+++ b/Source/Engine/AI/BehaviorKnowledgeSelector.h
@@ -21,13 +21,13 @@ API_STRUCT(NoDefault, MarshalAs=StringAnsi) struct FLAXENGINE_API BehaviorKnowle
API_FIELD() StringAnsi Path;
// Sets the selected knowledge value (as Variant).
- bool Set(BehaviorKnowledge* knowledge, const Variant& value);
+ bool Set(BehaviorKnowledge* knowledge, const Variant& value) const;
// Gets the selected knowledge value (as Variant).
- Variant Get(BehaviorKnowledge* knowledge);
+ Variant Get(const BehaviorKnowledge* knowledge) const;
// Tries to get the selected knowledge value (as Variant). Returns true if got value, otherwise false.
- bool TryGet(BehaviorKnowledge* knowledge, Variant& value);
+ bool TryGet(const BehaviorKnowledge* knowledge, Variant& value) const;
FORCE_INLINE bool operator==(const BehaviorKnowledgeSelectorAny& other) const
{
@@ -63,19 +63,19 @@ API_STRUCT(InBuild, Template, MarshalAs=StringAnsi) struct FLAXENGINE_API Behavi
using BehaviorKnowledgeSelectorAny::TryGet;
// Sets the selected knowledge value (typed).
- FORCE_INLINE void Set(BehaviorKnowledge* knowledge, const T& value)
+ FORCE_INLINE void Set(BehaviorKnowledge* knowledge, const T& value) const
{
BehaviorKnowledgeSelectorAny::Set(knowledge, Variant(value));
}
// Gets the selected knowledge value (typed).
- FORCE_INLINE T Get(BehaviorKnowledge* knowledge)
+ FORCE_INLINE T Get(const BehaviorKnowledge* knowledge) const
{
return TVariantValueCast::Cast(BehaviorKnowledgeSelectorAny::Get(knowledge));
}
// Tries to get the selected knowledge value (typed). Returns true if got value, otherwise false.
- FORCE_INLINE bool TryGet(BehaviorKnowledge* knowledge, T& value)
+ FORCE_INLINE bool TryGet(const BehaviorKnowledge* knowledge, T& value)
{
Variant variant;
if (BehaviorKnowledgeSelectorAny::TryGet(knowledge, variant))
diff --git a/Source/Engine/AI/BehaviorTree.cpp b/Source/Engine/AI/BehaviorTree.cpp
index 025c2c22d..e9014813e 100644
--- a/Source/Engine/AI/BehaviorTree.cpp
+++ b/Source/Engine/AI/BehaviorTree.cpp
@@ -17,6 +17,8 @@
REGISTER_BINARY_ASSET(BehaviorTree, "FlaxEngine.BehaviorTree", false);
+#define IS_BT_NODE(n) (n.GroupID == 19 && (n.TypeID == 1 || n.TypeID == 2 || n.TypeID == 3))
+
bool SortBehaviorTreeChildren(GraphBox* const& a, GraphBox* const& b)
{
// Sort by node X coordinate on surface
@@ -45,7 +47,8 @@ void BehaviorTreeGraph::Clear()
bool BehaviorTreeGraph::onNodeLoaded(Node* n)
{
- if (n->GroupID == 19 && (n->TypeID == 1 || n->TypeID == 2 || n->TypeID == 3))
+ const Node& node = *n;
+ if (IS_BT_NODE(node))
{
// Create node instance object
ScriptingTypeHandle type = Scripting::FindScriptingType((StringAnsiView)n->Values[0]);
@@ -151,6 +154,16 @@ BehaviorTree::BehaviorTree(const SpawnParams& params, const AssetInfo* info)
{
}
+BehaviorTreeNode* BehaviorTree::GetNodeInstance(uint32 id)
+{
+ for (const auto& node : Graph.Nodes)
+ {
+ if (node.ID == id && node.Instance && IS_BT_NODE(node))
+ return node.Instance;
+ }
+ return nullptr;
+}
+
BytesContainer BehaviorTree::LoadSurface()
{
if (WaitForLoaded())
diff --git a/Source/Engine/AI/BehaviorTree.h b/Source/Engine/AI/BehaviorTree.h
index 169210425..c22f99def 100644
--- a/Source/Engine/AI/BehaviorTree.h
+++ b/Source/Engine/AI/BehaviorTree.h
@@ -66,6 +66,13 @@ public:
///
BehaviorTreeGraph Graph;
+ ///
+ /// Gets a specific node instance object from Behavior Tree.
+ ///
+ /// The unique node identifier (Visject surface).
+ /// The node instance or null if cannot get it.
+ API_FUNCTION() BehaviorTreeNode* GetNodeInstance(uint32 id);
+
///
/// Tries to load surface graph from the asset.
///
diff --git a/Source/Engine/AI/BehaviorTreeNode.h b/Source/Engine/AI/BehaviorTreeNode.h
index 3ea05977e..51dee28dc 100644
--- a/Source/Engine/AI/BehaviorTreeNode.h
+++ b/Source/Engine/AI/BehaviorTreeNode.h
@@ -12,6 +12,7 @@
API_CLASS(Abstract) class FLAXENGINE_API BehaviorTreeNode : public SerializableScriptingObject
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeNode, SerializableScriptingObject);
+ friend class Behavior;
friend class BehaviorTreeGraph;
friend class BehaviorKnowledge;
friend class BehaviorTreeSubTreeNode;
@@ -76,6 +77,18 @@ public:
return BehaviorUpdateResult::Success;
}
+#if USE_EDITOR
+ ///
+ /// Gets the node debug state text (multiline). Used in Editor-only to display nodes state. Can be called without valid Behavior/Knowledge/Memory to display default debug info (eg. node properties).
+ ///
+ /// Behavior context data.
+ /// Debug info text (multiline).
+ API_FUNCTION() virtual String GetDebugInfo(const BehaviorUpdateContext& context) const
+ {
+ return String::Empty;
+ }
+#endif
+
// 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.
diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp
index f53684bb6..4be336b35 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.cpp
+++ b/Source/Engine/AI/BehaviorTreeNodes.cpp
@@ -6,6 +6,7 @@
#include "Engine/Core/Random.h"
#include "Engine/Scripting/Scripting.h"
#if USE_CSHARP
+#include "Engine/Core/Utilities.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#endif
#include "Engine/Engine/Engine.h"
@@ -264,6 +265,28 @@ BehaviorUpdateResult BehaviorTreeDelayNode::Update(const BehaviorUpdateContext&
return state->TimeLeft <= 0.0f ? BehaviorUpdateResult::Success : BehaviorUpdateResult::Running;
}
+#if USE_EDITOR
+
+String BehaviorTreeDelayNode::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (context.Memory)
+ {
+ const auto state = GetState(context.Memory);
+ return String::Format(TEXT("Time Left: {}s"), Utilities::RoundTo2DecimalPlaces(state->TimeLeft));
+ }
+
+ String delay;
+ if (WaitTimeSelector.Path.HasChars())
+ delay = String(WaitTimeSelector.Path);
+ else
+ delay = StringUtils::ToString(WaitTime);
+ if (RandomDeviation > 0.0f)
+ delay += String::Format(TEXT("+/-{}"), RandomDeviation);
+ return String::Format(TEXT("Delay: {}s"), delay);
+}
+
+#endif
+
int32 BehaviorTreeSubTreeNode::GetStateSize() const
{
return sizeof(State);
@@ -481,6 +504,35 @@ BehaviorUpdateResult BehaviorTreeMoveToNode::Update(const BehaviorUpdateContext&
return state->Result;
}
+#if USE_EDITOR
+
+String BehaviorTreeMoveToNode::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (context.Memory)
+ {
+ const auto state = GetState(context.Memory);
+ if (state->Agent)
+ {
+ const String agent = state->Agent->GetNamePath();
+ String goal;
+ const Actor* target = Target.Get(context.Knowledge);
+ if (target)
+ goal = target->GetNamePath();
+ else
+ goal = state->GoalLocation.ToString();
+ const Vector3 agentLocation = state->Agent->GetPosition();
+ const Vector3 agentLocationOnPath = agentLocation + state->AgentOffset;
+ float distanceLeft = state->Path.Count() > state->TargetPathIndex ? Vector3::Distance(state->Path[state->TargetPathIndex], agentLocationOnPath) : 0;
+ for (int32 i = state->TargetPathIndex; i < state->Path.Count(); i++)
+ distanceLeft += Vector3::Distance(state->Path[i - 1], state->Path[i]);
+ return String::Format(TEXT("Agent: '{}'\nGoal: '{}'\nDistance: {}"), agent, goal, (int32)distanceLeft);
+ }
+ }
+ return String::Empty;
+}
+
+#endif
+
void BehaviorTreeMoveToNode::State::OnUpdate()
{
if (Result != BehaviorUpdateResult::Running)
diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h
index 6b1d4097e..332835b0f 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.h
+++ b/Source/Engine/AI/BehaviorTreeNodes.h
@@ -122,6 +122,9 @@ public:
int32 GetStateSize() const override;
void InitState(const BehaviorUpdateContext& context) override;
BehaviorUpdateResult Update(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
private:
struct State
@@ -233,6 +236,9 @@ public:
void InitState(const BehaviorUpdateContext& context) override;
void ReleaseState(const BehaviorUpdateContext& context) override;
BehaviorUpdateResult Update(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
protected:
struct State