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