Add Time Limit and Cooldown decorators to BT

This commit is contained in:
Wojtek Figat
2023-08-24 16:41:01 +02:00
parent d2034622cb
commit 8c1dfb3087
7 changed files with 159 additions and 19 deletions

View File

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

View File

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

View File

@@ -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<BehaviorTreeDecorator>())
{
ASSERT_LOW_LAYER(decorator->Instance->Is<BehaviorTreeDecorator>());
node.Instance->_decorators.Add((BehaviorTreeDecorator*)decorator->Instance);
decorator->Instance->_parent = node.Instance;
LoadRecursive(*decorator);

View File

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

View File

@@ -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<State>(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<State>(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<State>(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<State>(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<State>(context.Memory);
return state->EndTime <= context.Time;
}
void BehaviorTreeCooldownDecorator::PostUpdate(BehaviorUpdateContext context, BehaviorUpdateResult& result)
{
if (result != BehaviorUpdateResult::Running)
{
// Initialize cooldown
auto state = GetState<State>(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;
}
}

View File

@@ -28,7 +28,7 @@ public:
protected:
// [BehaviorTreeNode]
void BecomeIrrelevant(const BehaviorUpdateContext& context, bool nodeOnly) override;
void BecomeIrrelevant(const BehaviorUpdateContext& context) override;
};
/// <summary>
@@ -226,3 +226,69 @@ public:
int32 Loops;
};
};
/// <summary>
/// Limits maximum duration of the node execution time (in seconds). Node will fail if it runs out of time.
/// </summary>
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<float> MaxDurationSelector;
public:
// [BehaviorTreeNode]
int32 GetStateSize() const override;
void InitState(Behavior* behavior, void* memory) override;
BehaviorUpdateResult Update(BehaviorUpdateContext context) override;
struct State
{
float TimeLeft;
};
};
/// <summary>
/// Adds cooldown in between node executions.
/// </summary>
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<float> 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;
};
};

View File

@@ -40,6 +40,11 @@ API_STRUCT(NoDefault) struct FLAXENGINE_API BehaviorUpdateContext
/// Simulation time delta (in seconds) since the last update.
/// </summary>
API_FIELD() float DeltaTime;
/// <summary>
/// Simulation time (in seconds) since the first update of the Behavior (sum of all deltas since the start).
/// </summary>
API_FIELD() float Time;
};
/// <summary>