From 8c1dfb308717ef65d3fe1511531291a5b8ab9821 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 24 Aug 2023 16:41:01 +0200 Subject: [PATCH] Add `Time Limit` and `Cooldown` decorators to BT --- Source/Engine/AI/Behavior.cpp | 4 ++ Source/Engine/AI/Behavior.h | 1 + Source/Engine/AI/BehaviorTree.cpp | 3 +- Source/Engine/AI/BehaviorTreeNode.h | 4 +- Source/Engine/AI/BehaviorTreeNodes.cpp | 93 ++++++++++++++++++++++---- Source/Engine/AI/BehaviorTreeNodes.h | 68 ++++++++++++++++++- Source/Engine/AI/BehaviorTypes.h | 5 ++ 7 files changed, 159 insertions(+), 19 deletions(-) diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index 0c7fb6e67..c992c721b 100644 --- a/Source/Engine/AI/Behavior.cpp +++ b/Source/Engine/AI/Behavior.cpp @@ -31,6 +31,7 @@ void Behavior::StopLogic(BehaviorUpdateResult result) if (_result != BehaviorUpdateResult::Running || result == BehaviorUpdateResult::Running) return; _accumulatedTime = 0.0f; + _totalTime = 0; _result = result; } @@ -43,6 +44,7 @@ void Behavior::ResetLogic() // Reset state _knowledge.FreeMemory(); _accumulatedTime = 0.0f; + _totalTime = 0; _result = BehaviorUpdateResult::Success; if (isActive) @@ -73,6 +75,7 @@ void Behavior::OnLateUpdate() if (_accumulatedTime < updateDeltaTime) return; _accumulatedTime -= updateDeltaTime; + _totalTime += updateDeltaTime; // Update tree BehaviorUpdateContext context; @@ -81,6 +84,7 @@ void Behavior::OnLateUpdate() context.Memory = _knowledge.Memory; context.RelevantNodes = &_knowledge.RelevantNodes; context.DeltaTime = updateDeltaTime; + context.Time = _totalTime; const BehaviorUpdateResult result = tree->Graph.Root->InvokeUpdate(context); if (result != BehaviorUpdateResult::Running) _result = result; diff --git a/Source/Engine/AI/Behavior.h b/Source/Engine/AI/Behavior.h index 6800eb9a1..7aad11c99 100644 --- a/Source/Engine/AI/Behavior.h +++ b/Source/Engine/AI/Behavior.h @@ -19,6 +19,7 @@ API_CLASS() class FLAXENGINE_API Behavior : public Script private: BehaviorKnowledge _knowledge; float _accumulatedTime = 0.0f; + float _totalTime = 0.0f; BehaviorUpdateResult _result = BehaviorUpdateResult::Success; void* _memory = nullptr; diff --git a/Source/Engine/AI/BehaviorTree.cpp b/Source/Engine/AI/BehaviorTree.cpp index 3196084dc..b3fc99ccf 100644 --- a/Source/Engine/AI/BehaviorTree.cpp +++ b/Source/Engine/AI/BehaviorTree.cpp @@ -112,9 +112,8 @@ void BehaviorTreeGraph::LoadRecursive(Node& node) for (int32 i = 0; i < ids.Length(); i++) { Node* decorator = GetNode(ids[i]); - if (decorator && decorator->Instance) + if (decorator && decorator->Instance && decorator->Instance->Is()) { - ASSERT_LOW_LAYER(decorator->Instance->Is()); node.Instance->_decorators.Add((BehaviorTreeDecorator*)decorator->Instance); decorator->Instance->_parent = node.Instance; LoadRecursive(*decorator); diff --git a/Source/Engine/AI/BehaviorTreeNode.h b/Source/Engine/AI/BehaviorTreeNode.h index 845879059..192db8f7e 100644 --- a/Source/Engine/AI/BehaviorTreeNode.h +++ b/Source/Engine/AI/BehaviorTreeNode.h @@ -81,9 +81,9 @@ 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); + virtual 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); + virtual void BecomeIrrelevant(const BehaviorUpdateContext& context); // [SerializableScriptingObject] void Serialize(SerializeStream& stream, const void* otherObj) override; diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index df424a864..c92331ce9 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -60,12 +60,21 @@ BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& if (relevantNodes.Get(_executionIndex) == false) BecomeRelevant(context); - // Node-specific update + // Update decorators + bool decoratorFailed = false; for (BehaviorTreeDecorator* decorator : _decorators) { - decorator->Update(context); + decoratorFailed |= decorator->Update(context) == BehaviorUpdateResult::Failed; } - BehaviorUpdateResult result = Update(context); + + // Node-specific update + BehaviorUpdateResult result; + if (decoratorFailed) + result = BehaviorUpdateResult::Failed; + else + result = Update(context); + + // Post-process result from decorators for (BehaviorTreeDecorator* decorator : _decorators) { decorator->PostUpdate(context, result); @@ -87,7 +96,7 @@ void BehaviorTreeNode::BecomeRelevant(const BehaviorUpdateContext& context) InitState(context.Behavior, context.Memory); } -void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) +void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context) { // Release state BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; @@ -95,9 +104,6 @@ void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bo relevantNodes.Set(_executionIndex, false); ReleaseState(context.Behavior, context.Memory); - if (nodeOnly) - return; - // Release decorators for (BehaviorTreeDecorator* decorator : _decorators) { @@ -141,7 +147,7 @@ BehaviorUpdateResult BehaviorTreeCompoundNode::Update(BehaviorUpdateContext cont return result; } -void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) +void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& context) { // Make any nested nodes irrelevant as well const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes; @@ -153,7 +159,7 @@ void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& con } } - BehaviorTreeNode::BecomeIrrelevant(context, nodeOnly); + BehaviorTreeNode::BecomeIrrelevant(context); } int32 BehaviorTreeSequenceNode::GetStateSize() const @@ -237,7 +243,7 @@ void BehaviorTreeDelayNode::InitState(Behavior* behavior, void* memory) auto state = GetState(memory); if (!WaitTimeSelector.TryGet(behavior->GetKnowledge(), state->TimeLeft)) state->TimeLeft = WaitTime; - state->TimeLeft = Random::RandRange(Math::Max(WaitTime - RandomDeviation, 0.0f), WaitTime + RandomDeviation); + state->TimeLeft = Random::RandRange(Math::Max(state->TimeLeft - RandomDeviation, 0.0f), state->TimeLeft + RandomDeviation); } BehaviorUpdateResult BehaviorTreeDelayNode::Update(BehaviorUpdateContext context) @@ -272,7 +278,7 @@ void BehaviorTreeSubTreeNode::ReleaseState(Behavior* behavior, void* memory) { for (const auto& node : tree->Graph.Nodes) { - if (node.Instance && node.Instance->_executionIndex != -1 && state->RelevantNodes[node.Instance->_executionIndex]) + if (node.Instance && node.Instance->_executionIndex != -1 && state->RelevantNodes.HasItems() && state->RelevantNodes[node.Instance->_executionIndex]) node.Instance->ReleaseState(behavior, state->Memory.Get()); } } @@ -354,10 +360,69 @@ void BehaviorTreeLoopDecorator::PostUpdate(BehaviorUpdateContext context, Behavi state->Loops--; if (state->Loops > 0) { - // Keep running in a loop but reset node's state (preserve decorators state including Loops counter) + // Keep running in a loop but reset node's state (preserve self state) result = BehaviorUpdateResult::Running; - _parent->BecomeIrrelevant(context, true); - _parent->BecomeRelevant(context); + BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; + relevantNodes.Set(_executionIndex, false); + _parent->BecomeIrrelevant(context); + relevantNodes.Set(_executionIndex, true); } } } + +int32 BehaviorTreeTimeLimitDecorator::GetStateSize() const +{ + return sizeof(State); +} + +void BehaviorTreeTimeLimitDecorator::InitState(Behavior* behavior, void* memory) +{ + auto state = GetState(memory); + if (!MaxDurationSelector.TryGet(behavior->GetKnowledge(), state->TimeLeft)) + state->TimeLeft = MaxDuration; + state->TimeLeft = Random::RandRange(Math::Max(state->TimeLeft - RandomDeviation, 0.0f), state->TimeLeft + RandomDeviation); +} + +BehaviorUpdateResult BehaviorTreeTimeLimitDecorator::Update(BehaviorUpdateContext context) +{ + auto state = GetState(context.Memory); + state->TimeLeft -= context.DeltaTime; + return state->TimeLeft <= 0.0f ? BehaviorUpdateResult::Failed : BehaviorUpdateResult::Success; +} + +int32 BehaviorTreeCooldownDecorator::GetStateSize() const +{ + return sizeof(State); +} + +void BehaviorTreeCooldownDecorator::InitState(Behavior* behavior, void* memory) +{ + auto state = GetState(memory); + state->EndTime = 0; // Allow to entry on start +} + +void BehaviorTreeCooldownDecorator::ReleaseState(Behavior* behavior, void* memory) +{ + // Preserve the decorator's state to keep cooldown + BitArray<>& relevantNodes = behavior->GetKnowledge()->RelevantNodes; + relevantNodes.Set(_executionIndex, true); +} + +bool BehaviorTreeCooldownDecorator::CanUpdate(BehaviorUpdateContext context) +{ + auto state = GetState(context.Memory); + return state->EndTime <= context.Time; +} + +void BehaviorTreeCooldownDecorator::PostUpdate(BehaviorUpdateContext context, BehaviorUpdateResult& result) +{ + if (result != BehaviorUpdateResult::Running) + { + // Initialize cooldown + auto state = GetState(context.Memory); + if (!MinDurationSelector.TryGet(context.Knowledge, state->EndTime)) + state->EndTime = MinDuration; + state->EndTime = Random::RandRange(Math::Max(state->EndTime - RandomDeviation, 0.0f), state->EndTime + RandomDeviation); + state->EndTime += context.Time; + } +} diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index 71bae05f0..2cd2d6f25 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -28,7 +28,7 @@ public: protected: // [BehaviorTreeNode] - void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) override; + void BecomeIrrelevant(const BehaviorUpdateContext& context) override; }; /// @@ -226,3 +226,69 @@ public: int32 Loops; }; }; + +/// +/// Limits maximum duration of the node execution time (in seconds). Node will fail if it runs out of time. +/// +API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeTimeLimitDecorator : public BehaviorTreeDecorator +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeTimeLimitDecorator, BehaviorTreeDecorator); + API_AUTO_SERIALIZATION(); + + // Maximum node execution time (in seconds). Unused if MaxDurationSelector is used. + API_FIELD(Attributes="EditorOrder(10), Limit(0)") + float MaxDuration = 3.0; + + // Duration time randomization range to deviate original value. + API_FIELD(Attributes="EditorOrder(20), Limit(0)") + float RandomDeviation = 0.0f; + + // Maximum node execution time (in seconds) from behavior's knowledge (blackboard, goal or sensor). If set, overrides MaxDuration but still uses RandomDeviation. + API_FIELD(Attributes="EditorOrder(20)") + BehaviorKnowledgeSelector MaxDurationSelector; + +public: + // [BehaviorTreeNode] + int32 GetStateSize() const override; + void InitState(Behavior* behavior, void* memory) override; + BehaviorUpdateResult Update(BehaviorUpdateContext context) override; + + struct State + { + float TimeLeft; + }; +}; + +/// +/// Adds cooldown in between node executions. +/// +API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeCooldownDecorator : public BehaviorTreeDecorator +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeCooldownDecorator, BehaviorTreeDecorator); + API_AUTO_SERIALIZATION(); + + // Minimum cooldown time (in seconds). Unused if MinDurationSelector is used. + API_FIELD(Attributes="EditorOrder(10), Limit(0)") + float MinDuration = 3.0; + + // Duration time randomization range to deviate original value. + API_FIELD(Attributes="EditorOrder(20), Limit(0)") + float RandomDeviation = 0.0f; + + // Minimum cooldown time (in seconds) from behavior's knowledge (blackboard, goal or sensor). If set, overrides MinDuration but still uses RandomDeviation. + API_FIELD(Attributes="EditorOrder(20)") + BehaviorKnowledgeSelector MinDurationSelector; + +public: + // [BehaviorTreeNode] + int32 GetStateSize() const override; + void InitState(Behavior* behavior, void* memory) override; + void ReleaseState(Behavior* behavior, void* memory) override; + bool CanUpdate(BehaviorUpdateContext context) override; + void PostUpdate(BehaviorUpdateContext context, BehaviorUpdateResult& result) override; + + struct State + { + float EndTime; + }; +}; diff --git a/Source/Engine/AI/BehaviorTypes.h b/Source/Engine/AI/BehaviorTypes.h index bfbde2c26..80133f53a 100644 --- a/Source/Engine/AI/BehaviorTypes.h +++ b/Source/Engine/AI/BehaviorTypes.h @@ -40,6 +40,11 @@ API_STRUCT(NoDefault) struct FLAXENGINE_API BehaviorUpdateContext /// Simulation time delta (in seconds) since the last update. /// API_FIELD() float DeltaTime; + + /// + /// Simulation time (in seconds) since the first update of the Behavior (sum of all deltas since the start). + /// + API_FIELD() float Time; }; ///