// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "BehaviorTreeNodes.h" #include "Behavior.h" #include "BehaviorKnowledge.h" #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" #include "Engine/Engine/Time.h" #include "Engine/Level/Actor.h" #include "Engine/Navigation/NavMeshRuntime.h" #include "Engine/Physics/Actors/RigidBody.h" #include "Engine/Physics/Colliders/CapsuleCollider.h" #include "Engine/Physics/Colliders/CharacterController.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/Serialization.h" bool IsAssignableFrom(const StringAnsiView& to, const StringAnsiView& from) { // Special case of null if (to.IsEmpty()) return from.IsEmpty(); if (from.IsEmpty()) return false; // Exact typename math if (to == from) return true; // Scripting Type match const ScriptingTypeHandle typeHandleTo = Scripting::FindScriptingType(to); const ScriptingTypeHandle typeHandleFrom = Scripting::FindScriptingType(from); if (typeHandleTo && typeHandleFrom) return typeHandleTo.IsAssignableFrom(typeHandleFrom); #if USE_CSHARP // MClass match const auto mclassTo = Scripting::FindClass(to); const auto mclassFrom = Scripting::FindClass(from); if (mclassTo && mclassFrom) return mclassTo == mclassFrom || mclassFrom->IsSubClassOf(mclassTo); #endif return false; } BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& context) { ASSERT_LOW_LAYER(_executionIndex != -1); const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes; // If node is not yet relevant if (relevantNodes.Get(_executionIndex) == false) { // Check decorators if node can be executed for (BehaviorTreeDecorator* decorator : _decorators) { ASSERT_LOW_LAYER(decorator->_executionIndex != -1); if (relevantNodes.Get(decorator->_executionIndex) == false) decorator->BecomeRelevant(context); if (!decorator->CanUpdate(context)) { return BehaviorUpdateResult::Failed; } } // Make node relevant BecomeRelevant(context); } // Update decorators bool decoratorFailed = false; for (BehaviorTreeDecorator* decorator : _decorators) { decoratorFailed |= decorator->Update(context) == BehaviorUpdateResult::Failed; } // Node-specific update BehaviorUpdateResult result; if (decoratorFailed) result = BehaviorUpdateResult::Failed; else result = Update(context); if ((int32)result < 0 || (int32)result > (int32)BehaviorUpdateResult::Failed) result = BehaviorUpdateResult::Failed; // Invalid value is a failure // Post-process result from decorators for (BehaviorTreeDecorator* decorator : _decorators) { decorator->PostUpdate(context, result); } // Check if node is not relevant anymore if (result != BehaviorUpdateResult::Running) BecomeIrrelevant(context); return result; } void BehaviorTreeNode::BecomeRelevant(const BehaviorUpdateContext& context) { // Initialize state BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == false); relevantNodes.Set(_executionIndex, true); InitState(context); } void BehaviorTreeNode::BecomeIrrelevant(const BehaviorUpdateContext& context) { // Release state BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; ASSERT_LOW_LAYER(relevantNodes.Get(_executionIndex) == true); relevantNodes.Set(_executionIndex, false); ReleaseState(context); // Release decorators for (BehaviorTreeDecorator* decorator : _decorators) { if (relevantNodes.Get(decorator->_executionIndex) == true) { decorator->BecomeIrrelevant(context); } } } void BehaviorTreeNode::Serialize(SerializeStream& stream, const void* otherObj) { SerializableScriptingObject::Serialize(stream, otherObj); SERIALIZE_GET_OTHER_OBJ(BehaviorTreeNode); SERIALIZE(Name); } void BehaviorTreeNode::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { SerializableScriptingObject::Deserialize(stream, modifier); Name.Clear(); // Missing Name is assumes as unnamed node DESERIALIZE(Name); } void BehaviorTreeCompoundNode::Init(BehaviorTree* tree) { for (BehaviorTreeNode* child : Children) child->Init(tree); } BehaviorUpdateResult BehaviorTreeCompoundNode::Update(const BehaviorUpdateContext& context) { auto result = BehaviorUpdateResult::Success; for (int32 i = 0; i < Children.Count() && result == BehaviorUpdateResult::Success; i++) { BehaviorTreeNode* child = Children[i]; result = child->InvokeUpdate(context); } return result; } void BehaviorTreeCompoundNode::BecomeIrrelevant(const BehaviorUpdateContext& context) { // Make any nested nodes irrelevant as well const BitArray<>& relevantNodes = *(const BitArray<>*)context.RelevantNodes; for (BehaviorTreeNode* child : Children) { if (relevantNodes.Get(child->_executionIndex) == true) { child->BecomeIrrelevant(context); } } BehaviorTreeNode::BecomeIrrelevant(context); } int32 BehaviorTreeSequenceNode::GetStateSize() const { return sizeof(State); } void BehaviorTreeSequenceNode::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); new(state)State(); } BehaviorUpdateResult BehaviorTreeSequenceNode::Update(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (state->CurrentChildIndex >= Children.Count()) return BehaviorUpdateResult::Success; if (state->CurrentChildIndex == -1) return BehaviorUpdateResult::Failed; auto result = Children[state->CurrentChildIndex]->InvokeUpdate(context); switch (result) { case BehaviorUpdateResult::Success: state->CurrentChildIndex++; // Move to the next node if (state->CurrentChildIndex < Children.Count()) result = BehaviorUpdateResult::Running; // Keep on running to the next child on the next update break; case BehaviorUpdateResult::Failed: state->CurrentChildIndex = -1; // Mark whole sequence as failed break; } return result; } int32 BehaviorTreeSelectorNode::GetStateSize() const { return sizeof(State); } void BehaviorTreeSelectorNode::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); new(state)State(); } BehaviorUpdateResult BehaviorTreeSelectorNode::Update(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (state->CurrentChildIndex >= Children.Count()) return BehaviorUpdateResult::Failed; auto result = Children[state->CurrentChildIndex]->InvokeUpdate(context); switch (result) { case BehaviorUpdateResult::Success: return BehaviorUpdateResult::Success; case BehaviorUpdateResult::Failed: state->CurrentChildIndex++; // Move to the next node if (state->CurrentChildIndex < Children.Count()) result = BehaviorUpdateResult::Running; // Keep on running to the next child on the next update break; } return result; } int32 BehaviorTreeDelayNode::GetStateSize() const { return sizeof(State); } void BehaviorTreeDelayNode::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (!WaitTimeSelector.TryGet(context.Knowledge, state->TimeLeft)) state->TimeLeft = WaitTime; state->TimeLeft = Random::RandRange(Math::Max(state->TimeLeft - RandomDeviation, 0.0f), state->TimeLeft + RandomDeviation); } BehaviorUpdateResult BehaviorTreeDelayNode::Update(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); state->TimeLeft -= context.DeltaTime; 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); } void BehaviorTreeSubTreeNode::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); new(state)State(); const BehaviorTree* tree = Tree.Get(); if (!tree || tree->WaitForLoaded()) return; state->Memory.Resize(tree->Graph.NodesStatesSize); state->RelevantNodes.Resize(tree->Graph.NodesCount, false); state->RelevantNodes.SetAll(false); } void BehaviorTreeSubTreeNode::ReleaseState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); const BehaviorTree* tree = Tree.Get(); if (tree && tree->IsLoaded()) { // Override memory with custom one for the subtree BehaviorUpdateContext subContext = context; subContext.Memory = state->Memory.Get(); subContext.RelevantNodes = &state->RelevantNodes; for (const auto& node : tree->Graph.Nodes) { if (node.Instance && node.Instance->_executionIndex != -1 && state->RelevantNodes.HasItems() && state->RelevantNodes[node.Instance->_executionIndex]) node.Instance->ReleaseState(subContext); } } state->~State(); } BehaviorUpdateResult BehaviorTreeSubTreeNode::Update(const BehaviorUpdateContext& context) { const BehaviorTree* tree = Tree.Get(); if (!tree || !tree->Graph.Root) return BehaviorUpdateResult::Failed; const StringAnsiView treeBlackboardType = tree->Graph.Root->BlackboardType; if (treeBlackboardType.HasChars()) { // Validate if nested tree blackboard data matches (the same type or base type) const VariantType& blackboardType = context.Knowledge->Blackboard.Type; if (IsAssignableFrom(treeBlackboardType, StringAnsiView(blackboardType.GetTypeName()))) { LOG(Error, "Cannot use nested '{}' with Blackboard of type '{}' inside '{}' with Blackboard of type '{}'", tree->ToString(), String(treeBlackboardType), context.Knowledge->Tree->ToString(), blackboardType.ToString()); return BehaviorUpdateResult::Failed; } } // Override memory with custom one for the subtree auto state = GetState(context.Memory); BehaviorUpdateContext subContext = context; subContext.Memory = state->Memory.Get(); subContext.RelevantNodes = &state->RelevantNodes; // Run nested tree return tree->Graph.Root->InvokeUpdate(subContext); } BehaviorUpdateResult BehaviorTreeForceFinishNode::Update(const BehaviorUpdateContext& context) { context.Behavior->StopLogic(Result); return Result; } bool BehaviorTreeMoveToNode::Move(Actor* agent, const Vector3& move) const { agent->AddMovement(move); return false; } NavMeshRuntime* BehaviorTreeMoveToNode::GetNavMesh(Actor* agent) const { return NavMeshRuntime::Get(); } void BehaviorTreeMoveToNode::GetAgentSize(Actor* agent, float& outRadius, float& outHeight) const { if (const auto* characterController = Cast(agent)) { // Character Controller is an capsule outRadius = characterController->GetRadius(); outHeight = characterController->GetHeight() + 2 * outRadius; return; } if (const auto* rigidBody = Cast(agent)) { // Rigid Body with a single Capsule collider (directed Up) Array> colliders; rigidBody->GetColliders(colliders); const auto* capsuleCollider = colliders.Count() == 1 ? (CapsuleCollider*)colliders[0] : nullptr; if (capsuleCollider && (capsuleCollider->GetLocalOrientation() == Quaternion::Euler(0, 0, 90) || capsuleCollider->GetLocalOrientation() == Quaternion::Euler(0, 0, -90))) { outRadius = capsuleCollider->GetRadius(); outHeight = capsuleCollider->GetHeight() + 2 * outRadius; return; } } // Estimate actor bounds to extract capsule information const BoundingBox box = agent->GetBox(); const BoundingSphere sphere = agent->GetSphere(); outRadius = sphere.Radius; outHeight = box.GetSize().Y; } int32 BehaviorTreeMoveToNode::GetStateSize() const { return sizeof(State); } void BehaviorTreeMoveToNode::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); new(state)State(); state->Node = this; state->Knowledge = context.Knowledge; // Get agent to move if (Agent.Path.HasChars()) state->Agent = Agent.Get(context.Knowledge); else state->Agent = context.Behavior->GetActor(); } void BehaviorTreeMoveToNode::ReleaseState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (state->HasTick) Engine::Update.Unbind(state); state->~State(); } BehaviorUpdateResult BehaviorTreeMoveToNode::Update(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (state->Agent == nullptr) return BehaviorUpdateResult::Failed; bool repath = !state->HasPath; Vector3 goalLocation = state->GoalLocation; if (repath || UseTargetGoalUpdate) { // Get current goal location const Actor* target = Target.Get(context.Knowledge); if (target) goalLocation = target->GetPosition(); else goalLocation = TargetLocation.Get(context.Knowledge); repath |= Vector3::Distance(goalLocation, state->GoalLocation) > TargetGoalUpdateTolerance; state->GoalLocation = goalLocation; } if (repath) { // Clear path state->HasPath = false; state->Path.Clear(); state->AgentOffset = Vector3::Zero; state->UpVector = Float3::Up; state->NavAgentRadius = 0; // Find a new path const Vector3 agentLocation = state->Agent->GetPosition(); if (UsePathfinding) { const NavMeshRuntime* navMesh = GetNavMesh(state->Agent); if (!navMesh) return BehaviorUpdateResult::Failed; NavMeshPathFlags pathFlags; if (!navMesh->FindPath(agentLocation, goalLocation, state->Path, pathFlags)) return BehaviorUpdateResult::Failed; if (!UsePartialPath && EnumHasAnyFlags(pathFlags, NavMeshPathFlags::PartialPath)) return BehaviorUpdateResult::Failed; state->UpVector = Float3::Transform(Float3::Up, navMesh->Properties.Rotation); state->NavAgentRadius = navMesh->Properties.Agent.Radius; // Place start and end on navmesh navMesh->ProjectPoint(state->Path.First(), state->Path.First()); navMesh->ProjectPoint(state->Path.Last(), state->Path.Last()); // Calculate offset between path and the agent (aka feet offset) state->AgentOffset = state->Path.First() - agentLocation; } else { // Dummy movement state->Path.Resize(2); state->Path.Get()[0] = agentLocation; state->Path.Get()[1] = goalLocation; } // Start path following state->HasPath = true; state->TargetPathIndex = 1; state->Result = BehaviorUpdateResult::Running; // TODO: add path debugging in Editor (eg. via BT window) // Register for ticking the path following logic at game update rate (BT usually use lower FPS due to performance) if (!state->HasTick) { state->HasTick = true; Engine::Update.Bind(state); } } 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) return; PROFILE_CPU(); // Get agent properties const Vector3 agentLocation = Agent->GetPosition(); float movementSpeed; if (!Node->MovementSpeed.TryGet(Knowledge, movementSpeed)) movementSpeed = 100; float agentRadius = 30.0f, agentHeight = 100.0f; Node->GetAgentSize(Agent, agentRadius, agentHeight); // Test if agent reached the next path segment Vector3 pathSegmentEnd = Path[TargetPathIndex]; const Vector3 agentLocationOnPath = agentLocation + AgentOffset; const bool isLastSegment = TargetPathIndex + 1 == Path.Count(); float testRadius; if (isLastSegment) testRadius = agentRadius + Node->AcceptableRadius; else testRadius = agentRadius * 0.05f + Math::Max(agentRadius - NavAgentRadius, 0.0f); // 5% threshold of agent radius and diff between navmesh vs agent radius as threshold for path segments reaching const float acceptableHeightPercentage = 1.05f; const float testHeight = agentHeight * acceptableHeightPercentage; const Vector3 toGoal = agentLocationOnPath - pathSegmentEnd; const float toGoalHeightDiff = (toGoal * UpVector).SumValues(); if (toGoal.Length() <= testRadius && toGoalHeightDiff <= testHeight) { TargetPathIndex++; if (TargetPathIndex == Path.Count()) { // Goal reached! Result = BehaviorUpdateResult::Success; return; } pathSegmentEnd = Path[TargetPathIndex]; } // Move agent const float maxMove = movementSpeed * Time::Update.DeltaTime.GetTotalSeconds(); if (maxMove <= ZeroTolerance) return; const Vector3 move = Vector3::MoveTowards(agentLocationOnPath, pathSegmentEnd, maxMove) - agentLocationOnPath; if (Node->Move(Agent, move)) { // Move failed! Result = BehaviorUpdateResult::Failed; } } void BehaviorTreeInvertDecorator::PostUpdate(const BehaviorUpdateContext& context, BehaviorUpdateResult& result) { if (result == BehaviorUpdateResult::Success) result = BehaviorUpdateResult::Failed; else if (result == BehaviorUpdateResult::Failed) result = BehaviorUpdateResult::Success; } void BehaviorTreeForceSuccessDecorator::PostUpdate(const BehaviorUpdateContext& context, BehaviorUpdateResult& result) { if (result != BehaviorUpdateResult::Running) result = BehaviorUpdateResult::Success; } void BehaviorTreeForceFailedDecorator::PostUpdate(const BehaviorUpdateContext& context, BehaviorUpdateResult& result) { if (result != BehaviorUpdateResult::Running) result = BehaviorUpdateResult::Failed; } int32 BehaviorTreeLoopDecorator::GetStateSize() const { return sizeof(State); } void BehaviorTreeLoopDecorator::InitState(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (!LoopCountSelector.TryGet(context.Knowledge, state->Loops)) state->Loops = LoopCount; } void BehaviorTreeLoopDecorator::PostUpdate(const BehaviorUpdateContext& context, BehaviorUpdateResult& result) { // Continue looping only if node succeeds if (result == BehaviorUpdateResult::Success) { auto state = GetState(context.Memory); if (!InfiniteLoop) state->Loops--; if (state->Loops > 0 || InfiniteLoop) { // Keep running in a loop but reset node's state (preserve self state) result = BehaviorUpdateResult::Running; 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(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); if (!MaxDurationSelector.TryGet(context.Knowledge, state->TimeLeft)) state->TimeLeft = MaxDuration; state->TimeLeft = Random::RandRange(Math::Max(state->TimeLeft - RandomDeviation, 0.0f), state->TimeLeft + RandomDeviation); } BehaviorUpdateResult BehaviorTreeTimeLimitDecorator::Update(const 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(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); state->EndTime = 0; // Allow to entry on start } void BehaviorTreeCooldownDecorator::ReleaseState(const BehaviorUpdateContext& context) { // Preserve the decorator's state to keep cooldown BitArray<>& relevantNodes = *(BitArray<>*)context.RelevantNodes; relevantNodes.Set(_executionIndex, true); } bool BehaviorTreeCooldownDecorator::CanUpdate(const BehaviorUpdateContext& context) { auto state = GetState(context.Memory); return state->EndTime <= context.Time; } void BehaviorTreeCooldownDecorator::PostUpdate(const 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; } } bool BehaviorTreeKnowledgeConditionalDecorator::CanUpdate(const BehaviorUpdateContext& context) { return BehaviorKnowledge::CompareValues((float)ValueA.Get(context.Knowledge), ValueB, Comparison); } bool BehaviorTreeKnowledgeValuesConditionalDecorator::CanUpdate(const BehaviorUpdateContext& context) { return BehaviorKnowledge::CompareValues((float)ValueA.Get(context.Knowledge), (float)ValueB.Get(context.Knowledge), Comparison); } bool BehaviorTreeHasTagDecorator::CanUpdate(const BehaviorUpdateContext& context) { bool result = false; ::Actor* actor; if (Actor.TryGet(context.Knowledge, actor) && actor) result = actor->HasTag(Tag); result ^= Invert; return result; } bool BehaviorTreeHasGoalDecorator::CanUpdate(const BehaviorUpdateContext& context) { Variant value; // TODO: use HasGoal in Knowledge to optimize this (goal struct is copied by selector accessor) return Goal.TryGet(context.Knowledge, value); }