Fix combat BT stuck on spline, LOS through target capsule, NoTeam targeting
- Attack/CoverShootCycle: de-escalate to Alerted on failure so decorator can re-trigger when threat returns (fixes BT stuck on spline branch) - UpdateThreat: keep BB ThreatActor during brief perception gaps instead of clearing immediately (use LOS timeout for graceful degradation) - HasLineOfSight: ignore actors up the attachment chain so trace doesn't hit the target Pawn's capsule when aiming at its AimTarget child actor - NoTeam actors (spectators, editor pawns) treated as Neutral instead of Hostile, plus SpectatorPawn explicit filter in perception - BB debug key ThreatPawnName shows owning Pawn name (resolved via perception's LastThreatOwningPawn) instead of cryptic AimTarget name - FindOwningPawn promoted to public static on PerceptionComponent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2588883a1c
commit
65b86e2fbd
@ -23,13 +23,13 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
|||||||
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
||||||
|
|
||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC) return;
|
if (!AIC) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("EvaluateReaction: no AIC!")); return; }
|
||||||
|
|
||||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||||
if (!Personality) return;
|
if (!Personality) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] EvaluateReaction: no PersonalityComponent!"), *AIC->GetName()); return; }
|
||||||
|
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
if (!BB) return;
|
if (!BB) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] EvaluateReaction: no BB!"), *AIC->GetName()); return; }
|
||||||
|
|
||||||
// ─── Check for hostility change → update TeamId dynamically ────────
|
// ─── Check for hostility change → update TeamId dynamically ────────
|
||||||
APawn* Pawn = AIC->GetPawn();
|
APawn* Pawn = AIC->GetPawn();
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "PS_AI_Behavior_AIController.h"
|
#include "PS_AI_Behavior_AIController.h"
|
||||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
|
#include "PS_AI_Behavior_Statics.h"
|
||||||
#include "PS_AI_Behavior_Settings.h"
|
#include "PS_AI_Behavior_Settings.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
@ -65,19 +66,157 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
|
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
||||||
|
|
||||||
// Update threat actor and location
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] UpdateThreat: raw=%.2f, stored=%.2f, final=%.2f"),
|
||||||
|
*AIC->GetName(), RawThreat, StoredThreat, FinalThreat);
|
||||||
|
|
||||||
|
// ─── Update threat actor and location with LOS tracking ─────────
|
||||||
|
FUpdateThreatMemory* Memory = reinterpret_cast<FUpdateThreatMemory*>(NodeMemory);
|
||||||
AActor* ThreatActor = Perception->GetHighestThreatActor();
|
AActor* ThreatActor = Perception->GetHighestThreatActor();
|
||||||
|
AActor* CurrentBBTarget = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
|
|
||||||
if (ThreatActor)
|
if (ThreatActor)
|
||||||
{
|
{
|
||||||
|
// Target switched by perception (higher score) → reset LOS tracking
|
||||||
|
if (ThreatActor != CurrentBBTarget)
|
||||||
|
{
|
||||||
|
Memory->TimeSinceLOS = 0.0f;
|
||||||
|
Memory->bHadLOS = true;
|
||||||
|
Memory->bInvestigating = false;
|
||||||
|
Memory->LastVisiblePosition = ThreatActor->GetActorLocation();
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] UpdateThreat: target switched to '%s', LOS tracking reset"),
|
||||||
|
*AIC->GetName(), *ThreatActor->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor);
|
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor);
|
||||||
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
|
||||||
|
// Debug: write the owning Pawn's name so BB shows who we're actually targeting
|
||||||
|
if (ThreatActor != CurrentBBTarget)
|
||||||
|
{
|
||||||
|
APawn* OwningPawn = Perception->LastThreatOwningPawn.Get();
|
||||||
|
BB->SetValueAsString(PS_AI_Behavior_BB::ThreatPawnName,
|
||||||
|
OwningPawn ? OwningPawn->GetName() : ThreatActor->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── LOS check ──────────────────────────────────────────────
|
||||||
|
const APawn* Pawn = AIC->GetPawn();
|
||||||
|
const bool bHasLOS = Pawn
|
||||||
|
? UPS_AI_Behavior_Statics::HasLineOfSight(Pawn->GetWorld(), Pawn, ThreatActor, EyeHeightOffset)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (bHasLOS)
|
||||||
|
{
|
||||||
|
// Can see target → update last visible position, reset timer
|
||||||
|
Memory->LastVisiblePosition = ThreatActor->GetActorLocation();
|
||||||
|
Memory->TimeSinceLOS = 0.0f;
|
||||||
|
Memory->bHadLOS = true;
|
||||||
|
Memory->bInvestigating = false;
|
||||||
|
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Cannot see target → accumulate time, write last known position
|
||||||
|
Memory->TimeSinceLOS += DeltaSeconds;
|
||||||
|
|
||||||
|
// Write ThreatLocation as the LAST VISIBLE position (not real-time)
|
||||||
|
if (!Memory->LastVisiblePosition.IsZero())
|
||||||
|
{
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, Memory->LastVisiblePosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: after LOSLostTimeout → write LastKnownTargetPosition for investigation
|
||||||
|
if (Memory->TimeSinceLOS > LOSLostTimeout && !Memory->bInvestigating)
|
||||||
|
{
|
||||||
|
Memory->bInvestigating = true;
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::LastKnownTargetPosition, Memory->LastVisiblePosition);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] UpdateThreat: LOS lost for %.0fs → investigating last known position %s"),
|
||||||
|
*AIC->GetName(), Memory->TimeSinceLOS, *Memory->LastVisiblePosition.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: after 2x timeout → give up, clear target so NPC can re-evaluate
|
||||||
|
if (Memory->TimeSinceLOS > LOSLostTimeout * 2.0f)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] UpdateThreat: LOS lost for %.0fs → clearing target, de-escalating"),
|
||||||
|
*AIC->GetName(), Memory->TimeSinceLOS);
|
||||||
|
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatPawnName);
|
||||||
|
Memory->TimeSinceLOS = 0.0f;
|
||||||
|
Memory->bInvestigating = false;
|
||||||
|
Memory->bHadLOS = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CurrentBBTarget)
|
||||||
|
{
|
||||||
|
// Perception lost the actor momentarily, but we had a target in BB.
|
||||||
|
// Don't clear immediately — use the LOS tracking timeout instead.
|
||||||
|
// This prevents brief perception gaps (1-2 ticks) from dropping the target.
|
||||||
|
Memory->TimeSinceLOS += DeltaSeconds;
|
||||||
|
|
||||||
|
// Keep ThreatLocation at last visible position
|
||||||
|
if (!Memory->LastVisiblePosition.IsZero())
|
||||||
|
{
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, Memory->LastVisiblePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: after LOSLostTimeout → investigate last known position
|
||||||
|
if (Memory->TimeSinceLOS > LOSLostTimeout && !Memory->bInvestigating)
|
||||||
|
{
|
||||||
|
Memory->bInvestigating = true;
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::LastKnownTargetPosition, Memory->LastVisiblePosition);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] UpdateThreat: perception lost target for %.0fs → investigating last known position %s"),
|
||||||
|
*AIC->GetName(), Memory->TimeSinceLOS, *Memory->LastVisiblePosition.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: after 2x timeout → give up, clear target
|
||||||
|
if (Memory->TimeSinceLOS > LOSLostTimeout * 2.0f)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] UpdateThreat: perception lost target for %.0fs → clearing target, de-escalating"),
|
||||||
|
*AIC->GetName(), Memory->TimeSinceLOS);
|
||||||
|
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatPawnName);
|
||||||
|
Memory->TimeSinceLOS = 0.0f;
|
||||||
|
Memory->bInvestigating = false;
|
||||||
|
Memory->bHadLOS = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No valid threat — clear BB and force threat to zero
|
// No threat actor at all (never had one)
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
Memory->TimeSinceLOS = 0.0f;
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
Memory->bInvestigating = false;
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
Memory->bHadLOS = true;
|
||||||
|
|
||||||
|
if (FinalThreat > 0.0f)
|
||||||
|
{
|
||||||
|
// Threat exists but no specific actor target (e.g. gunshot from friendly).
|
||||||
|
FVector ThreatLoc;
|
||||||
|
if (Perception->GetGunShotStimulusLocation(ThreatLoc))
|
||||||
|
{
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to PersonalityComponent
|
// Sync to PersonalityComponent
|
||||||
@ -90,5 +229,7 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
|
|
||||||
FString UPS_AI_Behavior_BTService_UpdateThreat::GetStaticDescription() const
|
FString UPS_AI_Behavior_BTService_UpdateThreat::GetStaticDescription() const
|
||||||
{
|
{
|
||||||
return TEXT("Updates Blackboard threat data from perception.\nWrites: ThreatActor, ThreatLocation, ThreatLevel.");
|
return FString::Printf(
|
||||||
|
TEXT("Updates Blackboard threat data from perception.\nLOS timeout: %.0fs (clear at %.0fs)\nWrites: ThreatActor, ThreatLocation, ThreatLevel, LastKnownTargetPosition."),
|
||||||
|
LOSLostTimeout, LOSLostTimeout * 2.0f);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,12 @@
|
|||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
|
#include "PS_AI_Behavior_Statics.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
#include "Navigation/PathFollowingComponent.h"
|
#include "Navigation/PathFollowingComponent.h"
|
||||||
#include "NavigationSystem.h"
|
#include "NavigationSystem.h"
|
||||||
|
#include "EnvironmentQuery/EnvQuery.h"
|
||||||
|
#include "EnvironmentQuery/EnvQueryManager.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_BTTask_Attack::UPS_AI_Behavior_BTTask_Attack()
|
UPS_AI_Behavior_BTTask_Attack::UPS_AI_Behavior_BTTask_Attack()
|
||||||
{
|
{
|
||||||
@ -21,15 +24,25 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||||
{
|
{
|
||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
if (!AIC || !AIC->GetPawn())
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("Attack: FAILED — no AIC or Pawn"));
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
if (!BB) return EBTNodeResult::Failed;
|
if (!BB)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: FAILED — no BB"), *AIC->GetName());
|
||||||
|
return EBTNodeResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: no ThreatActor in BB."), *AIC->GetName());
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: FAILED — no ThreatActor in BB"), *AIC->GetName());
|
||||||
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +53,11 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
{
|
{
|
||||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
||||||
{
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: FAILED — IsTargetActorValid returned false for '%s'"),
|
||||||
|
*AIC->GetName(), *Target->GetName());
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
return EBTNodeResult::Failed;
|
return EBTNodeResult::Failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,12 +67,16 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
Memory->bMovingToTarget = false;
|
Memory->bMovingToTarget = false;
|
||||||
Memory->bAttacking = false;
|
Memory->bAttacking = false;
|
||||||
Memory->bInRange = false;
|
Memory->bInRange = false;
|
||||||
|
Memory->bHasLOS = true;
|
||||||
|
Memory->bSeekingFiringPos = false;
|
||||||
|
Memory->bEQSQueryRunning = false;
|
||||||
Memory->RepositionTimer = 0.0f;
|
Memory->RepositionTimer = 0.0f;
|
||||||
|
Memory->LOSCheckTimer = 0.0f;
|
||||||
Memory->CombatType = EPS_AI_Behavior_CombatType::Melee;
|
Memory->CombatType = EPS_AI_Behavior_CombatType::Melee;
|
||||||
Memory->MinRange = 100.0f;
|
Memory->MinRange = 100.0f;
|
||||||
Memory->MaxRange = AttackMoveRadius;
|
Memory->MaxRange = AttackMoveRadius;
|
||||||
|
|
||||||
// CombatType from interface (depends on weapon/pawn)
|
// CombatType from interface
|
||||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
Memory->CombatType = IPS_AI_Behavior_Interface::Execute_GetBehaviorCombatType(Pawn);
|
Memory->CombatType = IPS_AI_Behavior_Interface::Execute_GetBehaviorCombatType(Pawn);
|
||||||
@ -72,7 +92,14 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Melee: approach to half MinRange (get close). Ranged: approach to midpoint of band.
|
// ─── Initial LOS check (ranged only) ────────────────────────────
|
||||||
|
if (Memory->CombatType == EPS_AI_Behavior_CombatType::Ranged)
|
||||||
|
{
|
||||||
|
Memory->bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||||
|
Pawn->GetWorld(), Pawn, Target, EyeHeightOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Melee: approach to half MinRange. Ranged: approach to midpoint of band.
|
||||||
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||||
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||||
|
|
||||||
@ -81,7 +108,8 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
if (DistToTarget <= Memory->MaxRange)
|
if (DistToTarget <= Memory->MaxRange)
|
||||||
{
|
{
|
||||||
Memory->bInRange = true;
|
Memory->bInRange = true;
|
||||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
// Only start attacking if we have LOS (ranged) or always (melee)
|
||||||
|
if (Memory->bHasLOS && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||||
Memory->bAttacking = true;
|
Memory->bAttacking = true;
|
||||||
@ -101,14 +129,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: started on '%s' (%s, range=[%.0f-%.0f], dist=%.0f, inRange=%d, attacking=%d, hasInterface=%d)"),
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: started on '%s' (%s, range=[%.0f-%.0f], dist=%.0f, inRange=%d, LOS=%d, attacking=%d)"),
|
||||||
*AIC->GetName(), *Target->GetName(),
|
*AIC->GetName(), *Target->GetName(),
|
||||||
Memory->CombatType == EPS_AI_Behavior_CombatType::Ranged ? TEXT("Ranged") : TEXT("Melee"),
|
Memory->CombatType == EPS_AI_Behavior_CombatType::Ranged ? TEXT("Ranged") : TEXT("Melee"),
|
||||||
Memory->MinRange, Memory->MaxRange, DistToTarget,
|
Memory->MinRange, Memory->MaxRange, DistToTarget,
|
||||||
Memory->bInRange ? 1 : 0, Memory->bAttacking ? 1 : 0,
|
Memory->bInRange ? 1 : 0, Memory->bHasLOS ? 1 : 0, Memory->bAttacking ? 1 : 0);
|
||||||
Pawn->Implements<UPS_AI_Behavior_Interface>() ? 1 : 0);
|
|
||||||
|
|
||||||
// Stay InProgress — the Decorator Observer Aborts will pull us out
|
|
||||||
return EBTNodeResult::InProgress;
|
return EBTNodeResult::InProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,40 +152,56 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
|||||||
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
|
AActor* Target = BB ? Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr;
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if target is still valid (alive, not despawning) via interface
|
|
||||||
APawn* Pawn = AIC->GetPawn();
|
APawn* Pawn = AIC->GetPawn();
|
||||||
|
|
||||||
|
// Validate target
|
||||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
||||||
{
|
{
|
||||||
// Target is dead/invalid — clear BB, StopAttack called by OnTaskFinished
|
|
||||||
AIC->StopMovement();
|
AIC->StopMovement();
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
|
|
||||||
// Tick reposition cooldown
|
// Tick cooldowns
|
||||||
if (Memory->RepositionTimer > 0.0f)
|
if (Memory->RepositionTimer > 0.0f)
|
||||||
{
|
{
|
||||||
Memory->RepositionTimer -= DeltaSeconds;
|
Memory->RepositionTimer -= DeltaSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Periodic LOS check (ranged only) ───────────────────────────
|
||||||
|
if (Memory->CombatType == EPS_AI_Behavior_CombatType::Ranged)
|
||||||
|
{
|
||||||
|
Memory->LOSCheckTimer -= DeltaSeconds;
|
||||||
|
if (Memory->LOSCheckTimer <= 0.0f)
|
||||||
|
{
|
||||||
|
Memory->LOSCheckTimer = LOSCheckInterval;
|
||||||
|
Memory->bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||||
|
Pawn->GetWorld(), Pawn, Target, EyeHeightOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Melee always has "LOS" (they just rush)
|
||||||
|
|
||||||
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
||||||
const bool bCanReposition = (Memory->RepositionTimer <= 0.0f);
|
const bool bCanReposition = (Memory->RepositionTimer <= 0.0f);
|
||||||
|
|
||||||
|
// ─── MOVEMENT LOGIC ─────────────────────────────────────────────
|
||||||
if (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
if (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||||
{
|
{
|
||||||
// ─── Melee: continuously chase target (no cooldown — always pursue) ──
|
// Melee: continuously chase target (no LOS check — just rush)
|
||||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle && DistToTarget > Memory->MinRange)
|
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle && DistToTarget > Memory->MinRange)
|
||||||
{
|
{
|
||||||
AIC->MoveToLocation(
|
AIC->MoveToLocation(
|
||||||
@ -169,19 +211,52 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// ─── Ranged: maintain distance between MinRange and MaxRange ─
|
// ─── Ranged: LOS-aware positioning ──────────────────────────
|
||||||
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
if (Memory->bSeekingFiringPos)
|
||||||
|
|
||||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
|
||||||
{
|
{
|
||||||
|
// Waiting for firing position move to complete
|
||||||
|
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
|
{
|
||||||
|
Memory->bSeekingFiringPos = false;
|
||||||
|
// Re-check LOS immediately after arriving
|
||||||
|
Memory->LOSCheckTimer = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!Memory->bHasLOS && !Memory->bEQSQueryRunning &&
|
||||||
|
AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
|
{
|
||||||
|
// No LOS and idle → find a firing position
|
||||||
|
// Check if investigation (last known position) is active
|
||||||
|
const FVector LastKnownPos = BB->GetValueAsVector(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
if (!LastKnownPos.IsZero())
|
||||||
|
{
|
||||||
|
// Investigation mode: go to last known position
|
||||||
|
AIC->MoveToLocation(
|
||||||
|
LastKnownPos, 100.0f, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||||
|
Memory->bSeekingFiringPos = true;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: investigating last known position %s"),
|
||||||
|
*AIC->GetName(), *LastKnownPos.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No investigation yet — try EQS for a flanking position
|
||||||
|
RunFiringPositionQuery(OwnerComp, NodeMemory, Pawn, Target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Memory->bHasLOS && AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||||
|
{
|
||||||
|
// Has LOS — normal range maintenance
|
||||||
|
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||||
|
|
||||||
if (bCanReposition && DistToTarget < Memory->MinRange)
|
if (bCanReposition && DistToTarget < Memory->MinRange)
|
||||||
{
|
{
|
||||||
// Too close — back away to midpoint of band
|
// Too close — back away
|
||||||
const FVector AwayDir = (Pawn->GetActorLocation() - Target->GetActorLocation()).GetSafeNormal2D();
|
const FVector AwayDir = (Pawn->GetActorLocation() - Target->GetActorLocation()).GetSafeNormal2D();
|
||||||
const float RetreatDist = MidRange - DistToTarget + 50.0f;
|
const float RetreatDist = MidRange - DistToTarget + 50.0f;
|
||||||
const FVector RetreatPoint = Pawn->GetActorLocation() + AwayDir * RetreatDist;
|
const FVector RetreatPoint = Pawn->GetActorLocation() + AwayDir * RetreatDist;
|
||||||
|
|
||||||
// Project to navmesh
|
|
||||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
||||||
if (NavSys)
|
if (NavSys)
|
||||||
{
|
{
|
||||||
@ -191,53 +266,64 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
|||||||
AIC->MoveToLocation(
|
AIC->MoveToLocation(
|
||||||
NavLoc.Location, 50.0f, /*bStopOnOverlap=*/true,
|
NavLoc.Location, 50.0f, /*bStopOnOverlap=*/true,
|
||||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/false, /*bCanStrafe=*/false);
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/false, /*bCanStrafe=*/false);
|
||||||
// Longer cooldown after retreat to prevent repeated backing
|
|
||||||
Memory->RepositionTimer = RepositionCooldown * 3.0f;
|
Memory->RepositionTimer = RepositionCooldown * 3.0f;
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: backing away %.0fcm (dist=%.0f < min=%.0f)"),
|
|
||||||
*AIC->GetName(), RetreatDist, DistToTarget, Memory->MinRange);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (DistToTarget > Memory->MaxRange)
|
else if (DistToTarget > Memory->MaxRange)
|
||||||
{
|
{
|
||||||
// Too far — advance toward target to midpoint of band (no cooldown — chase aggressively)
|
// Too far — advance
|
||||||
AIC->MoveToLocation(
|
AIC->MoveToLocation(
|
||||||
Target->GetActorLocation(), MidRange, /*bStopOnOverlap=*/true,
|
Target->GetActorLocation(), MidRange, /*bStopOnOverlap=*/true,
|
||||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: advancing to midRange=%.0f (dist=%.0f > max=%.0f)"),
|
|
||||||
*AIC->GetName(), MidRange, DistToTarget, Memory->MaxRange);
|
|
||||||
}
|
}
|
||||||
// else: between MinRange and MaxRange — hold position, Pawn handles shooting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Toggle attack based on range (with hysteresis to prevent flickering) ──
|
// ─── Toggle attack based on range + LOS ─────────────────────────
|
||||||
// Enter range at MaxRange, leave range at MaxRange + buffer
|
|
||||||
const float EnterRange = Memory->MaxRange;
|
const float EnterRange = Memory->MaxRange;
|
||||||
const float LeaveRange = Memory->MaxRange * 1.1f; // 10% hysteresis
|
const float LeaveRange = Memory->MaxRange * 1.1f;
|
||||||
const bool bNowInRange = Memory->bInRange
|
const bool bNowInRange = Memory->bInRange
|
||||||
? (DistToTarget <= LeaveRange) // already in range → need to go PAST LeaveRange to exit
|
? (DistToTarget <= LeaveRange)
|
||||||
: (DistToTarget <= EnterRange); // not in range → need to get WITHIN EnterRange to enter
|
: (DistToTarget <= EnterRange);
|
||||||
|
|
||||||
if (bNowInRange && !Memory->bInRange)
|
// For ranged: require LOS to attack. For melee: always allow.
|
||||||
|
const bool bCanAttack = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee) || Memory->bHasLOS;
|
||||||
|
|
||||||
|
if (bNowInRange && bCanAttack && !Memory->bInRange)
|
||||||
{
|
{
|
||||||
// Entered range → start attacking
|
// Entered range with LOS → start attacking
|
||||||
Memory->bInRange = true;
|
Memory->bInRange = true;
|
||||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>() && !Memory->bAttacking)
|
if (Pawn->Implements<UPS_AI_Behavior_Interface>() && !Memory->bAttacking)
|
||||||
{
|
{
|
||||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||||
Memory->bAttacking = true;
|
Memory->bAttacking = true;
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: in range (%.0f <= %.0f) — StartAttack on '%s'"),
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: in range + LOS → StartAttack on '%s'"),
|
||||||
*AIC->GetName(), DistToTarget, AttackRange, *Target->GetName());
|
*AIC->GetName(), *Target->GetName());
|
||||||
}
|
}
|
||||||
else if (!Pawn->Implements<UPS_AI_Behavior_Interface>())
|
}
|
||||||
|
else if (bNowInRange && !bCanAttack && Memory->bAttacking)
|
||||||
|
{
|
||||||
|
// In range but lost LOS → stop attacking (stay in range though)
|
||||||
|
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Error, TEXT("[%s] Attack: in range but Pawn does NOT implement IPS_AI_Behavior_Interface — StartAttack cannot be called!"),
|
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||||
|
Memory->bAttacking = false;
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: in range but NO LOS → StopAttack"),
|
||||||
*AIC->GetName());
|
*AIC->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (bNowInRange && bCanAttack && !Memory->bAttacking)
|
||||||
|
{
|
||||||
|
// In range, regained LOS → restart attacking
|
||||||
|
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||||
|
Memory->bAttacking = true;
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: LOS regained → StartAttack on '%s'"),
|
||||||
|
*AIC->GetName(), *Target->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (!bNowInRange && Memory->bInRange)
|
else if (!bNowInRange && Memory->bInRange)
|
||||||
{
|
{
|
||||||
// Left range → stop attacking
|
// Left range → stop attacking
|
||||||
@ -246,8 +332,8 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
|||||||
{
|
{
|
||||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||||
Memory->bAttacking = false;
|
Memory->bAttacking = false;
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: out of range (%.0f > %.0f) — StopAttack"),
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: out of range → StopAttack"),
|
||||||
*AIC->GetName(), DistToTarget, AttackRange);
|
*AIC->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,14 +346,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
|||||||
{
|
{
|
||||||
AIC->StopMovement();
|
AIC->StopMovement();
|
||||||
}
|
}
|
||||||
// StopAttack is called by OnTaskFinished (covers all exit paths)
|
|
||||||
return EBTNodeResult::Aborted;
|
return EBTNodeResult::Aborted;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished(
|
void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished(
|
||||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
|
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
|
||||||
{
|
{
|
||||||
// Always stop attacking when the task ends, regardless of how it ended
|
|
||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (AIC)
|
if (AIC)
|
||||||
{
|
{
|
||||||
@ -281,8 +365,99 @@ void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished(
|
|||||||
Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult);
|
Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── EQS Firing Position Query ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_BTTask_Attack::RunFiringPositionQuery(
|
||||||
|
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||||
|
APawn* Pawn, AActor* Target)
|
||||||
|
{
|
||||||
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
|
|
||||||
|
if (!FiringPositionQuery)
|
||||||
|
{
|
||||||
|
// No EQS query configured → fallback: advance toward target
|
||||||
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
|
if (AIC)
|
||||||
|
{
|
||||||
|
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||||
|
AIC->MoveToLocation(
|
||||||
|
Target->GetActorLocation(), MidRange, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||||
|
Memory->bSeekingFiringPos = true;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||||
|
TEXT("[%s] Attack: no EQS query, fallback advance toward target"),
|
||||||
|
*AIC->GetName());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* World = Pawn->GetWorld();
|
||||||
|
UEnvQueryManager* EQSManager = UEnvQueryManager::GetCurrent(World);
|
||||||
|
if (!EQSManager)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Memory->bEQSQueryRunning = true;
|
||||||
|
TWeakObjectPtr<AActor> WeakTarget = Target;
|
||||||
|
|
||||||
|
FEnvQueryRequest Request(FiringPositionQuery, Pawn);
|
||||||
|
Request.Execute(EEnvQueryRunMode::SingleResult,
|
||||||
|
FQueryFinishedSignature::CreateUObject(this,
|
||||||
|
&UPS_AI_Behavior_BTTask_Attack::OnFiringPositionQueryFinished,
|
||||||
|
&OwnerComp, NodeMemory, WeakTarget));
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: EQS firing position query launched"),
|
||||||
|
*Pawn->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_BTTask_Attack::OnFiringPositionQueryFinished(
|
||||||
|
TSharedPtr<FEnvQueryResult> Result,
|
||||||
|
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory,
|
||||||
|
TWeakObjectPtr<AActor> WeakTarget)
|
||||||
|
{
|
||||||
|
if (!OwnerComp || !NodeMemory) return;
|
||||||
|
|
||||||
|
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||||
|
Memory->bEQSQueryRunning = false;
|
||||||
|
|
||||||
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp->GetAIOwner());
|
||||||
|
if (!AIC || !AIC->GetPawn()) return;
|
||||||
|
|
||||||
|
AActor* Target = WeakTarget.Get();
|
||||||
|
|
||||||
|
if (Result.IsValid() && Result->IsSuccessful())
|
||||||
|
{
|
||||||
|
const FVector FiringPos = Result->GetItemAsLocation(0);
|
||||||
|
AIC->MoveToLocation(
|
||||||
|
FiringPos, 50.0f, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/false, /*bCanStrafe=*/false);
|
||||||
|
Memory->bSeekingFiringPos = true;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: EQS found firing position %s"),
|
||||||
|
*AIC->GetName(), *FiringPos.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// EQS found nothing — fallback: advance toward target
|
||||||
|
if (Target)
|
||||||
|
{
|
||||||
|
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||||
|
AIC->MoveToLocation(
|
||||||
|
Target->GetActorLocation(), MidRange, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||||
|
Memory->bSeekingFiringPos = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: EQS no result, fallback advance"),
|
||||||
|
*AIC->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
||||||
{
|
{
|
||||||
return FString::Printf(TEXT("Range-aware attack.\nFallback radius: %.0fcm\nReposition cooldown: %.1fs"),
|
return FString::Printf(TEXT("LOS-aware attack.\nFallback radius: %.0fcm\nLOS check: every %.1fs\nEQS query: %s"),
|
||||||
AttackMoveRadius, RepositionCooldown);
|
AttackMoveRadius, LOSCheckInterval,
|
||||||
|
FiringPositionQuery ? *FiringPositionQuery->GetName() : TEXT("None (fallback)"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,10 @@
|
|||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
|
#include "PS_AI_Behavior_Statics.h"
|
||||||
#include "BehaviorTree/BlackboardComponent.h"
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
#include "Navigation/PathFollowingComponent.h"
|
#include "Navigation/PathFollowingComponent.h"
|
||||||
|
#include "CollisionQueryParams.h"
|
||||||
#include "EngineUtils.h"
|
#include "EngineUtils.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_BTTask_CoverShootCycle::UPS_AI_Behavior_BTTask_CoverShootCycle()
|
UPS_AI_Behavior_BTTask_CoverShootCycle::UPS_AI_Behavior_BTTask_CoverShootCycle()
|
||||||
@ -137,6 +139,8 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
|||||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
if (!Target)
|
if (!Target)
|
||||||
{
|
{
|
||||||
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -149,8 +153,9 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
|||||||
{
|
{
|
||||||
AIC->StopMovement();
|
AIC->StopMovement();
|
||||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
// De-escalate so decorator can re-trigger when threat returns
|
||||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||||
|
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +190,68 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
|||||||
Memory->Timer -= DeltaSeconds;
|
Memory->Timer -= DeltaSeconds;
|
||||||
if (Memory->Timer <= 0.0f)
|
if (Memory->Timer <= 0.0f)
|
||||||
{
|
{
|
||||||
// Timer expired → peek and shoot
|
// LOS check before peeking — no point shooting into a wall
|
||||||
|
const bool bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||||
|
Pawn->GetWorld(), Pawn, Target, 150.0f);
|
||||||
|
|
||||||
|
if (!bHasLOS)
|
||||||
|
{
|
||||||
|
// No LOS → skip Peeking, force Advancing immediately to find a better position
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] CoverShootCycle: no LOS to target from cover, skipping peek → advancing"),
|
||||||
|
*AIC->GetName());
|
||||||
|
|
||||||
|
Memory->SubState = EPS_AI_Behavior_CombatSubState::Advancing;
|
||||||
|
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||||
|
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Advancing));
|
||||||
|
|
||||||
|
// Release current cover point
|
||||||
|
APS_AI_Behavior_CoverPoint* OldPoint =
|
||||||
|
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||||
|
if (OldPoint)
|
||||||
|
{
|
||||||
|
OldPoint->Release(Pawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a cover with better firing angle
|
||||||
|
const FVector NpcLoc = Pawn->GetActorLocation();
|
||||||
|
const FVector ThreatLoc = Target->GetActorLocation();
|
||||||
|
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||||
|
if (UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||||
|
{
|
||||||
|
NPCType = Personality->GetNPCType();
|
||||||
|
}
|
||||||
|
|
||||||
|
float NewScore = -1.0f;
|
||||||
|
APS_AI_Behavior_CoverPoint* NewPoint =
|
||||||
|
FindAdvancingCover(GetWorld(), NpcLoc, ThreatLoc, NPCType, NewScore);
|
||||||
|
|
||||||
|
if (NewPoint)
|
||||||
|
{
|
||||||
|
NewPoint->Claim(Pawn);
|
||||||
|
BB->SetValueAsObject(PS_AI_Behavior_BB::CoverPoint, NewPoint);
|
||||||
|
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, NewPoint->GetActorLocation());
|
||||||
|
|
||||||
|
AIC->MoveToLocation(
|
||||||
|
NewPoint->GetActorLocation(), 80.0f, /*bStopOnOverlap=*/true,
|
||||||
|
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||||
|
Memory->bMoveRequested = true;
|
||||||
|
Memory->CycleCount = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No better cover → stay, retry next cycle
|
||||||
|
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||||
|
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||||
|
Memory->Timer = Memory->PhaseDuration;
|
||||||
|
Memory->CycleCount = 0;
|
||||||
|
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||||
|
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has LOS → peek and shoot normally
|
||||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
||||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
||||||
Memory->Timer = Memory->PhaseDuration;
|
Memory->Timer = Memory->PhaseDuration;
|
||||||
@ -412,6 +478,18 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_CoverShootCycle::FindAdvancin
|
|||||||
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOS bonus — strongly favor covers with clear line of sight to the target
|
||||||
|
{
|
||||||
|
const FVector TraceStart = Point->GetActorLocation() + FVector(0, 0, 150.0f);
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(CoverLOS), true);
|
||||||
|
if (!const_cast<UWorld*>(World)->LineTraceSingleByChannel(
|
||||||
|
Hit, TraceStart, ThreatLoc, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
Score += 0.3f; // Clear LOS from this cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Score > OutScore)
|
if (Score > OutScore)
|
||||||
{
|
{
|
||||||
OutScore = Score;
|
OutScore = Score;
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Int.h"
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_Int.h"
|
||||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
|
||||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
|
||||||
|
#include "BehaviorTree/Blackboard/BlackboardKeyType_String.h"
|
||||||
|
|
||||||
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
||||||
{
|
{
|
||||||
@ -171,6 +172,18 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
|
|||||||
CombatSubStateEntry.EntryName = PS_AI_Behavior_BB::CombatSubState;
|
CombatSubStateEntry.EntryName = PS_AI_Behavior_BB::CombatSubState;
|
||||||
CombatSubStateEntry.KeyType = NewObject<UBlackboardKeyType_Enum>(BlackboardAsset);
|
CombatSubStateEntry.KeyType = NewObject<UBlackboardKeyType_Enum>(BlackboardAsset);
|
||||||
BlackboardAsset->Keys.Add(CombatSubStateEntry);
|
BlackboardAsset->Keys.Add(CombatSubStateEntry);
|
||||||
|
|
||||||
|
// LastKnownTargetPosition (vector — for LOS investigation)
|
||||||
|
FBlackboardEntry LastKnownEntry;
|
||||||
|
LastKnownEntry.EntryName = PS_AI_Behavior_BB::LastKnownTargetPosition;
|
||||||
|
LastKnownEntry.KeyType = NewObject<UBlackboardKeyType_Vector>(BlackboardAsset);
|
||||||
|
BlackboardAsset->Keys.Add(LastKnownEntry);
|
||||||
|
|
||||||
|
// ThreatPawnName (debug: name of the Pawn behind ThreatActor)
|
||||||
|
FBlackboardEntry ThreatPawnNameEntry;
|
||||||
|
ThreatPawnNameEntry.EntryName = PS_AI_Behavior_BB::ThreatPawnName;
|
||||||
|
ThreatPawnNameEntry.KeyType = NewObject<UBlackboardKeyType_String>(BlackboardAsset);
|
||||||
|
BlackboardAsset->Keys.Add(ThreatPawnNameEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
UBlackboardComponent* RawBBComp = nullptr;
|
UBlackboardComponent* RawBBComp = nullptr;
|
||||||
@ -306,8 +319,6 @@ void APS_AI_Behavior_AIController::SetGenericTeamId(const FGenericTeamId& InTeam
|
|||||||
|
|
||||||
ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const AActor& Other) const
|
ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const AActor& Other) const
|
||||||
{
|
{
|
||||||
const uint8 OtherTeamId = FGenericTeamId::NoTeam;
|
|
||||||
|
|
||||||
// Try to get the other actor's team ID
|
// Try to get the other actor's team ID
|
||||||
const APawn* OtherPawn = Cast<APawn>(&Other);
|
const APawn* OtherPawn = Cast<APawn>(&Other);
|
||||||
if (!OtherPawn)
|
if (!OtherPawn)
|
||||||
@ -336,11 +347,38 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
|
|||||||
OtherTeam = TeamComp->GetGenericTeamId().GetId();
|
OtherTeam = TeamComp->GetGenericTeamId().GetId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType
|
||||||
|
if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
const EPS_AI_Behavior_NPCType OtherNPCType =
|
||||||
|
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn));
|
||||||
|
if (OtherNPCType != EPS_AI_Behavior_NPCType::Any)
|
||||||
|
{
|
||||||
|
OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoTeam (255) → Neutral
|
// ─── NoTeam handling ────────────────────────────────────────────────
|
||||||
if (TeamId == FGenericTeamId::NoTeam || OtherTeam == FGenericTeamId::NoTeam)
|
if (TeamId == FGenericTeamId::NoTeam)
|
||||||
{
|
{
|
||||||
|
// We have no team → can't determine attitude
|
||||||
|
return ETeamAttitude::Neutral;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OtherTeam == FGenericTeamId::NoTeam)
|
||||||
|
{
|
||||||
|
// NoTeam = not part of the behavior system (spectators, editor pawns, etc.) → always Neutral
|
||||||
|
static bool bLoggedNoTeamWarning = false;
|
||||||
|
if (!bLoggedNoTeamWarning)
|
||||||
|
{
|
||||||
|
bLoggedNoTeamWarning = true;
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("GetTeamAttitudeTowards: '%s' has NoTeam (no AIController team, no TeamComponent, no Interface). "
|
||||||
|
"Add UPS_AI_Behavior_TeamComponent to fix team detection. Treating as Neutral."),
|
||||||
|
OtherPawn ? *OtherPawn->GetName() : *Other.GetName());
|
||||||
|
}
|
||||||
return ETeamAttitude::Neutral;
|
return ETeamAttitude::Neutral;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,9 @@
|
|||||||
#include "Perception/AISense_Hearing.h"
|
#include "Perception/AISense_Hearing.h"
|
||||||
#include "Perception/AISense_Damage.h"
|
#include "Perception/AISense_Damage.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/SpectatorPawn.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
|
#include "BehaviorTree/BlackboardComponent.h"
|
||||||
|
|
||||||
UPS_AI_Behavior_PerceptionComponent::UPS_AI_Behavior_PerceptionComponent()
|
UPS_AI_Behavior_PerceptionComponent::UPS_AI_Behavior_PerceptionComponent()
|
||||||
{
|
{
|
||||||
@ -83,7 +85,7 @@ void UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated(const TArray<A
|
|||||||
* Returns the Pawn itself — used for team/attitude checks.
|
* Returns the Pawn itself — used for team/attitude checks.
|
||||||
* Returns nullptr if no Pawn can be found.
|
* Returns nullptr if no Pawn can be found.
|
||||||
*/
|
*/
|
||||||
static APawn* FindOwningPawn(AActor* Actor)
|
APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
|
||||||
{
|
{
|
||||||
if (!Actor) return nullptr;
|
if (!Actor) return nullptr;
|
||||||
|
|
||||||
@ -318,6 +320,9 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
if (OwningPawn == Owner) continue;
|
if (OwningPawn == Owner) continue;
|
||||||
if (AIC && OwningPawn == AIC->GetPawn()) continue;
|
if (AIC && OwningPawn == AIC->GetPawn()) continue;
|
||||||
|
|
||||||
|
// Skip spectator pawns (editor camera, spectating players, etc.)
|
||||||
|
if (OwningPawn->IsA<ASpectatorPawn>()) continue;
|
||||||
|
|
||||||
// Get the threat target (PS_AimTargetActor or Pawn itself — for BB targeting)
|
// Get the threat target (PS_AimTargetActor or Pawn itself — for BB targeting)
|
||||||
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
||||||
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
||||||
@ -338,7 +343,7 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
// Allied teams (Civilian ↔ Protector) → allow gunfire through
|
// Allied teams (Civilian ↔ Protector) → allow gunfire through
|
||||||
if (AIC->GetGenericTeamId().GetId() == GetActorTeamId(OwningPawn))
|
if (AIC->GetGenericTeamId().GetId() == GetActorTeamId(OwningPawn))
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Target '%s': SKIPPED (same team 0x%02X)"),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Target '%s': SKIPPED (same team 0x%02X)"),
|
||||||
*Owner->GetName(), *OwningPawn->GetName(), GetActorTeamId(OwningPawn));
|
*Owner->GetName(), *OwningPawn->GetName(), GetActorTeamId(OwningPawn));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -361,7 +366,7 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
|
|
||||||
if (!bActorHasGunshot)
|
if (!bActorHasGunshot)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Target '%s': SKIPPED (attitude=%d, not hostile, no gunshot, theirTeam=0x%02X, myTeam=0x%02X)"),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Target '%s': SKIPPED (attitude=%d, not hostile, no gunshot, theirTeam=0x%02X, myTeam=0x%02X)"),
|
||||||
*Owner->GetName(), *OwningPawn->GetName(), static_cast<int32>(Attitude),
|
*Owner->GetName(), *OwningPawn->GetName(), static_cast<int32>(Attitude),
|
||||||
GetActorTeamId(OwningPawn), AIC->GetGenericTeamId().GetId());
|
GetActorTeamId(OwningPawn), AIC->GetGenericTeamId().GetId());
|
||||||
continue; // Not hostile, no gunshot — skip
|
continue; // Not hostile, no gunshot — skip
|
||||||
@ -441,24 +446,27 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
float CurrentTargetScore = -1.0f;
|
float CurrentTargetScore = -1.0f;
|
||||||
if (AIC)
|
if (AIC)
|
||||||
{
|
{
|
||||||
if (UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
if (const UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||||
{
|
{
|
||||||
CurrentTarget = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
CurrentTarget = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
APawn* BestOwningPawn = nullptr;
|
||||||
|
|
||||||
for (const auto& Pair : ScoreMap)
|
for (const auto& Pair : ScoreMap)
|
||||||
{
|
{
|
||||||
const FActorScore& Entry = Pair.Value;
|
const FActorScore& Entry = Pair.Value;
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Target '%s': type=%d, hostile=%d, score=%.0f"),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Target '%s' (pawn='%s'): type=%d, hostile=%d, score=%.0f"),
|
||||||
*Owner->GetName(), *Entry.Actor->GetName(),
|
*Owner->GetName(), *Entry.Actor->GetName(), *Pair.Key->GetName(),
|
||||||
static_cast<int32>(Entry.ActorType), Entry.bIsHostile ? 1 : 0, Entry.Score);
|
static_cast<int32>(Entry.ActorType), Entry.bIsHostile ? 1 : 0, Entry.Score);
|
||||||
|
|
||||||
if (Entry.Score > BestScore)
|
if (Entry.Score > BestScore)
|
||||||
{
|
{
|
||||||
BestScore = Entry.Score;
|
BestScore = Entry.Score;
|
||||||
BestThreat = Entry.Actor;
|
BestThreat = Entry.Actor;
|
||||||
|
BestOwningPawn = Cast<APawn>(Pair.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track current target's score for persistence check
|
// Track current target's score for persistence check
|
||||||
@ -476,15 +484,32 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
|||||||
{
|
{
|
||||||
BestThreat = CurrentTarget;
|
BestThreat = CurrentTarget;
|
||||||
BestScore = CurrentTargetScore;
|
BestScore = CurrentTargetScore;
|
||||||
|
// Find the owning Pawn for the kept target
|
||||||
|
for (const auto& Pair : ScoreMap)
|
||||||
|
{
|
||||||
|
if (Pair.Value.Actor == CurrentTarget) { BestOwningPawn = Cast<APawn>(Pair.Key); break; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the owning Pawn for callers (UpdateThreat reads this for debug display)
|
||||||
|
LastThreatOwningPawn = BestOwningPawn;
|
||||||
|
|
||||||
if (BestThreat)
|
if (BestThreat)
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] → Best target: '%s' (score=%.0f%s)"),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] → Best target: '%s' (pawn='%s', score=%.0f%s)"),
|
||||||
*Owner->GetName(), *BestThreat->GetName(), BestScore,
|
*Owner->GetName(), *BestThreat->GetName(),
|
||||||
|
BestOwningPawn ? *BestOwningPawn->GetName() : TEXT("?"),
|
||||||
|
BestScore,
|
||||||
(BestThreat == CurrentTarget) ? TEXT(" [kept]") : TEXT(""));
|
(BestThreat == CurrentTarget) ? TEXT(" [kept]") : TEXT(""));
|
||||||
}
|
}
|
||||||
|
else if (PerceivedActors.Num() > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
|
TEXT("[%s] GetHighestThreatActor: perceived %d actors but ALL were filtered out (myTeam=0x%02X). Check TeamIds and attitude."),
|
||||||
|
*Owner->GetName(), PerceivedActors.Num(),
|
||||||
|
AIC ? AIC->GetGenericTeamId().GetId() : 255);
|
||||||
|
}
|
||||||
|
|
||||||
return BestThreat;
|
return BestThreat;
|
||||||
}
|
}
|
||||||
@ -512,6 +537,9 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
|||||||
APawn* OwningPawn = FindOwningPawn(RawActor);
|
APawn* OwningPawn = FindOwningPawn(RawActor);
|
||||||
if (!OwningPawn) continue;
|
if (!OwningPawn) continue;
|
||||||
|
|
||||||
|
// Skip spectator pawns
|
||||||
|
if (OwningPawn->IsA<ASpectatorPawn>()) continue;
|
||||||
|
|
||||||
// Get the threat target for position-based calculations
|
// Get the threat target for position-based calculations
|
||||||
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
||||||
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
||||||
|
|||||||
@ -88,6 +88,12 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
const float EffectiveFleeThresh = FleeThresh * (0.5f + Courage * 0.5f) * (1.5f - Caution * 0.5f);
|
const float EffectiveFleeThresh = FleeThresh * (0.5f + Courage * 0.5f) * (1.5f - Caution * 0.5f);
|
||||||
const float EffectiveAttackThresh = AttackThresh * (1.5f - Aggressivity * 0.5f);
|
const float EffectiveAttackThresh = AttackThresh * (1.5f - Aggressivity * 0.5f);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] EvaluateReaction: threat=%.2f, aggr=%.2f, courage=%.2f, caution=%.2f | attackThresh=%.2f, fleeThresh=%.2f, alertThresh=%.2f"),
|
||||||
|
GetOwner() ? *GetOwner()->GetName() : TEXT("?"),
|
||||||
|
PerceivedThreatLevel, Aggressivity, Courage, Caution,
|
||||||
|
EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh);
|
||||||
|
|
||||||
// Decision cascade
|
// Decision cascade
|
||||||
if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
||||||
{
|
{
|
||||||
@ -231,10 +237,15 @@ void UPS_AI_Behavior_PersonalityComponent::DrawDebugInfo() const
|
|||||||
{
|
{
|
||||||
DebugTeamId = AIC->GetGenericTeamId().GetId();
|
DebugTeamId = AIC->GetGenericTeamId().GetId();
|
||||||
|
|
||||||
// Get ThreatActor from Blackboard
|
// Get threat target name from Blackboard (prefer Pawn name for readability)
|
||||||
if (const UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
if (const UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||||
{
|
{
|
||||||
if (AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)))
|
const FString PawnName = BB->GetValueAsString(PS_AI_Behavior_BB::ThreatPawnName);
|
||||||
|
if (!PawnName.IsEmpty())
|
||||||
|
{
|
||||||
|
ThreatActorName = PawnName;
|
||||||
|
}
|
||||||
|
else if (AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)))
|
||||||
{
|
{
|
||||||
ThreatActorName = ThreatActor->GetName();
|
ThreatActorName = ThreatActor->GetName();
|
||||||
}
|
}
|
||||||
@ -274,11 +285,15 @@ void UPS_AI_Behavior_PersonalityComponent::DrawDebugInfo() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Build text ─────────────────────────────────────────────────────
|
// ─── Build text ─────────────────────────────────────────────────────
|
||||||
|
// Decode faction from TeamId
|
||||||
|
const uint8 DebugFaction = PS_AI_Behavior_Team::GetFaction(DebugTeamId);
|
||||||
|
const FString FactionStr = DebugFaction > 0 ? FString::Printf(TEXT(" F%d"), DebugFaction) : TEXT("");
|
||||||
|
|
||||||
const FString DebugText = FString::Printf(
|
const FString DebugText = FString::Printf(
|
||||||
TEXT("%s [%s] Team:%d %s\n%s Threat:%.2f → %s%s"),
|
TEXT("%s [%s%s] %s\n%s Threat:%.2f → %s%s"),
|
||||||
*NPCName,
|
*NPCName,
|
||||||
*UEnum::GetDisplayValueAsText(NPCType).ToString(),
|
*UEnum::GetDisplayValueAsText(NPCType).ToString(),
|
||||||
DebugTeamId,
|
*FactionStr,
|
||||||
bHostile ? TEXT("HOSTILE") : TEXT(""),
|
bHostile ? TEXT("HOSTILE") : TEXT(""),
|
||||||
*UEnum::GetDisplayValueAsText(CurrentState).ToString(),
|
*UEnum::GetDisplayValueAsText(CurrentState).ToString(),
|
||||||
ThreatLevel,
|
ThreatLevel,
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "PS_AI_Behavior_Statics.h"
|
||||||
|
#include "Perception/AISense_Hearing.h"
|
||||||
|
#include "CollisionQueryParams.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_Statics::ReportGunfire(UObject* WorldContext, FVector Location,
|
||||||
|
AActor* Shooter, bool bIsEnemyFire, float Loudness, float MaxRange)
|
||||||
|
{
|
||||||
|
UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::LogAndReturnNull) : nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("ReportGunfire: invalid WorldContext."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FName Tag = bIsEnemyFire ? PS_AI_Behavior_Tags::EnemyFire : PS_AI_Behavior_Tags::PlayerFire;
|
||||||
|
|
||||||
|
UAISense_Hearing::ReportNoiseEvent(
|
||||||
|
World,
|
||||||
|
Location,
|
||||||
|
Loudness,
|
||||||
|
Shooter,
|
||||||
|
MaxRange,
|
||||||
|
Tag);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("ReportGunfire: at %s by '%s' (tag=%s, loudness=%.1f)"),
|
||||||
|
*Location.ToString(),
|
||||||
|
Shooter ? *Shooter->GetName() : TEXT("null"),
|
||||||
|
*Tag.ToString(),
|
||||||
|
Loudness);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UPS_AI_Behavior_Statics::HasLineOfSight(const UWorld* World, const AActor* Source,
|
||||||
|
const AActor* Target, float EyeHeightOffset)
|
||||||
|
{
|
||||||
|
if (!World || !Source || !Target) return false;
|
||||||
|
|
||||||
|
const FVector TraceStart = Source->GetActorLocation() + FVector(0, 0, EyeHeightOffset);
|
||||||
|
const FVector TraceEnd = Target->GetActorLocation();
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(BehaviorLOS), true);
|
||||||
|
Params.AddIgnoredActor(Source);
|
||||||
|
Params.AddIgnoredActor(Target);
|
||||||
|
|
||||||
|
// Also ignore actors up the attachment chain (e.g. AimTarget → ChildActor → Character capsule)
|
||||||
|
// This handles cases where Owner/Instigator aren't set but the actor is physically attached.
|
||||||
|
for (const AActor* Cur = Target->GetAttachParentActor(); Cur; Cur = Cur->GetAttachParentActor())
|
||||||
|
{
|
||||||
|
Params.AddIgnoredActor(Cur);
|
||||||
|
}
|
||||||
|
for (const AActor* Cur = Source->GetAttachParentActor(); Cur; Cur = Cur->GetAttachParentActor())
|
||||||
|
{
|
||||||
|
Params.AddIgnoredActor(Cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params);
|
||||||
|
}
|
||||||
@ -22,7 +22,26 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTService_UpdateThreat : public UBTServ
|
|||||||
public:
|
public:
|
||||||
UPS_AI_Behavior_BTService_UpdateThreat();
|
UPS_AI_Behavior_BTService_UpdateThreat();
|
||||||
|
|
||||||
|
/** Seconds without LOS before writing LastKnownTargetPosition and starting investigation. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Threat|LOS", meta = (ClampMin = "5.0"))
|
||||||
|
float LOSLostTimeout = 20.0f;
|
||||||
|
|
||||||
|
/** Eye height offset for LOS traces (cm above pawn origin). */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Threat|LOS", meta = (ClampMin = "50.0", ClampMax = "200.0"))
|
||||||
|
float EyeHeightOffset = 150.0f;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||||
virtual FString GetStaticDescription() const override;
|
virtual FString GetStaticDescription() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FUpdateThreatMemory
|
||||||
|
{
|
||||||
|
float TimeSinceLOS = 0.0f;
|
||||||
|
FVector LastVisiblePosition = FVector::ZeroVector;
|
||||||
|
bool bHadLOS = true;
|
||||||
|
bool bInvestigating = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FUpdateThreatMemory); }
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "BehaviorTree/BTTaskNode.h"
|
#include "BehaviorTree/BTTaskNode.h"
|
||||||
|
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
|
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
|
||||||
|
|
||||||
|
class UEnvQuery;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
||||||
*
|
*
|
||||||
@ -37,6 +40,22 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "0.5", ClampMax = "5.0"))
|
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "0.5", ClampMax = "5.0"))
|
||||||
float RepositionCooldown = 1.5f;
|
float RepositionCooldown = 1.5f;
|
||||||
|
|
||||||
|
/** Eye height offset for line-of-sight traces (cm above actor origin). */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Attack|LOS", meta = (ClampMin = "50.0", ClampMax = "200.0"))
|
||||||
|
float EyeHeightOffset = 150.0f;
|
||||||
|
|
||||||
|
/** How often to check LOS (seconds). Avoids per-frame traces. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Attack|LOS", meta = (ClampMin = "0.1", ClampMax = "1.0"))
|
||||||
|
float LOSCheckInterval = 0.2f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EQS query asset for finding a firing position with clear LOS.
|
||||||
|
* Compose in editor: OnCircle generator + LineOfSight filter + Distance tests.
|
||||||
|
* If null, fallback: advance directly toward the target.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Attack|LOS")
|
||||||
|
TObjectPtr<UEnvQuery> FiringPositionQuery;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||||
@ -50,11 +69,24 @@ private:
|
|||||||
bool bMovingToTarget = false;
|
bool bMovingToTarget = false;
|
||||||
bool bAttacking = false; // true when BehaviorStartAttack is active
|
bool bAttacking = false; // true when BehaviorStartAttack is active
|
||||||
bool bInRange = false; // true when within MaxRange of target
|
bool bInRange = false; // true when within MaxRange of target
|
||||||
|
bool bHasLOS = true; // true when line-of-sight to target is clear
|
||||||
|
bool bSeekingFiringPos = false; // true when moving to a flanking/firing position
|
||||||
EPS_AI_Behavior_CombatType CombatType = EPS_AI_Behavior_CombatType::Melee;
|
EPS_AI_Behavior_CombatType CombatType = EPS_AI_Behavior_CombatType::Melee;
|
||||||
float MinRange = 100.0f; // backs away if closer
|
float MinRange = 100.0f; // backs away if closer
|
||||||
float MaxRange = 300.0f; // advances if farther
|
float MaxRange = 300.0f; // advances if farther
|
||||||
float RepositionTimer = 0.0f;
|
float RepositionTimer = 0.0f;
|
||||||
|
float LOSCheckTimer = 0.0f; // cooldown for LOS trace checks
|
||||||
|
bool bEQSQueryRunning = false; // true while an EQS query is in flight
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
||||||
|
|
||||||
|
/** Run the EQS firing position query. Falls back to advancing toward target if no query asset. */
|
||||||
|
void RunFiringPositionQuery(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||||
|
APawn* Pawn, AActor* Target);
|
||||||
|
|
||||||
|
/** Callback when the EQS query completes. */
|
||||||
|
void OnFiringPositionQueryFinished(TSharedPtr<FEnvQueryResult> Result,
|
||||||
|
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory,
|
||||||
|
TWeakObjectPtr<AActor> WeakTarget);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -101,6 +101,9 @@ protected:
|
|||||||
UPROPERTY(Transient)
|
UPROPERTY(Transient)
|
||||||
TObjectPtr<UPS_AI_Behavior_PersonalityComponent> PersonalityComp;
|
TObjectPtr<UPS_AI_Behavior_PersonalityComponent> PersonalityComp;
|
||||||
|
|
||||||
|
/** Shut down all AI systems when the NPC dies. Called automatically by SetBehaviorState(Dead). */
|
||||||
|
void HandleDeath();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/** Initialize Blackboard with required keys. */
|
/** Initialize Blackboard with required keys. */
|
||||||
void SetupBlackboard();
|
void SetupBlackboard();
|
||||||
|
|||||||
@ -184,5 +184,7 @@ namespace PS_AI_Behavior_BB
|
|||||||
inline const FName HomeLocation = TEXT("HomeLocation");
|
inline const FName HomeLocation = TEXT("HomeLocation");
|
||||||
inline const FName CurrentSpline = TEXT("CurrentSpline");
|
inline const FName CurrentSpline = TEXT("CurrentSpline");
|
||||||
inline const FName SplineProgress = TEXT("SplineProgress");
|
inline const FName SplineProgress = TEXT("SplineProgress");
|
||||||
inline const FName CombatSubState = TEXT("CombatSubState");
|
inline const FName CombatSubState = TEXT("CombatSubState");
|
||||||
|
inline const FName LastKnownTargetPosition = TEXT("LastKnownTargetPosition");
|
||||||
|
inline const FName ThreatPawnName = TEXT("ThreatPawnName"); // Debug: name of the owning Pawn behind ThreatActor
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,6 +64,13 @@ public:
|
|||||||
/** Extract an actor's TeamId (checking controller, then TeamComponent). Returns NoTeam if unresolvable. */
|
/** Extract an actor's TeamId (checking controller, then TeamComponent). Returns NoTeam if unresolvable. */
|
||||||
static uint8 GetActorTeamId(const AActor* Actor);
|
static uint8 GetActorTeamId(const AActor* Actor);
|
||||||
|
|
||||||
|
/** Walk Owner/Instigator chain to find the Pawn that owns a perceived actor (weapon, AimTarget, etc.). */
|
||||||
|
static APawn* FindOwningPawn(AActor* Actor);
|
||||||
|
|
||||||
|
/** The owning Pawn of the last selected ThreatActor (set by GetHighestThreatActor). */
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TWeakObjectPtr<APawn> LastThreatOwningPawn;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright Asterion. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
|
#include "PS_AI_Behavior_Statics.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helpers for the PS AI Behavior plugin.
|
||||||
|
* Call from Blueprint or C++ to interact with the behavior system.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_Statics : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a gunfire noise event so all nearby NPCs react.
|
||||||
|
* Uses tag "EnemyFire" or "PlayerFire" depending on bIsEnemyFire.
|
||||||
|
*
|
||||||
|
* Compatible with existing ReportNoiseEvent calls using those tags.
|
||||||
|
*
|
||||||
|
* - Enemies (non-hostile) hearing this will become hostile toward the shooter.
|
||||||
|
* - Civilians hearing this will flee based on their personality traits.
|
||||||
|
* - Already-hostile enemies treat this as additional threat.
|
||||||
|
*
|
||||||
|
* @param WorldContext Any world-context object (self, weapon, etc.)
|
||||||
|
* @param Location World location of the gunshot.
|
||||||
|
* @param Shooter The actor who fired (Instigator for perception).
|
||||||
|
* @param bIsEnemyFire True = tag "EnemyFire", False = tag "PlayerFire".
|
||||||
|
* @param Loudness Noise loudness (default 1.0). Scales with HearingRange.
|
||||||
|
* @param MaxRange Override max hearing range (0 = use sense default).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception",
|
||||||
|
meta = (WorldContext = "WorldContext", DefaultToSelf = "Shooter"))
|
||||||
|
static void ReportGunfire(UObject* WorldContext, FVector Location,
|
||||||
|
AActor* Shooter, bool bIsEnemyFire = false,
|
||||||
|
float Loudness = 1.0f, float MaxRange = 0.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is a clear line-of-sight between two actors.
|
||||||
|
* Traces from the source actor's eye height to the target actor's location.
|
||||||
|
*
|
||||||
|
* @param World The world to trace in.
|
||||||
|
* @param Source The actor looking (trace starts at Source + EyeHeightOffset).
|
||||||
|
* @param Target The actor being looked at (trace ends at Target location).
|
||||||
|
* @param EyeHeightOffset Height offset above Source's origin for the trace start (cm).
|
||||||
|
* @return True if line-of-sight is clear (no blocking geometry).
|
||||||
|
*/
|
||||||
|
static bool HasLineOfSight(const UWorld* World, const AActor* Source, const AActor* Target,
|
||||||
|
float EyeHeightOffset = 150.0f);
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user