Add GetDebugInfo to BT nodes for debugging

This commit is contained in:
Wojtek Figat
2023-09-19 20:57:19 +02:00
parent f845344d03
commit 336fe46e03
12 changed files with 225 additions and 20 deletions

View File

@@ -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,10 +516,13 @@ 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

View File

@@ -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);
}
}
/// <inheritdoc />
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);
}

View File

@@ -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

View File

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

View File

@@ -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<BehaviorKnowledge*>(this), path, value, false);
}
bool BehaviorKnowledge::Set(const StringAnsiView& path, const Variant& value)

View File

@@ -67,7 +67,7 @@ public:
/// <param name="path">Selector path.</param>
/// <param name="value">Result value (valid only when returned true).</param>
/// <returns>True if got value, otherwise false.</returns>
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;
/// <summary>
/// Sets the knowledge item value via selector path.

View File

@@ -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<T>::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))

View File

@@ -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())

View File

@@ -66,6 +66,13 @@ public:
/// </summary>
BehaviorTreeGraph Graph;
/// <summary>
/// Gets a specific node instance object from Behavior Tree.
/// </summary>
/// <param name="id">The unique node identifier (Visject surface).</param>
/// <returns>The node instance or null if cannot get it.</returns>
API_FUNCTION() BehaviorTreeNode* GetNodeInstance(uint32 id);
/// <summary>
/// Tries to load surface graph from the asset.
/// </summary>

View File

@@ -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
/// <summary>
/// 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).
/// </summary>
/// <param name="context">Behavior context data.</param>
/// <returns>Debug info text (multiline).</returns>
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.

View File

@@ -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<State>(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<State>(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)

View File

@@ -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