Multiple fixes: threat decay per profile, state hysteresis, spline pathfinding, attack readiness
- Move ThreatDecayRate from global Settings to PersonalityProfile (per-archetype) - Add state hysteresis in EvaluateReaction to prevent Fleeing/Combat flickering - FindCover: verify distance on arrival, retry movement if too far - FindAndFollowSpline: sample multiple spline points when closest is blocked - BTTask_Attack: call BehaviorStartAttack immediately (draw weapon before LOS/range) - CoverShootCycle: call BehaviorStartAttack on entry (weapon ready during cover approach) - FindOwningPawn: walk AttachParentActor chain for ChildActor weapons without Owner - Initialize PreferCover BB key to false at setup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5897b46cc
commit
af93194f48
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,6 +4,7 @@
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "PS_AI_Behavior_Statics.h"
|
||||
#include "PS_AI_Behavior_Settings.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
@ -34,6 +35,8 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
||||
return;
|
||||
}
|
||||
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
|
||||
// Debug: check what perception sees
|
||||
TArray<AActor*> PerceivedActors;
|
||||
Perception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
|
||||
@ -60,8 +63,13 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
||||
const float StoredThreat = BB->GetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel);
|
||||
|
||||
// Apply decay when no threat, or take the max of new vs decayed
|
||||
const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
|
||||
const float DecayedThreat = FMath::Max(0.0f, StoredThreat - Settings->ThreatDecayRate * DeltaSeconds);
|
||||
// Decay rate comes from PersonalityProfile (per-archetype), fallback to global Settings
|
||||
float DecayRate = GetDefault<UPS_AI_Behavior_Settings>()->ThreatDecayRate;
|
||||
if (Personality && Personality->Profile)
|
||||
{
|
||||
DecayRate = Personality->Profile->ThreatDecayRate;
|
||||
}
|
||||
const float DecayedThreat = FMath::Max(0.0f, StoredThreat - DecayRate * DeltaSeconds);
|
||||
const float FinalThreat = FMath::Max(RawThreat, DecayedThreat);
|
||||
|
||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
||||
@ -261,7 +269,6 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
||||
}
|
||||
|
||||
// Sync to PersonalityComponent
|
||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||
if (Personality)
|
||||
{
|
||||
Personality->PerceivedThreatLevel = FinalThreat;
|
||||
|
||||
@ -103,17 +103,19 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||
|
||||
// Start attack immediately (draw weapon, enter combat stance)
|
||||
// regardless of range or LOS — the NPC is committed to combat
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
Memory->bAttacking = true;
|
||||
}
|
||||
|
||||
// Check if already in range
|
||||
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
||||
if (DistToTarget <= Memory->MaxRange)
|
||||
{
|
||||
Memory->bInRange = true;
|
||||
// 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);
|
||||
Memory->bAttacking = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial move toward target if not in range
|
||||
|
||||
@ -132,6 +132,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
// Face the threat while in cover
|
||||
AIC->SetFocus(Target);
|
||||
|
||||
// Start attack immediately (draw weapon, enter combat stance)
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
|
||||
@ -148,34 +148,64 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
||||
|
||||
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
||||
{
|
||||
// Walk to spline first via NavMesh
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
SplinePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
||||
/*bCanStrafe=*/false);
|
||||
// Try to reach the closest spline point. If blocked, sample other points along the spline.
|
||||
const float SplineLength = ClosestSpline->GetSplineLength();
|
||||
const int32 NumSamples = 8;
|
||||
const float SampleStep = SplineLength / NumSamples;
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
// Start with the closest point, then try evenly spaced samples
|
||||
TArray<float> DistancesToTry;
|
||||
DistancesToTry.Add(DistAlongSpline);
|
||||
for (int32 i = 1; i <= NumSamples; ++i)
|
||||
{
|
||||
// Can't reach via NavMesh — try starting anyway (snap)
|
||||
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
||||
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
||||
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
||||
return EBTNodeResult::Succeeded;
|
||||
const float D = FMath::Fmod(DistAlongSpline + i * SampleStep, SplineLength);
|
||||
DistancesToTry.Add(D);
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
float ReachableDist = -1.0f;
|
||||
FVector ReachablePoint = FVector::ZeroVector;
|
||||
|
||||
for (float TestDist : DistancesToTry)
|
||||
{
|
||||
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
||||
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
||||
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
||||
return EBTNodeResult::Succeeded;
|
||||
const FVector TestPoint = ClosestSpline->GetWorldLocationAtDistance(TestDist);
|
||||
const EPathFollowingRequestResult::Type TestResult = AIC->MoveToLocation(
|
||||
TestPoint, AcceptanceRadius, true, true, true, false);
|
||||
|
||||
if (TestResult == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
||||
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(TestDist);
|
||||
Follower->StartFollowingAtDistance(ClosestSpline, TestDist,
|
||||
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
|
||||
if (TestResult != EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
ReachableDist = TestDist;
|
||||
ReachablePoint = TestPoint;
|
||||
break; // Found a reachable point, use it
|
||||
}
|
||||
|
||||
// This point is blocked, try next
|
||||
AIC->StopMovement();
|
||||
}
|
||||
|
||||
// Store the spline to connect to after reaching it
|
||||
if (ReachableDist < 0.0f)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] FindAndFollowSpline: no reachable point on spline '%s' (%d samples tested)"),
|
||||
*AIC->GetName(), *ClosestSpline->GetName(), DistancesToTry.Num());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] FindAndFollowSpline: walking to reachable spline point at dist=%.0f (gap=%.0fcm)"),
|
||||
*AIC->GetName(), ReachableDist,
|
||||
FVector::Dist(AIC->GetPawn()->GetActorLocation(), ReachablePoint));
|
||||
|
||||
Follower->CurrentSpline = ClosestSpline;
|
||||
Follower->CurrentDistance = DistAlongSpline;
|
||||
Follower->CurrentDistance = ReachableDist;
|
||||
|
||||
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
||||
Memory->bMovingToSpline = true;
|
||||
|
||||
@ -207,21 +207,51 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask(
|
||||
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Verify we actually reached the cover — pathfinding can stop early (blocked by other NPCs, etc.)
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (BB && Pawn)
|
||||
{
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
const float DistToCover = FVector::Dist2D(Pawn->GetActorLocation(), CoverLoc);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] FindCover: MoveStatus=Idle, dist2D=%.0fcm, acceptance=%.0fcm, coverLoc=%s, pawnLoc=%s"),
|
||||
*AIC->GetName(), DistToCover, AcceptanceRadius,
|
||||
*CoverLoc.ToString(), *Pawn->GetActorLocation().ToString());
|
||||
|
||||
if (DistToCover > AcceptanceRadius * 2.0f)
|
||||
{
|
||||
// Still too far — retry movement
|
||||
const EPathFollowingRequestResult::Type Retry = AIC->MoveToLocation(
|
||||
CoverLoc, AcceptanceRadius, true, true, true, false);
|
||||
|
||||
if (Retry == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
// Can't reach at all — give up
|
||||
Memory->bMoveRequested = false;
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Retry != EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
return; // Keep waiting for retry movement
|
||||
}
|
||||
// AlreadyAtGoal — fall through to success
|
||||
}
|
||||
}
|
||||
|
||||
Memory->bMoveRequested = false;
|
||||
|
||||
// Crouch at cover if the point requires it
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>() && BB)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
const APS_AI_Behavior_CoverPoint* CoverPt =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (CoverPt && CoverPt->bCrouch)
|
||||
{
|
||||
const APS_AI_Behavior_CoverPoint* CoverPt =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (CoverPt && CoverPt->bCrouch)
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true);
|
||||
}
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -204,6 +204,7 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
|
||||
Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
|
||||
static_cast<uint8>(EPS_AI_Behavior_State::Idle));
|
||||
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -96,7 +96,7 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
|
||||
return ActorAsPawn;
|
||||
}
|
||||
|
||||
// Not a Pawn — walk up Owner/Instigator chain to find the owning Pawn
|
||||
// Not a Pawn — walk up Owner/Instigator/Attachment chain to find the owning Pawn
|
||||
AActor* Current = Actor;
|
||||
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
|
||||
{
|
||||
@ -108,14 +108,29 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
|
||||
|
||||
// Try Owner
|
||||
AActor* OwnerActor = Current->GetOwner();
|
||||
if (!OwnerActor || OwnerActor == Current) break;
|
||||
|
||||
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
|
||||
if (OwnerActor && OwnerActor != Current)
|
||||
{
|
||||
return OwnerPawn;
|
||||
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
|
||||
{
|
||||
return OwnerPawn;
|
||||
}
|
||||
Current = OwnerActor;
|
||||
continue;
|
||||
}
|
||||
|
||||
Current = OwnerActor; // Continue up the chain
|
||||
// Try attachment parent (ChildActorComponent → parent Character)
|
||||
AActor* ParentActor = Current->GetAttachParentActor();
|
||||
if (ParentActor && ParentActor != Current)
|
||||
{
|
||||
if (APawn* ParentPawn = Cast<APawn>(ParentActor))
|
||||
{
|
||||
return ParentPawn;
|
||||
}
|
||||
Current = ParentActor;
|
||||
continue;
|
||||
}
|
||||
|
||||
break; // No more chain to walk
|
||||
}
|
||||
|
||||
// Fallback: could not resolve to a Pawn.
|
||||
|
||||
@ -93,13 +93,32 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
PerceivedThreatLevel, Aggressivity, Courage, Caution,
|
||||
EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh);
|
||||
|
||||
// Decision cascade
|
||||
if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
||||
// Decision cascade — with hysteresis to prevent state flickering.
|
||||
// Once in a state, require a larger threshold drop to leave it.
|
||||
const float Hysteresis = 0.15f;
|
||||
|
||||
if (CurrentState == EPS_AI_Behavior_State::Fleeing)
|
||||
{
|
||||
// Stay fleeing until threat drops well below the threshold
|
||||
if (PerceivedThreatLevel >= (EffectiveFleeThresh - Hysteresis))
|
||||
{
|
||||
return EPS_AI_Behavior_State::Fleeing;
|
||||
}
|
||||
}
|
||||
else if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
||||
{
|
||||
return EPS_AI_Behavior_State::Fleeing;
|
||||
}
|
||||
|
||||
if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.0f)
|
||||
if (CurrentState == EPS_AI_Behavior_State::Combat)
|
||||
{
|
||||
// Stay in combat until threat drops well below the threshold
|
||||
if (PerceivedThreatLevel >= (EffectiveAttackThresh - Hysteresis))
|
||||
{
|
||||
return EPS_AI_Behavior_State::Combat;
|
||||
}
|
||||
}
|
||||
else if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.0f)
|
||||
{
|
||||
return EPS_AI_Behavior_State::Combat;
|
||||
}
|
||||
|
||||
@ -79,6 +79,15 @@ public:
|
||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AlertThreshold = 0.15f;
|
||||
|
||||
/**
|
||||
* How fast the perceived threat level decays per second when no active threat is perceived.
|
||||
* Low values = NPC stays scared/alert longer (civilians).
|
||||
* High values = NPC calms down quickly (trained soldiers).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Reaction",
|
||||
meta = (ClampMin = "0.001", ClampMax = "1.0"))
|
||||
float ThreatDecayRate = 0.05f;
|
||||
|
||||
// ─── Target Priority (Combat) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user