I’m currently working on a project in C++ that involves StateTree and AI Perception. My goal is to create an NPC character that will run towards the enemy when it sees or hears them, and this is done using delegates, not on a tick. While the overall concept works, there are some issues with the implementation.
The NPC can see and hear the enemy when they make a sound, but after the enemy has been destroyed, the “Hearing Perception” becomes false, while the “Sight Perception” remains true. I’m not sure why this is happening, as the NPC should not be able to see the player.
Additionally, I noticed that the code responsible for the NPC forgetting about the enemies does not seem to be firing immediately after the enemy is destroyed. I can’t determine when this happens, and it’s causing the issue with the Sight Perception.
I have attached the relevant code and a screenshot of the StateTree. Can you please help me identify the problem and suggest a solution? Thank you for your assistance!
NPC_AIController:
// Fill out your copyright notice in the Description page of Project Settings.
#include "NPC/NPC_AIController.h"
#include "Enemy.h"
#include "HearingProjectCharacter.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AISense_Hearing.h"
#include "Components/StateTreeComponent.h"
#include "StateTreeExecutionContext.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISense_Sight.h"
#include "Slate/SGameLayerManager.h"
ANPC_AIController::ANPC_AIController(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
{
StateTreeComp = CreateDefaultSubobject<UStateTreeComponent>(TEXT("StateTreeComp"));
StateTreeComp->SetStartLogicAutomatically(false);
PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetupPerceptionSystem();
isEnemyHear = false;
isEnemySee = false;
}
UStateTreeComponent* ANPC_AIController::GetStateTreeComp()
{
return StateTreeComp;
}
bool ANPC_AIController::getIsEnemySee()
{
return isEnemySee;
}
bool ANPC_AIController::HasVisibleEnemy() const
{
if (!PerceptionComponent) return false;
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetCurrentlyPerceivedActors(UAISense_Sight::StaticClass(), PerceivedActors);
for (AActor* Actor : PerceivedActors)
{
if (IsValid(Actor) && Cast<AEnemy>(Actor))
{
return true;
}
}
return false;
}
FVector ANPC_AIController::GetNearestVisibleEnemyLocation() const
{
if (!PerceptionComponent) return FVector::ZeroVector;
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetCurrentlyPerceivedActors(UAISense_Sight::StaticClass(), PerceivedActors);
FVector NearestLoc;
float NearestDist = TNumericLimits<float>::Max();
APawn* ZPawn = GetPawn();
FVector PawnPos = ZPawn ? ZPawn->GetActorLocation() : FVector::ZeroVector;
for (AActor* Actor : PerceivedActors)
{
if (!IsValid(Actor)) continue;
if (AEnemy* EnemyActor = Cast<AEnemy>(Actor))
{
if (!IsValid(EnemyActor)) continue;
float Dist = FVector::Dist(PawnPos, Actor->GetActorLocation());
if (Dist < NearestDist)
{
NearestDist = Dist;
NearestLoc = Actor->GetActorLocation();
}
}
}
return NearestLoc;
}
bool ANPC_AIController::HasHeardEnemy() const
{
if (!PerceptionComponent) return false;
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetCurrentlyPerceivedActors(UAISense_Hearing::StaticClass(), PerceivedActors);
for (AActor* Actor : PerceivedActors)
{
if (IsValid(Actor) && Cast<AEnemy>(Actor))
{
return true;
}
}
return false;
}
FVector ANPC_AIController::getSeenEnemyLocation()
{
return seenEnemyLocation;
}
FVector ANPC_AIController::getHeardEnemyLocation()
{
return heardEnemyLocation;
}
FVector ANPC_AIController::GetNearestHeardEnemyLocation() const
{
if (!PerceptionComponent) return FVector::ZeroVector;
TArray<AActor*> PerceivedActors;
PerceptionComponent->GetCurrentlyPerceivedActors(UAISense_Hearing::StaticClass(), PerceivedActors);
FVector HeardLoc;
float NearestDist = TNumericLimits<float>::Max();
APawn* ZPawn = GetPawn();
FVector PawnPos = ZPawn ? ZPawn->GetActorLocation() : FVector::ZeroVector;
for (AActor* Actor : PerceivedActors)
{
if (!IsValid(Actor)) continue;
if (AEnemy* EnemyActor = Cast<AEnemy>(Actor))
{
if (!IsValid(EnemyActor)) continue;
float Dist = FVector::Dist(PawnPos, Actor->GetActorLocation());
if (Dist < NearestDist)
{
NearestDist = Dist;
HeardLoc = Actor->GetActorLocation();
}
}
}
return HeardLoc;
}
bool ANPC_AIController::getIsEnemyHear()
{
return isEnemyHear;
}
void ANPC_AIController::BeginPlay()
{
Super::BeginPlay();
StateTreeComp->StartLogic();
}
void ANPC_AIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (StateTreeAsset && StateTreeComp)
{
StateTreeComp->SetStateTree(StateTreeAsset);
}
}
void ANPC_AIController::SetupPerceptionSystem()
{
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight Config"));
SightConfig->SightRadius=1000.f;
SightConfig->LoseSightRadius = SightConfig->SightRadius + 25.f;
SightConfig->PeripheralVisionAngleDegrees = 180.f;
SightConfig->SetMaxAge(5.f);
SightConfig->AutoSuccessRangeFromLastSeenLocation = 3000.f;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
HearingConfig = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("Hearing Config"));
HearingConfig->HearingRange = 2000.f;
HearingConfig->LoSHearingRange = HearingConfig->HearingRange + 50.f;
HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;
HearingConfig->SetMaxAge(5.f);
GetPerceptionComponent()->SetDominantSense(SightConfig->GetSenseImplementation());
GetPerceptionComponent()->ConfigureSense(*SightConfig);
GetPerceptionComponent()->ConfigureSense(*HearingConfig);
GetPerceptionComponent()->OnTargetPerceptionUpdated.AddDynamic(this, &ANPC_AIController::OnTargetPerceptionUpdated);
GetPerceptionComponent()->OnTargetPerceptionForgotten.AddDynamic(this, &ANPC_AIController::OnTargetPerceptionForgotten);
}
/*void ANPC_AIController::OnTargetDetected(AActor* Actor, FAIStimulus const Stimulus)
{
isEnemySee = HasVisibleEnemy();
isEnemyHear = HasHeardEnemy();
seenEnemyLocation = GetNearestVisibleEnemyLocation();
heardEnemyLocation = GetNearestHeardEnemyLocation();
}*/
void ANPC_AIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (AEnemy* Enemy = Cast<AEnemy>(Actor))
{
if (!Cast<AHearingProjectCharacter>(Actor))
{
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
{
if (Stimulus.WasSuccessfullySensed())
{
isEnemySee = true;
seenEnemyLocation = Stimulus.StimulusLocation;
OnSightStimulusDetected.Broadcast(Actor, Stimulus);
}
else
{
isEnemySee = false;
OnSightStimulusForgotten.Broadcast(Actor);
}
}
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>())
{
if (Stimulus.WasSuccessfullySensed())
{
isEnemyHear = true;
heardEnemyLocation = Stimulus.StimulusLocation;
OnHearingStimulusDetected.Broadcast(Actor, Stimulus);
}
else
{
isEnemyHear = false;
OnHearingStimulusForgotten.Broadcast(Actor);
}
}
}
}
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 4.0f, FColor::Yellow,
FString::Printf(TEXT("Updated: %s | See: %s | Hear: %s | Loc: %s"),
*GetNameSafe(Actor),
isEnemySee ? TEXT("TRUE") : TEXT("FALSE"),
isEnemyHear ? TEXT("TRUE") : TEXT("FALSE"),
*seenEnemyLocation.ToCompactString()));
}
}
void ANPC_AIController::OnTargetPerceptionForgotten(AActor* Actor)
{
if (Cast<AEnemy>(Actor))
{
isEnemySee = false;
isEnemyHear = false;
seenEnemyLocation = FVector::ZeroVector;
heardEnemyLocation = FVector::ZeroVector;
}
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Orange,
FString::Printf(TEXT("Forgotten: %s | See: %s | Hear: %s | Loc: %s"),
*GetNameSafe(Actor),
isEnemySee ? TEXT("TRUE") : TEXT("FALSE"),
isEnemyHear ? TEXT("TRUE") : TEXT("FALSE"),
*seenEnemyLocation.ToCompactString()));
}
}
STE_Initializing (STE_Perception)
// Fill out your copyright notice in the Description page of Project Settings.
#include "NPC/STE_Initializing.h"
#include "Kismet/GameplayStatics.h"
#include "Components/StateTreeComponent.h"
#include "StateTreeExecutionContext.h"
#include "StateTreeInstanceData.h"
void USTE_Initializing::Tick(FStateTreeExecutionContext& Context, const float DeltaTime)
{
Super::Tick(Context, DeltaTime);
if (NeedToUpdate)
{
if (bool* isSEnemy = isSeeEnemyRef.GetMutablePtr(Context))
{
*isSEnemy = isSeeEnemy;
//*isSeeEnemy = false;
}
if (bool* isHEnemy = isHearEnemyRef.GetMutablePtr(Context))
{
*isHEnemy = isHearEnemy;
//*isHearEnemy = false;
}
if (FVector* eLoc = EnemyLocRef.GetMutablePtr(Context))
{
*eLoc = SeenEnemyLoc;
}
if (FVector* heardLoc = HeardLocRef.GetMutablePtr(Context))
{
*heardLoc = HeardEnemyLoc;
}
if (FVector* pLoc = PlayerLocRef.GetMutablePtr(Context))
{
*pLoc = PlayerLoc;
}
NeedToUpdate = false;
}
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 0.5f, isSeeEnemy ? FColor::Green : FColor::Red,
FString::Printf(TEXT("SIGHT: %s | Loc: %s"),
isSeeEnemy ? TEXT("TRUE") : TEXT("FALSE"),
*Controller->getSeenEnemyLocation().ToCompactString()));
}
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 0.5f, isHearEnemy ? FColor::Green : FColor::Red,
FString::Printf(TEXT("Hearing: %s | Loc: %s"),
isHearEnemy ? TEXT("TRUE") : TEXT("FALSE"),
*Controller->getHeardEnemyLocation().ToCompactString()));
}
if (FVector* pLoc = PlayerLocRef.GetMutablePtr(Context))
{
if (APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(Context.GetWorld(), 0))
{
FVector PlayerPos = PlayerPawn->GetActorLocation();
FVector NPCPos = Pawn->GetActorLocation();
FVector DirectionToPlayer = (PlayerPos - NPCPos).GetSafeNormal();
float FollowDistance = 150.0f;
FVector TargetPos = PlayerPos - (DirectionToPlayer * FollowDistance);
*pLoc = TargetPos;
}
else
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("No PlayerPawn found!"));
}
}
}
void USTE_Initializing::TreeStart(FStateTreeExecutionContext& Context)
{
Super::TreeStart(Context);
Controller->OnSightStimulusDetected.AddDynamic(this, &USTE_Initializing::HandleSightStimulus);
Controller->OnSightStimulusForgotten.AddDynamic(this, &USTE_Initializing::HandleSightStimulusForgotten);
Controller->OnHearingStimulusDetected.AddDynamic(this, &USTE_Initializing::HandleHearingStimulus);
Controller->OnHearingStimulusForgotten.AddDynamic(this, &USTE_Initializing::HandleHearingStimulusForgotten);
StateTreeComp = Controller->GetStateTreeComp();
}
void USTE_Initializing::TreeStop(FStateTreeExecutionContext& Context)
{
Super::TreeStop(Context);
if (Controller)
{
Controller->OnSightStimulusDetected.RemoveDynamic(this, &USTE_Initializing::HandleSightStimulus);
Controller->OnSightStimulusForgotten.RemoveDynamic(this, &USTE_Initializing::HandleSightStimulusForgotten);
Controller->OnHearingStimulusDetected.RemoveDynamic(this, &USTE_Initializing::HandleHearingStimulus);
Controller->OnHearingStimulusForgotten.RemoveDynamic(this, &USTE_Initializing::HandleHearingStimulusForgotten);
}
StateTreeComp = nullptr;
}
void USTE_Initializing::HandleSightStimulus(AActor* Actor, const FAIStimulus& Stimulus)
{
OnSightStimulus(Actor, Stimulus);
isSeeEnemy = true;
//SeenEnemyLoc = Stimulus.StimulusLocation;
SeenEnemyLoc = Controller->GetNearestVisibleEnemyLocation();
NeedToUpdate = true;
}
void USTE_Initializing::HandleSightStimulusForgotten(AActor* Actor)
{
OnSightStimulusForgotten(Actor);
isSeeEnemy = false;
SeenEnemyLoc = FVector::ZeroVector;
NeedToUpdate = true;
}
void USTE_Initializing::HandleHearingStimulus(AActor* Actor, const FAIStimulus& Stimulus)
{
OnHearingStimulus(Actor, Stimulus);
isHearEnemy = true;
//HeardEnemyLoc = Stimulus.StimulusLocation;
HeardEnemyLoc = Controller->GetNearestHeardEnemyLocation();
NeedToUpdate = true;
}
void USTE_Initializing::HandleHearingStimulusForgotten(AActor* Actor)
{
OnHearingStimulusForgotten(Actor);
isHearEnemy = false;
HeardEnemyLoc = FVector::ZeroVector;
NeedToUpdate = true;
}
StateTree:
