Loading...
Loading...
Use this skill when implementing AI, AIController, behavior tree, blackboard, AI perception, NavMesh, EQS, navigation, pathfinding, State Tree, or Smart Objects in Unreal Engine. See references/behavior-tree-patterns.md for BT patterns and references/eqs-reference.md for EQS configuration. For AI ability use, see ue-gameplay-abilities.
npx skill4agent add quodsoler/unreal-engine-skills ue-ai-navigation.agents/ue-project-context.mdAPawn
└── AAIController (server-only in multiplayer)
├── UBehaviorTreeComponent (UBrainComponent subclass)
│ └── UBehaviorTree asset → UBlackboardData
├── UBlackboardComponent (AI knowledge store)
├── UAIPerceptionComponent (sight, hearing, damage)
└── UPathFollowingComponent (NavMesh path execution)AIModuleNavigationSystemGameplayTasks// MyAIController.h
UCLASS()
class AMyAIController : public AAIController
{
GENERATED_BODY()
public:
AMyAIController();
UPROPERTY(EditDefaultsOnly, Category = AI)
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
protected:
virtual void OnPossess(APawn* InPawn) override;
UFUNCTION()
void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
};
// MyAIController.cpp
AMyAIController::AMyAIController()
{
bStartAILogicOnPossess = true;
bStopAILogicOnUnposses = true;
// PerceptionComponent declared in AAIController; configure senses here or in BP defaults
}
void AMyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTreeAsset)
RunBehaviorTree(BehaviorTreeAsset); // calls UseBlackboard internally
if (UAIPerceptionComponent* PC = GetAIPerceptionComponent())
PC->OnTargetPerceptionUpdated.AddDynamic(this, &AMyAIController::OnTargetPerceptionUpdated);
}// Navigation
EPathFollowingRequestResult::Type MoveToActor(AActor* Goal, float AcceptanceRadius = -1,
bool bStopOnOverlap = true, bool bUsePathfinding = true, bool bCanStrafe = true,
TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);
EPathFollowingRequestResult::Type MoveToLocation(const FVector& Dest, float AcceptanceRadius = -1,
bool bStopOnOverlap = true, bool bUsePathfinding = true,
bool bProjectDestinationToNavigation = false, bool bCanStrafe = true,
TSubclassOf<UNavigationQueryFilter> FilterClass = {}, bool bAllowPartialPath = true);
void StopMovement();
bool HasPartialPath() const;
EPathFollowingStatus::Type GetMoveStatus() const;
// Focus
void SetFocus(AActor* NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void SetFocalPoint(FVector NewFocus, EAIFocusPriority::Type Priority = EAIFocusPriority::Gameplay);
void ClearFocus(EAIFocusPriority::Type Priority);
// Brain / Blackboard
bool RunBehaviorTree(UBehaviorTree* BTAsset);
bool UseBlackboard(UBlackboardData* BlackboardAsset, UBlackboardComponent*& BlackboardComponent);
UBlackboardComponent* GetBlackboardComponent();
// Team (IGenericTeamAgentInterface)
void SetGenericTeamId(const FGenericTeamId& NewTeamID);
// Delegate: FAIMoveCompletedSignature ReceiveMoveCompleted (RequestID, Result)AIControllerClass = AMyAIController::StaticClass(); AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;| Type | Get | Set |
|---|---|---|
| Object | | |
| Vector | | |
| Bool | | |
| Float | | |
| Int | | |
| Enum | | |
| Name | | |
| Rotator | | |
| String | | |
| Class | | |
UBlackboardComponent* BB = GetBlackboardComponent();
BB->SetValueAsObject(TEXT("TargetActor"), SomeActor);
BB->SetValueAsVector(TEXT("LastKnownLocation"), Location);
BB->ClearValue(TEXT("TargetActor"));
bool bSet = BB->IsVectorValueSet(TEXT("PatrolLocation"));
// Observer (called when key changes)
FBlackboard::FKey KeyID = BB->GetKeyID(TEXT("TargetActor"));
FDelegateHandle H = BB->RegisterObserver(KeyID, this,
FOnBlackboardChangeNotification::CreateUObject(this, &AMyAIController::OnBBKeyChanged));
BB->UnregisterObserver(KeyID, H);
// High-perf cached accessor (avoids repeated name lookups):
FBBKeyCachedAccessor<UBlackboardKeyType_Bool> BBInCombat;
// Init: BBInCombat = FBBKeyCachedAccessor<...>(*BBComp, KeyID);
// Use: bool b = BBInCombat.Get(); BBInCombat.SetValue(*BB, true);UBlackboardDataUAISystemUCLASS()
class UMyBTTask_Attack : public UBTTaskNode
{
GENERATED_BODY()
public:
UMyBTTask_Attack() { NodeName = TEXT("Attack"); INIT_TASK_NODE_NOTIFY_FLAGS(); }
UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
EBTNodeResult::Type UMyBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AActor* Target = Cast<AActor>(
OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
if (!IsValid(Target)) return EBTNodeResult::Failed;
// Start async work → return InProgress; call FinishLatentTask() when done
return EBTNodeResult::InProgress;
}
void UMyBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); // or Failed
}
EBTNodeResult::Type UMyBTTask_Attack::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
return EBTNodeResult::Aborted; // cleanup; or InProgress + FinishLatentAbort()
}UCLASS()
class UMyBTDecorator_CanSee : public UBTDecorator
{
GENERATED_BODY()
public:
UMyBTDecorator_CanSee()
{
INIT_DECORATOR_NODE_NOTIFY_FLAGS();
bAllowAbortLowerPri = true; bAllowAbortChildNodes = true;
FlowAbortMode = EBTFlowAbortMode::Both;
}
UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey;
protected:
virtual bool CalculateRawConditionValue(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override
{
AActor* Target = Cast<AActor>(
OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetKey.SelectedKeyName));
return IsValid(Target) && OwnerComp.GetAIOwner()->LineOfSightTo(Target);
}
};UCLASS()
class UMyBTService_UpdateTarget : public UBTService
{
GENERATED_BODY()
public:
UMyBTService_UpdateTarget() { Interval = 0.5f; RandomDeviation = 0.1f; INIT_SERVICE_NODE_NOTIFY_FLAGS(); }
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override
{
TArray<AActor*> Hostiles;
OwnerComp.GetAIOwner()->GetAIPerceptionComponent()->GetPerceivedHostileActors(Hostiles);
AActor* Best = nullptr; float BestDist = FLT_MAX;
FVector MyLoc = OwnerComp.GetAIOwner()->GetPawn()->GetActorLocation();
for (AActor* A : Hostiles)
{
float D = FVector::Dist(MyLoc, A->GetActorLocation());
if (D < BestDist) { BestDist = D; Best = A; }
}
OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), Best);
}
};BTTask_MoveToBTTask_MoveDirectlyTowardBTTask_WaitBTTask_WaitBlackboardTimeBTTask_RunEQSQueryBTTask_PlayAnimationBTTask_MakeNoiseBTTask_RotateToFaceBBEntryBTTask_RunBehaviorBTTask_RunBehaviorDynamicBTTask_FinishWithResultBTDecorator_BlackboardBTDecorator_CompareBBEntriesBTDecorator_CooldownBTDecorator_TagCooldownBTDecorator_LoopBTDecorator_TimeLimitBTDecorator_DoesPathExistBTDecorator_IsAtLocationBTDecorator_CheckGameplayTagsOnActorBTDecorator_ForceSuccessSelectorSequenceSimpleParallelSimpleParallelUBTCompositeNodeSimpleParallel// In AIController constructor:
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AISenseConfig_Damage.h"
UAIPerceptionComponent* AIP = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*AIP);
UAISenseConfig_Sight* Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight"));
Sight->SightRadius = 2000.f;
Sight->LoseSightRadius = 2500.f;
Sight->PeripheralVisionAngleDegrees = 60.f;
Sight->AutoSuccessRangeFromLastSeenLocation = 400.f;
Sight->DetectionByAffiliation.bDetectEnemies = true;
AIP->ConfigureSense(*Sight);
AIP->SetDominantSense(Sight->GetSenseImplementation());
UAISenseConfig_Hearing* Hearing = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Hearing"));
Hearing->HearingRange = 3000.f;
Hearing->DetectionByAffiliation.bDetectEnemies = true;
Hearing->DetectionByAffiliation.bDetectNeutrals = true;
AIP->ConfigureSense(*Hearing);// Perception handler:
void AMyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
UBlackboardComponent* BB = GetBlackboardComponent();
if (Stimulus.WasSuccessfullySensed())
{
BB->SetValueAsObject(TEXT("TargetActor"), Actor);
BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
}
else
{
// Lost target — keep last known location for investigation
BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
}
}
// Report noise manually (e.g., gunshot):
UAISense_Hearing::ReportNoiseEvent(GetWorld(), Location, Loudness, Instigator, MaxRange, Tag);
// Report damage:
UAISense_Damage::ReportDamageEvent(GetWorld(), DamagedActor, Instigator, Amount, EventLoc, HitLoc);// On perceived actors — add UAIPerceptionStimuliSourceComponent:
#include "Perception/AIPerceptionStimuliSourceComponent.h"
UAIPerceptionStimuliSourceComponent* Source =
CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("StimuliSource"));
Source->bAutoRegister = true;
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Sight::StaticClass()));
Source->RegisterForSense(TSubclassOf<UAISense>(UAISense_Hearing::StaticClass()));// Query perception state:
UAIPerceptionComponent* PC = GetAIPerceptionComponent();
TArray<AActor*> Visible; PC->GetCurrentlyPerceivedActors(UAISense_Sight::StaticClass(), Visible);
TArray<AActor*> Hostiles; PC->GetPerceivedHostileActors(Hostiles);
bool bCanSee = PC->HasActiveStimulus(*Target, UAISense::GetSenseID<UAISense_Sight>());
PC->ForgetActor(Target);
PC->ForgetAll();
// Per-actor delegate (shown above):
// OnTargetPerceptionUpdated — fires once per actor whose perception state changed
// Batch delegate — fires once per frame with all updated actors:
// OnPerceptionUpdated — signature: void(const TArray<AActor*>& UpdatedActors)
// Also: OnTargetPerceptionForgotten, OnTargetPerceptionInfoUpdatedUAISenseConfig_TouchUAISense_TeamIGenericTeamAgentInterfaceUAISense_PredictionUAISense_Prediction::RequestPawnPredictionEvent#include "NavigationSystem.h"
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
// Random reachable point
FNavLocation ResultLoc;
bool bOk = NavSys->GetRandomReachablePointInRadius(Origin, Radius, ResultLoc);
// Project onto NavMesh
FNavLocation Projected;
NavSys->ProjectPointToNavigation(WorldLoc, Projected, FVector(500, 500, 500));
// Sync path query
FPathFindingQuery Query; Query.StartLocation = Start; Query.EndLocation = End;
FPathFindingResult Result = NavSys->FindPathSync(Query);
if (Result.IsSuccessful()) { TArray<FNavPathPoint>& Pts = Result.Path->GetPathPoints(); }
// Async path query
FNavAgentProperties NavAgent = GetPawn()->GetNavAgentPropertiesRef();
NavSys->FindPathAsync(NavAgent, Query,
FNavPathQueryDelegate::CreateUObject(this, &AMyAI::OnPathFound));
// Dynamic obstacle on actor:
#include "NavModifierComponent.h"
UNavModifierComponent* Mod = CreateDefaultSubobject<UNavModifierComponent>(TEXT("NavMod"));
Mod->AreaClass = UNavArea_Obstacle::StaticClass();
// Custom nav filter (prefer certain areas):
UCLASS() class UMyNavFilter : public UNavigationQueryFilter { ... };
MoveToActor(Target, -1.f, true, true, true, UMyNavFilter::StaticClass());
// Runtime NavMesh rebuild (after procedural generation):
NavSys->Build();
// Access RecastNavMesh for agent config (mirrors Project Settings > Navigation)
ARecastNavMesh* RNM = Cast<ARecastNavMesh>(NavSys->GetDefaultNavDataInstance());
// Key properties: AgentRadius, AgentHeight, AgentMaxStepHeight, AgentMaxSlope
// ANavMeshBoundsVolume must exist in level — without it, no tiles generate.
// For streaming/open-world: use UNavigationInvokerComponent on AI pawns instead.ANavLinkProxyINavLinkCustomInterface#include "EnvironmentQuery/EnvQueryManager.h"
UPROPERTY(EditDefaultsOnly) TObjectPtr<UEnvQuery> FindCoverQuery;
void AMyAIController::RunCoverQuery()
{
FEnvQueryRequest Request(FindCoverQuery, this);
Request.Execute(EEnvQueryRunMode::SingleResult,
FQueryFinishedSignature::CreateUObject(this, &AMyAIController::OnCoverDone));
}
void AMyAIController::OnCoverDone(TSharedPtr<FEnvQueryResult> Result)
{
if (Result.IsValid() && Result->IsSuccessful())
GetBlackboardComponent()->SetValueAsVector(TEXT("CoverLocation"),
Result->GetItemAsLocation(0));
}SingleResultRandomBest5PctRandomBest25PctAllMatchingEQSRequest.QueryTemplateBlackboardKeyRunModeFBTEnvQueryTaskMemory.RequestIDUCLASS()
class UEnvQueryContext_Enemy : public UEnvQueryContext
{
GENERATED_BODY()
virtual void ProvideContext(FEnvQueryInstance& QI, FEnvQueryContextData& CD) const override
{
AAIController* C = Cast<AAIController>(Cast<APawn>(QI.Owner.Get())->GetController());
AActor* Enemy = Cast<AActor>(C->GetBlackboardComponent()->GetValueAsObject(TEXT("TargetActor")));
if (IsValid(Enemy)) UEnvQueryItemType_Actor::SetContextHelper(CD, Enemy);
}
};references/eqs-reference.md// Build.cs: "GameplayStateTreeModule"
#include "Components/StateTreeComponent.h"
UCLASS()
class AMyNPC : public ACharacter
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere) TObjectPtr<UStateTreeComponent> StateTreeComp;
};
AMyNPC::AMyNPC() { StateTreeComp = CreateDefaultSubobject<UStateTreeComponent>(TEXT("StateTree")); }
void AMyNPC::BeginPlay() { Super::BeginPlay(); StateTreeComp->StartLogic(); }FStateTreeTaskBaseFInstanceDataTypeEnterStateTickExitStateFStateTreeEvaluatorBaseUSmartObjectComponentSmartObjectDefinitionUSmartObjectSubsystem// Build.cs: "SmartObjectsModule"
#include "SmartObjectSubsystem.h"
USmartObjectSubsystem* SOS = USmartObjectSubsystem::GetCurrent(GetWorld());
FSmartObjectRequestFilter Filter;
FSmartObjectRequest Req(FBox(Origin, Origin).ExpandBy(500.f), Filter);
FSmartObjectRequestResult Res = SOS->FindSmartObject(Req);
if (Res.IsValid())
{
FSmartObjectClaimHandle Handle = SOS->MarkSlotAsClaimed(Res.SlotHandle, ESmartObjectClaimPriority::Normal);
// ... use slot, then:
SOS->MarkSlotAsFree(Handle);
}TickTaskBTDecorator_BlackboardWaitForMessageInterval >= 0.5sBTDecorator_CooldownANavMeshBoundsVolumeNavSys->Build()UNavigationInvokerComponentAAIControllerFNavAgentProperties::bCanFlybCanSwimUNavAreaSupportedAgentsUNavAreaUNavArea_WaterHasAuthority()ue-actor-component-architectureue-gameplay-frameworkue-cpp-foundationsue-gameplay-abilitiesreferences/behavior-tree-patterns.mdreferences/eqs-reference.md