Fix perception to separate Pawn (team checks) from ThreatTarget (aiming)
Split ResolveToPawn into FindOwningPawn + GetThreatTarget so non-Pawn actors (PS_AimTargetActor) are properly resolved for team/attitude checks while still being used as BB target. Add attack range hysteresis (10% buffer), target persistence (80% threshold), melee no-cooldown chase, ranged midpoint approach. New files: CoverShootCycle task, CheckCombatType decorator, MinAttackRange/MaxAttackRange in PersonalityProfile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7c3598dce
commit
2588883a1c
@ -0,0 +1,29 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTDecorator_CheckCombatType.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
|
||||
UPS_AI_Behavior_BTDecorator_CheckCombatType::UPS_AI_Behavior_BTDecorator_CheckCombatType()
|
||||
{
|
||||
NodeName = TEXT("Check Combat Type");
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_BTDecorator_CheckCombatType::CalculateRawConditionValue(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC) return false;
|
||||
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (!Pawn || !Pawn->Implements<UPS_AI_Behavior_Interface>()) return false;
|
||||
|
||||
return IPS_AI_Behavior_Interface::Execute_GetBehaviorCombatType(Pawn) == RequiredType;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTDecorator_CheckCombatType::GetStaticDescription() const
|
||||
{
|
||||
const UEnum* TypeEnum = StaticEnum<EPS_AI_Behavior_CombatType>();
|
||||
return FString::Printf(TEXT("Combat Type == %s"),
|
||||
*TypeEnum->GetDisplayNameTextByValue(static_cast<int64>(RequiredType)).ToString());
|
||||
}
|
||||
@ -2,10 +2,13 @@
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTService_EvaluateReaction.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Perception/AISense_Hearing.h"
|
||||
|
||||
UPS_AI_Behavior_BTService_EvaluateReaction::UPS_AI_Behavior_BTService_EvaluateReaction()
|
||||
{
|
||||
@ -35,25 +38,124 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
||||
const bool bHostile = IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(Pawn);
|
||||
const EPS_AI_Behavior_NPCType NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(Pawn);
|
||||
|
||||
// An infiltrated Enemy (hostile=false) has TeamId=1 (civilian disguise).
|
||||
// When hostile flips to true, switch to TeamId=2 (enemy).
|
||||
// Get faction from profile
|
||||
const uint8 Faction = (Personality && Personality->Profile)
|
||||
? Personality->Profile->Faction : 0;
|
||||
|
||||
// Infiltrated Enemy (hostile=false) → disguised as Civilian.
|
||||
// When hostile flips to true → reveal true Enemy TeamId.
|
||||
uint8 ExpectedTeamId;
|
||||
switch (NPCType)
|
||||
if (NPCType == EPS_AI_Behavior_NPCType::Enemy && !bHostile)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: ExpectedTeamId = 1; break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: ExpectedTeamId = bHostile ? 2 : 1; break;
|
||||
case EPS_AI_Behavior_NPCType::Protector: ExpectedTeamId = 3; break;
|
||||
default: ExpectedTeamId = FGenericTeamId::NoTeam; break;
|
||||
ExpectedTeamId = PS_AI_Behavior_Team::DisguisedTeamId;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExpectedTeamId = PS_AI_Behavior_Team::MakeTeamId(NPCType, Faction);
|
||||
}
|
||||
|
||||
if (AIC->GetGenericTeamId().GetId() != ExpectedTeamId)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Hostility changed: TeamId %d -> %d (hostile=%d)"),
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Hostility changed: TeamId 0x%02X -> 0x%02X (hostile=%d)"),
|
||||
*AIC->GetName(), AIC->GetGenericTeamId().GetId(), ExpectedTeamId, (int32)bHostile);
|
||||
AIC->SetTeamId(ExpectedTeamId);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gunshot reaction: flip non-hostile enemies to hostile ──────────
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
const EPS_AI_Behavior_NPCType NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(Pawn);
|
||||
const bool bHostile = IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(Pawn);
|
||||
|
||||
if (NPCType == EPS_AI_Behavior_NPCType::Enemy && !bHostile)
|
||||
{
|
||||
// Check if this enemy perceives a gunshot stimulus → become hostile toward shooter
|
||||
if (UPS_AI_Behavior_PerceptionComponent* Perception = AIC->GetBehaviorPerception())
|
||||
{
|
||||
AActor* GunShotInstigator = nullptr;
|
||||
TArray<AActor*> PerceivedActors;
|
||||
Perception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
|
||||
|
||||
for (AActor* RawActor : PerceivedActors)
|
||||
{
|
||||
if (!RawActor || GunShotInstigator) continue;
|
||||
|
||||
// Resolve weapon/item to owning Pawn (walk Owner/Instigator chain)
|
||||
AActor* ResolvedActor = RawActor;
|
||||
if (!Cast<APawn>(RawActor))
|
||||
{
|
||||
AActor* Cur = RawActor;
|
||||
for (int32 D = 0; D < 4; ++D)
|
||||
{
|
||||
if (APawn* IP = Cur->GetInstigator()) { ResolvedActor = IP; break; }
|
||||
AActor* OA = Cur->GetOwner();
|
||||
if (!OA || OA == Cur) break;
|
||||
if (APawn* OP = Cast<APawn>(OA)) { ResolvedActor = OP; break; }
|
||||
Cur = OA;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip same exact team (same NPCType + same Faction)
|
||||
// Allied teams still allow gunfire through (e.g. disguised enemy hears Protector fire)
|
||||
const uint8 MyTeam = AIC->GetGenericTeamId().GetId();
|
||||
const uint8 TheirTeam = UPS_AI_Behavior_PerceptionComponent::GetActorTeamId(ResolvedActor);
|
||||
if (MyTeam == TheirTeam)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (Perception->GetActorsPerception(RawActor, Info))
|
||||
{
|
||||
for (const FAIStimulus& S : Info.LastSensedStimuli)
|
||||
{
|
||||
if (S.IsValid() &&
|
||||
S.Type == UAISense::GetSenseID<UAISense_Hearing>() &&
|
||||
PS_AI_Behavior_Tags_Internal::IsGunfire(S.Tag))
|
||||
{
|
||||
// For VR: check if Pawn has a custom threat actor
|
||||
if (ResolvedActor->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
AActor* ThreatActor = IPS_AI_Behavior_Interface::Execute_GetBehaviorThreatActor(ResolvedActor);
|
||||
if (ThreatActor) ResolvedActor = ThreatActor;
|
||||
}
|
||||
GunShotInstigator = ResolvedActor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GunShotInstigator)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] Gunshot heard from '%s' — becoming hostile!"),
|
||||
*AIC->GetName(), *GunShotInstigator->GetName());
|
||||
|
||||
// 1. Flip hostile → TeamId will update on next block above
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorHostile(Pawn, true);
|
||||
|
||||
// 2. Immediately update TeamId so perception sees the shooter as hostile NOW
|
||||
const uint8 EnemyFaction = (Personality && Personality->Profile)
|
||||
? Personality->Profile->Faction : 0;
|
||||
AIC->SetTeamId(PS_AI_Behavior_Team::MakeTeamId(EPS_AI_Behavior_NPCType::Enemy, EnemyFaction));
|
||||
|
||||
// 3. Write the shooter as ThreatActor in BB so combat targets them directly
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, GunShotInstigator);
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, GunShotInstigator->GetActorLocation());
|
||||
|
||||
// 4. Set a meaningful threat level so EvaluateReaction enters Combat
|
||||
if (Personality)
|
||||
{
|
||||
Personality->PerceivedThreatLevel = FMath::Max(Personality->PerceivedThreatLevel, 0.6f);
|
||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, Personality->PerceivedThreatLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Evaluate and apply the reaction ────────────────────────────────
|
||||
EPS_AI_Behavior_State NewState = Personality->ApplyReaction();
|
||||
|
||||
|
||||
@ -4,8 +4,11 @@
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
#include "NavigationSystem.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_Attack::UPS_AI_Behavior_BTTask_Attack()
|
||||
{
|
||||
@ -42,28 +45,68 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Query combat type and range ─────────────────────────────────
|
||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||
Memory->bMovingToTarget = false;
|
||||
Memory->bAttacking = false;
|
||||
Memory->bInRange = false;
|
||||
Memory->RepositionTimer = 0.0f;
|
||||
Memory->CombatType = EPS_AI_Behavior_CombatType::Melee;
|
||||
Memory->MinRange = 100.0f;
|
||||
Memory->MaxRange = AttackMoveRadius;
|
||||
|
||||
// Tell the Pawn to start attacking via interface
|
||||
// CombatType from interface (depends on weapon/pawn)
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
Memory->CombatType = IPS_AI_Behavior_Interface::Execute_GetBehaviorCombatType(Pawn);
|
||||
}
|
||||
|
||||
// Min/Max attack range from PersonalityProfile
|
||||
if (UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||
{
|
||||
if (Personality->Profile)
|
||||
{
|
||||
Memory->MinRange = Personality->Profile->MinAttackRange;
|
||||
Memory->MaxRange = Personality->Profile->MaxAttackRange;
|
||||
}
|
||||
}
|
||||
|
||||
// Melee: approach to half MinRange (get close). Ranged: approach to midpoint of band.
|
||||
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||
|
||||
// Check if already in range
|
||||
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
||||
if (DistToTarget <= Memory->MaxRange)
|
||||
{
|
||||
Memory->bInRange = true;
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
Memory->bAttacking = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Move toward target
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToActor(
|
||||
Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true);
|
||||
// Initial move toward target if not in range
|
||||
if (DistToTarget > Memory->MaxRange)
|
||||
{
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
Target->GetActorLocation(), ApproachRange, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
|
||||
if (Result != EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
Memory->bMovingToTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: started on '%s'"),
|
||||
*AIC->GetName(), *Target->GetName());
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: started on '%s' (%s, range=[%.0f-%.0f], dist=%.0f, inRange=%d, attacking=%d, hasInterface=%d)"),
|
||||
*AIC->GetName(), *Target->GetName(),
|
||||
Memory->CombatType == EPS_AI_Behavior_CombatType::Ranged ? TEXT("Ranged") : TEXT("Melee"),
|
||||
Memory->MinRange, Memory->MaxRange, DistToTarget,
|
||||
Memory->bInRange ? 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;
|
||||
@ -105,15 +148,108 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
||||
|
||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||
|
||||
// Keep moving toward target if out of range
|
||||
if (Memory->bMovingToTarget && AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
// Tick reposition cooldown
|
||||
if (Memory->RepositionTimer > 0.0f)
|
||||
{
|
||||
// Re-issue move if target moved
|
||||
AIC->MoveToActor(Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true);
|
||||
Memory->RepositionTimer -= DeltaSeconds;
|
||||
}
|
||||
|
||||
// The Pawn handles the actual shooting/melee via the interface
|
||||
// We just keep the NPC moving toward the target
|
||||
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
||||
const bool bCanReposition = (Memory->RepositionTimer <= 0.0f);
|
||||
|
||||
if (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||
{
|
||||
// ─── Melee: continuously chase target (no cooldown — always pursue) ──
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle && DistToTarget > Memory->MinRange)
|
||||
{
|
||||
AIC->MoveToLocation(
|
||||
Target->GetActorLocation(), Memory->MinRange * 0.5f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ─── Ranged: maintain distance between MinRange and MaxRange ─
|
||||
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||
|
||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
if (bCanReposition && DistToTarget < Memory->MinRange)
|
||||
{
|
||||
// Too close — back away to midpoint of band
|
||||
const FVector AwayDir = (Pawn->GetActorLocation() - Target->GetActorLocation()).GetSafeNormal2D();
|
||||
const float RetreatDist = MidRange - DistToTarget + 50.0f;
|
||||
const FVector RetreatPoint = Pawn->GetActorLocation() + AwayDir * RetreatDist;
|
||||
|
||||
// Project to navmesh
|
||||
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
|
||||
if (NavSys)
|
||||
{
|
||||
FNavLocation NavLoc;
|
||||
if (NavSys->ProjectPointToNavigation(RetreatPoint, NavLoc, FVector(300.0f, 300.0f, 200.0f)))
|
||||
{
|
||||
AIC->MoveToLocation(
|
||||
NavLoc.Location, 50.0f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/false, /*bCanStrafe=*/false);
|
||||
// Longer cooldown after retreat to prevent repeated backing
|
||||
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)
|
||||
{
|
||||
// Too far — advance toward target to midpoint of band (no cooldown — chase aggressively)
|
||||
AIC->MoveToLocation(
|
||||
Target->GetActorLocation(), MidRange, /*bStopOnOverlap=*/true,
|
||||
/*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) ──
|
||||
// Enter range at MaxRange, leave range at MaxRange + buffer
|
||||
const float EnterRange = Memory->MaxRange;
|
||||
const float LeaveRange = Memory->MaxRange * 1.1f; // 10% hysteresis
|
||||
const bool bNowInRange = Memory->bInRange
|
||||
? (DistToTarget <= LeaveRange) // already in range → need to go PAST LeaveRange to exit
|
||||
: (DistToTarget <= EnterRange); // not in range → need to get WITHIN EnterRange to enter
|
||||
|
||||
if (bNowInRange && !Memory->bInRange)
|
||||
{
|
||||
// Entered range → start attacking
|
||||
Memory->bInRange = true;
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>() && !Memory->bAttacking)
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
Memory->bAttacking = true;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: in range (%.0f <= %.0f) — StartAttack on '%s'"),
|
||||
*AIC->GetName(), DistToTarget, AttackRange, *Target->GetName());
|
||||
}
|
||||
else 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!"),
|
||||
*AIC->GetName());
|
||||
}
|
||||
}
|
||||
else if (!bNowInRange && Memory->bInRange)
|
||||
{
|
||||
// Left range → stop attacking
|
||||
Memory->bInRange = false;
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>() && Memory->bAttacking)
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||
Memory->bAttacking = false;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: out of range (%.0f > %.0f) — StopAttack"),
|
||||
*AIC->GetName(), DistToTarget, AttackRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
|
||||
@ -147,5 +283,6 @@ void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished(
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(TEXT("Move to threat (radius %.0fcm) and attack via interface."), AttackMoveRadius);
|
||||
return FString::Printf(TEXT("Range-aware attack.\nFallback radius: %.0fcm\nReposition cooldown: %.1fs"),
|
||||
AttackMoveRadius, RepositionCooldown);
|
||||
}
|
||||
|
||||
@ -0,0 +1,432 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_CoverShootCycle.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_CoverPoint.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
#include "EngineUtils.h"
|
||||
|
||||
UPS_AI_Behavior_BTTask_CoverShootCycle::UPS_AI_Behavior_BTTask_CoverShootCycle()
|
||||
{
|
||||
NodeName = TEXT("Cover Shoot Cycle");
|
||||
bNotifyTick = true;
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
// We need a cover location (written by BTTask_FindCover)
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
if (CoverLoc.IsZero())
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] CoverShootCycle: no CoverLocation in BB."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// We need a threat
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!Target)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] CoverShootCycle: no ThreatActor in BB."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// ─── Initialize memory with personality modulation ───────────────
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Engaging;
|
||||
Memory->Timer = 0.0f;
|
||||
Memory->PhaseDuration = 0.0f;
|
||||
Memory->CycleCount = 0;
|
||||
Memory->bMoveRequested = false;
|
||||
|
||||
// Base values
|
||||
Memory->EffPeekMin = PeekDurationMin;
|
||||
Memory->EffPeekMax = PeekDurationMax;
|
||||
Memory->EffCoverMin = CoverDurationMin;
|
||||
Memory->EffCoverMax = CoverDurationMax;
|
||||
Memory->EffMaxCycles = MaxCyclesBeforeAdvance;
|
||||
Memory->bCanAdvance = true;
|
||||
|
||||
// Modulate by personality traits
|
||||
if (UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||
{
|
||||
const float Aggressivity = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||
const float Caution = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
const float Courage = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
||||
|
||||
// Aggressive → peek longer, cover shorter, advance sooner
|
||||
const float AggrFactor = 0.7f + Aggressivity * 0.6f; // 0.7 – 1.3
|
||||
Memory->EffPeekMin *= AggrFactor;
|
||||
Memory->EffPeekMax *= AggrFactor;
|
||||
Memory->EffCoverMin /= AggrFactor;
|
||||
Memory->EffCoverMax /= AggrFactor;
|
||||
Memory->EffMaxCycles = FMath::Max(1, FMath::RoundToInt(MaxCyclesBeforeAdvance * (1.5f - Aggressivity * 0.5f)));
|
||||
|
||||
// Cautious → cover longer, peek shorter
|
||||
const float CautionFactor = 0.5f + Caution * 1.0f; // 0.5 – 1.5
|
||||
Memory->EffCoverMin *= CautionFactor;
|
||||
Memory->EffCoverMax *= CautionFactor;
|
||||
Memory->EffPeekMin /= CautionFactor;
|
||||
Memory->EffPeekMax /= CautionFactor;
|
||||
|
||||
// Low courage → never advance
|
||||
Memory->bCanAdvance = (Courage >= 0.3f);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] CoverShootCycle: peek=[%.1f-%.1f]s, cover=[%.1f-%.1f]s, maxCycles=%d, canAdvance=%d"),
|
||||
*AIC->GetName(),
|
||||
Memory->EffPeekMin, Memory->EffPeekMax,
|
||||
Memory->EffCoverMin, Memory->EffCoverMax,
|
||||
Memory->EffMaxCycles, (int32)Memory->bCanAdvance);
|
||||
}
|
||||
|
||||
// ─── Move to cover position ──────────────────────────────────────
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
CoverLoc, 80.0f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
// Already at cover — start the cycle
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||
}
|
||||
else
|
||||
{
|
||||
Memory->bMoveRequested = true;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Engaging));
|
||||
}
|
||||
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn())
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) { FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; }
|
||||
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!Target)
|
||||
{
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate target
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target))
|
||||
{
|
||||
AIC->StopMovement();
|
||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
|
||||
switch (Memory->SubState)
|
||||
{
|
||||
// ─── ENGAGING: Moving to cover ──────────────────────────────────
|
||||
case EPS_AI_Behavior_CombatSubState::Engaging:
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Arrived at cover → start duck phase
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: at cover, ducking for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ─── AT COVER: Ducked, waiting ──────────────────────────────────
|
||||
case EPS_AI_Behavior_CombatSubState::AtCover:
|
||||
{
|
||||
Memory->Timer -= DeltaSeconds;
|
||||
if (Memory->Timer <= 0.0f)
|
||||
{
|
||||
// Timer expired → peek and shoot
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Peeking));
|
||||
|
||||
// Start attacking
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: peeking, shooting for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ─── PEEKING: Shooting at target ────────────────────────────────
|
||||
case EPS_AI_Behavior_CombatSubState::Peeking:
|
||||
{
|
||||
Memory->Timer -= DeltaSeconds;
|
||||
if (Memory->Timer <= 0.0f)
|
||||
{
|
||||
// Stop attacking
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||
}
|
||||
|
||||
Memory->CycleCount++;
|
||||
|
||||
// Should we advance to closer cover?
|
||||
if (Memory->bCanAdvance && Memory->CycleCount >= Memory->EffMaxCycles)
|
||||
{
|
||||
// ─── Advance to next cover ──────────────────────
|
||||
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 closer cover
|
||||
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;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: advancing to '%s' (score %.2f)"),
|
||||
*AIC->GetName(), *NewPoint->GetName(), NewScore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No better cover found — stay at current position, reset cycle
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] CoverShootCycle: no advancing cover found, resetting cycle"),
|
||||
*AIC->GetName());
|
||||
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));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ─── Duck back behind cover ─────────────────────
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] CoverShootCycle: ducking back (cycle %d/%d)"),
|
||||
*AIC->GetName(), Memory->CycleCount, Memory->EffMaxCycles);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ─── ADVANCING: Moving to next cover ────────────────────────────
|
||||
case EPS_AI_Behavior_CombatSubState::Advancing:
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Arrived at new cover → duck
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: arrived at new cover, ducking"),
|
||||
*AIC->GetName());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
AIC->StopMovement();
|
||||
|
||||
// Stop attacking if we were peeking
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||
}
|
||||
}
|
||||
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::OnTaskFinished(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
// Stop attacking
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||
}
|
||||
|
||||
// Release cover point
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (Point && Pawn)
|
||||
{
|
||||
Point->Release(Pawn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult);
|
||||
}
|
||||
|
||||
// ─── Advancing Cover Search ─────────────────────────────────────────────────
|
||||
|
||||
APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_CoverShootCycle::FindAdvancingCover(
|
||||
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
|
||||
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* BestPoint = nullptr;
|
||||
OutScore = -1.0f;
|
||||
|
||||
const float NpcDistToThreat = FVector::Dist(NpcLoc, ThreatLoc);
|
||||
|
||||
for (TActorIterator<APS_AI_Behavior_CoverPoint> It(const_cast<UWorld*>(World)); It; ++It)
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point = *It;
|
||||
if (!Point || !Point->bEnabled) continue;
|
||||
|
||||
// Type filter
|
||||
if (Point->PointType != CoverPointType) continue;
|
||||
|
||||
// NPC type accessibility
|
||||
if (!Point->IsAccessibleTo(NPCType)) continue;
|
||||
|
||||
// Availability
|
||||
if (!Point->HasRoom()) continue;
|
||||
|
||||
// Distance check from NPC
|
||||
const float DistFromNpc = FVector::Dist(NpcLoc, Point->GetActorLocation());
|
||||
if (DistFromNpc > AdvanceSearchRadius) continue;
|
||||
|
||||
// Must be closer to threat than NPC currently is
|
||||
const float CoverDistToThreat = FVector::Dist(Point->GetActorLocation(), ThreatLoc);
|
||||
if (CoverDistToThreat >= NpcDistToThreat) continue;
|
||||
|
||||
// Evaluate quality against threat
|
||||
float Score = Point->EvaluateAgainstThreat(ThreatLoc);
|
||||
|
||||
// Distance bonus — closer to NPC is better (less travel time)
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, AdvanceSearchRadius), FVector2D(0.15f, 0.0f), DistFromNpc);
|
||||
|
||||
// Advancement bonus — how much closer to threat this cover gets us
|
||||
if (NpcDistToThreat > 0.0f)
|
||||
{
|
||||
const float AdvanceRatio = (NpcDistToThreat - CoverDistToThreat) / NpcDistToThreat;
|
||||
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
||||
}
|
||||
|
||||
if (Score > OutScore)
|
||||
{
|
||||
OutScore = Score;
|
||||
BestPoint = Point;
|
||||
}
|
||||
}
|
||||
|
||||
return BestPoint;
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_CoverShootCycle::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("Cover-shoot cycle.\nPeek: %.1f–%.1fs | Cover: %.1f–%.1fs\nAdvance after %d cycles (radius %.0fcm)"),
|
||||
PeekDurationMin, PeekDurationMax,
|
||||
CoverDurationMin, CoverDurationMax,
|
||||
MaxCyclesBeforeAdvance, AdvanceSearchRadius);
|
||||
}
|
||||
@ -215,6 +215,18 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_FindCover::FindBestManualCove
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, SearchRadius), FVector2D(0.15f, 0.0f), Dist);
|
||||
|
||||
// Advancement bias — prefer covers closer to threat than NPC is
|
||||
if (AdvancementBias > 0.0f)
|
||||
{
|
||||
const float NpcDistToThreat = FVector::Dist(NpcLoc, ThreatLoc);
|
||||
const float CoverDistToThreat = FVector::Dist(Point->GetActorLocation(), ThreatLoc);
|
||||
if (NpcDistToThreat > 0.0f && CoverDistToThreat < NpcDistToThreat)
|
||||
{
|
||||
const float AdvanceRatio = (NpcDistToThreat - CoverDistToThreat) / NpcDistToThreat;
|
||||
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
if (Score > OutScore)
|
||||
{
|
||||
OutScore = Score;
|
||||
@ -258,6 +270,17 @@ float UPS_AI_Behavior_BTTask_FindCover::EvaluateCoverQuality(
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, SearchRadius), FVector2D(0.15f, 0.0f), DistFromNpc);
|
||||
|
||||
// Advancement bias — prefer candidates closer to threat than NPC is
|
||||
if (AdvancementBias > 0.0f)
|
||||
{
|
||||
const float NpcDistToThreat = FVector::Dist(NpcLoc, ThreatLoc);
|
||||
if (NpcDistToThreat > 0.0f && DistFromThreat < NpcDistToThreat)
|
||||
{
|
||||
const float AdvanceRatio = (NpcDistToThreat - DistFromThreat) / NpcDistToThreat;
|
||||
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
return Score;
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,11 @@
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_SplineFollowerComponent.h"
|
||||
#include "PS_AI_Behavior_TeamComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "BehaviorTree/BehaviorTree.h"
|
||||
#include "BehaviorTree/BehaviorTreeComponent.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "BehaviorTree/BlackboardData.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Enum.h"
|
||||
@ -40,52 +42,37 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
||||
*GetName(), *InPawn->GetName());
|
||||
}
|
||||
|
||||
// Auto-assign Team ID from IPS_AI_Behavior interface (preferred) or PersonalityComponent
|
||||
// Always recalculate — BP child CDOs may reset TeamId to 0
|
||||
// Assign Team ID from NPCType + Faction
|
||||
{
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
|
||||
if (InPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
// Use the interface — the host project controls the storage
|
||||
NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(InPawn);
|
||||
}
|
||||
else if (PersonalityComp)
|
||||
{
|
||||
// Fallback: get from PersonalityProfile
|
||||
NPCType = PersonalityComp->GetNPCType();
|
||||
}
|
||||
|
||||
// Derive TeamId from NPCType
|
||||
{
|
||||
switch (NPCType)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian:
|
||||
TeamId = 1;
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy:
|
||||
// Check if infiltrated (hostile=false → disguised as civilian)
|
||||
if (InPawn->Implements<UPS_AI_Behavior_Interface>() &&
|
||||
// Get faction from profile (0 = default)
|
||||
const uint8 Faction = (PersonalityComp && PersonalityComp->Profile)
|
||||
? PersonalityComp->Profile->Faction : 0;
|
||||
|
||||
// Infiltrated enemy: disguised as Civilian until hostile
|
||||
if (NPCType == EPS_AI_Behavior_NPCType::Enemy &&
|
||||
InPawn->Implements<UPS_AI_Behavior_Interface>() &&
|
||||
!IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(InPawn))
|
||||
{
|
||||
TeamId = 1; // Disguised as Civilian
|
||||
TeamId = PS_AI_Behavior_Team::DisguisedTeamId;
|
||||
}
|
||||
else
|
||||
{
|
||||
TeamId = 2;
|
||||
}
|
||||
break;
|
||||
case EPS_AI_Behavior_NPCType::Protector:
|
||||
TeamId = 3;
|
||||
break;
|
||||
default:
|
||||
TeamId = FGenericTeamId::NoTeam; // 255 → Neutral to everyone
|
||||
break;
|
||||
}
|
||||
TeamId = PS_AI_Behavior_Team::MakeTeamId(NPCType, Faction);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Auto-assigned TeamId=%d from NPCType=%s"),
|
||||
*GetName(), TeamId, *UEnum::GetValueAsString(NPCType));
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] TeamId=0x%02X (%s, faction %d)"),
|
||||
*GetName(), TeamId, *UEnum::GetValueAsString(NPCType), Faction);
|
||||
}
|
||||
|
||||
SetupBlackboard();
|
||||
@ -178,6 +165,12 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
|
||||
SplineProgressEntry.EntryName = PS_AI_Behavior_BB::SplineProgress;
|
||||
SplineProgressEntry.KeyType = NewObject<UBlackboardKeyType_Float>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(SplineProgressEntry);
|
||||
|
||||
// CombatSubState (stored as uint8 enum)
|
||||
FBlackboardEntry CombatSubStateEntry;
|
||||
CombatSubStateEntry.EntryName = PS_AI_Behavior_BB::CombatSubState;
|
||||
CombatSubStateEntry.KeyType = NewObject<UBlackboardKeyType_Enum>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(CombatSubStateEntry);
|
||||
}
|
||||
|
||||
UBlackboardComponent* RawBBComp = nullptr;
|
||||
@ -230,6 +223,56 @@ void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewSta
|
||||
*UEnum::GetValueAsString(NewState));
|
||||
}
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, NewVal);
|
||||
|
||||
// ─── Dead: shut down all AI systems ─────────────────────────
|
||||
if (NewState == EPS_AI_Behavior_State::Dead)
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APS_AI_Behavior_AIController::HandleDeath()
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] HandleDeath — shutting down AI systems."), *GetName());
|
||||
|
||||
// 1. Stop the Behavior Tree (no more services, tasks, or decorators)
|
||||
if (UBrainComponent* Brain = GetBrainComponent())
|
||||
{
|
||||
Brain->StopLogic(TEXT("Dead"));
|
||||
}
|
||||
|
||||
// 2. Stop any active movement
|
||||
StopMovement();
|
||||
|
||||
// 3. Stop spline following
|
||||
if (APawn* MyPawn = GetPawn())
|
||||
{
|
||||
if (UPS_AI_Behavior_SplineFollowerComponent* Spline =
|
||||
MyPawn->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
|
||||
{
|
||||
Spline->StopFollowing();
|
||||
}
|
||||
|
||||
// 4. Stop attack if in combat
|
||||
if (MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(MyPawn);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Disable perception (stop detecting / being source of stimuli updates)
|
||||
if (BehaviorPerception)
|
||||
{
|
||||
BehaviorPerception->Deactivate();
|
||||
}
|
||||
|
||||
// 6. Clear Blackboard threat data
|
||||
if (Blackboard)
|
||||
{
|
||||
Blackboard->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||
Blackboard->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||
Blackboard->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,21 +344,25 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
|
||||
return ETeamAttitude::Neutral;
|
||||
}
|
||||
|
||||
// Same team → Friendly
|
||||
// Same TeamId = same NPCType + same Faction → always Friendly
|
||||
if (TeamId == OtherTeam)
|
||||
{
|
||||
return ETeamAttitude::Friendly;
|
||||
}
|
||||
|
||||
// ─── Custom cross-team attitudes ────────────────────────────────────
|
||||
// ─── NPCType-based attitude ─────────────────────────────────────────
|
||||
const EPS_AI_Behavior_NPCType MyType = PS_AI_Behavior_Team::GetNPCType(TeamId);
|
||||
const EPS_AI_Behavior_NPCType TheirType = PS_AI_Behavior_Team::GetNPCType(OtherTeam);
|
||||
|
||||
// Civilian (1) ↔ Protector (3) → Friendly
|
||||
if ((TeamId == 1 && OtherTeam == 3) || (TeamId == 3 && OtherTeam == 1))
|
||||
// Civilian ↔ Protector → always Friendly (regardless of faction)
|
||||
if ((MyType == EPS_AI_Behavior_NPCType::Civilian && TheirType == EPS_AI_Behavior_NPCType::Protector) ||
|
||||
(MyType == EPS_AI_Behavior_NPCType::Protector && TheirType == EPS_AI_Behavior_NPCType::Civilian))
|
||||
{
|
||||
return ETeamAttitude::Friendly;
|
||||
}
|
||||
|
||||
// Everything else → Hostile
|
||||
// Same NPCType but different faction (e.g. rival enemy gangs) → Hostile
|
||||
// Different NPCType (e.g. Enemy vs Civilian) → Hostile
|
||||
return ETeamAttitude::Hostile;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
#include "Components/ArrowComponent.h"
|
||||
#include "Components/BillboardComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
|
||||
APS_AI_Behavior_CoverPoint::APS_AI_Behavior_CoverPoint()
|
||||
{
|
||||
@ -27,6 +29,13 @@ APS_AI_Behavior_CoverPoint::APS_AI_Behavior_CoverPoint()
|
||||
SpriteComp->SetRelativeLocation(FVector(0, 0, 80.0f));
|
||||
SpriteComp->bIsScreenSizeScaled = true;
|
||||
SpriteComp->ScreenSize = 0.0025f;
|
||||
|
||||
// Load editor sprites — Cover: default (no change), HidingSpot: fog icon
|
||||
static ConstructorHelpers::FObjectFinder<UTexture2D> HidingSpotSpriteFinder(
|
||||
TEXT("/Engine/EditorResources/S_AtmosphericHeightFog"));
|
||||
|
||||
CoverSpriteTexture = nullptr; // Keep default billboard sprite for Cover
|
||||
HidingSpotSpriteTexture = HidingSpotSpriteFinder.Succeeded() ? HidingSpotSpriteFinder.Object : nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -164,13 +173,17 @@ void APS_AI_Behavior_CoverPoint::UpdateVisualization()
|
||||
if (!ArrowComp) return;
|
||||
|
||||
FLinearColor Color = FLinearColor::White;
|
||||
UTexture2D* SpriteTexture = nullptr;
|
||||
|
||||
switch (PointType)
|
||||
{
|
||||
case EPS_AI_Behavior_CoverPointType::Cover:
|
||||
Color = FLinearColor(0.2f, 0.5f, 1.0f); // Blue
|
||||
SpriteTexture = CoverSpriteTexture;
|
||||
break;
|
||||
case EPS_AI_Behavior_CoverPointType::HidingSpot:
|
||||
Color = FLinearColor(1.0f, 0.85f, 0.0f); // Yellow
|
||||
SpriteTexture = HidingSpotSpriteTexture;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -182,6 +195,11 @@ void APS_AI_Behavior_CoverPoint::UpdateVisualization()
|
||||
}
|
||||
|
||||
ArrowComp->SetArrowColor(Color);
|
||||
|
||||
if (SpriteComp && SpriteTexture)
|
||||
{
|
||||
SpriteComp->SetSprite(SpriteTexture);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||
#include "PS_AI_Behavior_TeamComponent.h"
|
||||
#include "PS_AI_Behavior_Settings.h"
|
||||
#include "Perception/AISenseConfig_Sight.h"
|
||||
#include "Perception/AISenseConfig_Hearing.h"
|
||||
@ -53,7 +54,7 @@ void UPS_AI_Behavior_PerceptionComponent::ConfigureSenses()
|
||||
// ─── Hearing ────────────────────────────────────────────────────────
|
||||
UAISenseConfig_Hearing* HearingConfig = NewObject<UAISenseConfig_Hearing>(this);
|
||||
HearingConfig->HearingRange = Settings->DefaultHearingRange;
|
||||
HearingConfig->SetMaxAge(Settings->PerceptionMaxAge);
|
||||
HearingConfig->SetMaxAge(Settings->HearingMaxAge);
|
||||
HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
|
||||
HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;
|
||||
HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
|
||||
@ -74,20 +75,113 @@ void UPS_AI_Behavior_PerceptionComponent::HandlePerceptionUpdated(const TArray<A
|
||||
// This callback can be used for immediate alert reactions.
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Find the owning Pawn for a perceived actor.
|
||||
* Walks up the Owner/Instigator chain until a Pawn is found.
|
||||
* Returns the Pawn itself — used for team/attitude checks.
|
||||
* Returns nullptr if no Pawn can be found.
|
||||
*/
|
||||
static APawn* FindOwningPawn(AActor* Actor)
|
||||
{
|
||||
if (!Actor) return nullptr;
|
||||
|
||||
// If already a Pawn, return it
|
||||
if (APawn* ActorAsPawn = Cast<APawn>(Actor))
|
||||
{
|
||||
return ActorAsPawn;
|
||||
}
|
||||
|
||||
// Not a Pawn — walk up Owner/Instigator chain to find the owning Pawn
|
||||
AActor* Current = Actor;
|
||||
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
|
||||
{
|
||||
// Try Instigator first (most direct for weapons)
|
||||
if (APawn* InstigatorPawn = Current->GetInstigator())
|
||||
{
|
||||
return InstigatorPawn;
|
||||
}
|
||||
|
||||
// Try Owner
|
||||
AActor* OwnerActor = Current->GetOwner();
|
||||
if (!OwnerActor || OwnerActor == Current) break;
|
||||
|
||||
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
|
||||
{
|
||||
return OwnerPawn;
|
||||
}
|
||||
|
||||
Current = OwnerActor; // Continue up the chain
|
||||
}
|
||||
|
||||
// Fallback: could not resolve to a Pawn.
|
||||
// Log once per actor class to avoid spam.
|
||||
static TSet<FName> WarnedClasses;
|
||||
const FName ClassName = Actor->GetClass()->GetFName();
|
||||
if (!WarnedClasses.Contains(ClassName))
|
||||
{
|
||||
WarnedClasses.Add(ClassName);
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("FindOwningPawn: '%s' (class=%s) could not be resolved to a Pawn. Set Owner or Instigator. Instigator=%s, Owner=%s"),
|
||||
*Actor->GetName(), *Actor->GetClass()->GetName(),
|
||||
Actor->GetInstigator() ? *Actor->GetInstigator()->GetName() : TEXT("null"),
|
||||
Actor->GetOwner() ? *Actor->GetOwner()->GetName() : TEXT("null"));
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the threat target actor for a Pawn.
|
||||
* Calls GetBehaviorThreatActor() if the Pawn implements the interface,
|
||||
* otherwise returns the Pawn itself.
|
||||
* This is what goes into the Blackboard (e.g. PS_AimTargetActor for aiming).
|
||||
*/
|
||||
static AActor* GetThreatTarget(APawn* Pawn)
|
||||
{
|
||||
if (!Pawn) return nullptr;
|
||||
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
AActor* Resolved = IPS_AI_Behavior_Interface::Execute_GetBehaviorThreatActor(Pawn);
|
||||
return Resolved ? Resolved : Pawn;
|
||||
}
|
||||
|
||||
return Pawn;
|
||||
}
|
||||
|
||||
/** Extract an actor's TeamId (checking controller, then TeamComponent). */
|
||||
uint8 UPS_AI_Behavior_PerceptionComponent::GetActorTeamId(const AActor* Actor)
|
||||
{
|
||||
const APawn* ActorPawn = Cast<APawn>(Actor);
|
||||
if (!ActorPawn) return FGenericTeamId::NoTeam;
|
||||
|
||||
if (const AController* C = ActorPawn->GetController())
|
||||
{
|
||||
if (const IGenericTeamAgentInterface* T = Cast<IGenericTeamAgentInterface>(C))
|
||||
{
|
||||
return T->GetGenericTeamId().GetId();
|
||||
}
|
||||
}
|
||||
|
||||
if (const UPS_AI_Behavior_TeamComponent* TC = ActorPawn->FindComponentByClass<UPS_AI_Behavior_TeamComponent>())
|
||||
{
|
||||
return TC->GetGenericTeamId().GetId();
|
||||
}
|
||||
|
||||
return FGenericTeamId::NoTeam;
|
||||
}
|
||||
|
||||
// ─── Actor Classification ───────────────────────────────────────────────────
|
||||
|
||||
EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(const AActor* Actor)
|
||||
{
|
||||
if (!Actor) return EPS_AI_Behavior_TargetType::Civilian; // Safe default
|
||||
|
||||
// Check if player-controlled
|
||||
const APawn* Pawn = Cast<APawn>(Actor);
|
||||
if (Pawn && Pawn->IsPlayerControlled())
|
||||
{
|
||||
return EPS_AI_Behavior_TargetType::Player;
|
||||
}
|
||||
|
||||
// Check via IPS_AI_Behavior interface
|
||||
// 1) Check via IPS_AI_Behavior interface (NPCs and any Pawn implementing it)
|
||||
if (Actor->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
const EPS_AI_Behavior_NPCType NPCType =
|
||||
@ -102,9 +196,21 @@ EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(co
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check PersonalityComponent
|
||||
// 2) Check TeamComponent (player characters, non-AI pawns)
|
||||
if (Pawn)
|
||||
{
|
||||
if (const UPS_AI_Behavior_TeamComponent* TeamComp = Pawn->FindComponentByClass<UPS_AI_Behavior_TeamComponent>())
|
||||
{
|
||||
switch (TeamComp->Role)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: return EPS_AI_Behavior_TargetType::Civilian;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: return EPS_AI_Behavior_TargetType::Enemy;
|
||||
case EPS_AI_Behavior_NPCType::Protector: return EPS_AI_Behavior_TargetType::Protector;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: PersonalityComponent
|
||||
if (const auto* PersonalityComp = Pawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
|
||||
{
|
||||
switch (PersonalityComp->GetNPCType())
|
||||
@ -115,6 +221,12 @@ EPS_AI_Behavior_TargetType UPS_AI_Behavior_PerceptionComponent::ClassifyActor(co
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Player-controlled but no role defined → Player type
|
||||
if (Pawn->IsPlayerControlled())
|
||||
{
|
||||
return EPS_AI_Behavior_TargetType::Player;
|
||||
}
|
||||
}
|
||||
|
||||
return EPS_AI_Behavior_TargetType::Civilian;
|
||||
@ -184,56 +296,117 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||
APawn* MyPawn = AIC ? AIC->GetPawn() : Cast<APawn>(const_cast<AActor*>(Owner));
|
||||
|
||||
for (AActor* Actor : PerceivedActors)
|
||||
// ─── Score accumulation per resolved actor ───────────────────────
|
||||
// Multiple RawActors can resolve to the same Pawn (e.g. Pawn seen + weapon heard).
|
||||
// Accumulate their sense scores so a single actor gets all stimuli credit.
|
||||
struct FActorScore
|
||||
{
|
||||
if (!Actor || Actor == Owner) continue;
|
||||
AActor* Actor = nullptr;
|
||||
float Score = 0.0f;
|
||||
EPS_AI_Behavior_TargetType ActorType = EPS_AI_Behavior_TargetType::Civilian;
|
||||
bool bIsHostile = false;
|
||||
};
|
||||
TMap<AActor*, FActorScore> ScoreMap;
|
||||
|
||||
// Skip self (when owner is AIController, also skip own pawn)
|
||||
if (AIC && Actor == AIC->GetPawn()) continue;
|
||||
for (AActor* RawActor : PerceivedActors)
|
||||
{
|
||||
// Find the owning Pawn (for team/attitude checks)
|
||||
APawn* OwningPawn = FindOwningPawn(RawActor);
|
||||
if (!OwningPawn) continue; // Can't resolve → skip
|
||||
|
||||
// Skip self
|
||||
if (OwningPawn == Owner) continue;
|
||||
if (AIC && OwningPawn == AIC->GetPawn()) continue;
|
||||
|
||||
// Get the threat target (PS_AimTargetActor or Pawn itself — for BB targeting)
|
||||
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
||||
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
||||
|
||||
// Skip non-hostile actors UNLESS they have a gunfire hearing stimulus
|
||||
bool bActorIsHostile = false;
|
||||
bool bActorHasGunshot = false;
|
||||
|
||||
// Skip non-hostile actors (only Hostile actors are valid threats)
|
||||
if (AIC)
|
||||
{
|
||||
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*Actor);
|
||||
if (Attitude != ETeamAttitude::Hostile)
|
||||
// Attitude check against the PAWN (has TeamId), not the ThreatTarget
|
||||
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*OwningPawn);
|
||||
bActorIsHostile = (Attitude == ETeamAttitude::Hostile);
|
||||
|
||||
if (!bActorIsHostile)
|
||||
{
|
||||
// Same exact team (same NPCType + same Faction) → always skip
|
||||
// Allied teams (Civilian ↔ Protector) → allow gunfire through
|
||||
if (AIC->GetGenericTeamId().GetId() == GetActorTeamId(OwningPawn))
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Target '%s': SKIPPED (same team 0x%02X)"),
|
||||
*Owner->GetName(), *OwningPawn->GetName(), GetActorTeamId(OwningPawn));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allied or Neutral: check for gunfire tag — gunshot source is a valid threat
|
||||
FActorPerceptionBlueprintInfo GunInfo;
|
||||
if (GetActorsPerception(RawActor, GunInfo))
|
||||
{
|
||||
for (const FAIStimulus& S : GunInfo.LastSensedStimuli)
|
||||
{
|
||||
if (S.IsValid() &&
|
||||
S.Type == UAISense::GetSenseID<UAISense_Hearing>() &&
|
||||
PS_AI_Behavior_Tags_Internal::IsGunfire(S.Tag))
|
||||
{
|
||||
bActorHasGunshot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)"),
|
||||
*Owner->GetName(), *OwningPawn->GetName(), static_cast<int32>(Attitude),
|
||||
GetActorTeamId(OwningPawn), AIC->GetGenericTeamId().GetId());
|
||||
continue; // Not hostile, no gunshot — skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip invalid targets (dead, despawning, etc.) via interface
|
||||
if (MyPawn && MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor))
|
||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, ThreatTarget))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Classify this actor ────────────────────────────────────────
|
||||
const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor);
|
||||
// ─── Classify & score per owning Pawn, but store ThreatTarget for BB ───
|
||||
FActorScore& Entry = ScoreMap.FindOrAdd(OwningPawn);
|
||||
if (!Entry.Actor)
|
||||
{
|
||||
// First time seeing this Pawn — initialize with its ThreatTarget
|
||||
Entry.Actor = ThreatTarget;
|
||||
Entry.ActorType = ClassifyActor(OwningPawn);
|
||||
Entry.bIsHostile = bActorIsHostile;
|
||||
|
||||
// ─── Score calculation ──────────────────────────────────────────
|
||||
float Score = 0.0f;
|
||||
|
||||
// Priority rank bonus: actors in the priority list score higher
|
||||
// This is used for COMBAT targeting (who to attack first)
|
||||
// But ALL hostile actors are valid threats (for fleeing, alerting, etc.)
|
||||
const int32 PriorityIndex = ActivePriority.IndexOfByKey(ActorType);
|
||||
// Priority rank bonus (applied once per actor)
|
||||
const int32 PriorityIndex = ActivePriority.IndexOfByKey(Entry.ActorType);
|
||||
if (PriorityIndex != INDEX_NONE)
|
||||
{
|
||||
Score += (ActivePriority.Num() - PriorityIndex) * 100.0f;
|
||||
Entry.Score += (ActivePriority.Num() - PriorityIndex) * 100.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not in priority list but still Hostile — valid threat, lower score
|
||||
Score += 10.0f;
|
||||
Entry.Score += 10.0f;
|
||||
}
|
||||
|
||||
// Damage sense override: actor that hit us gets a massive bonus
|
||||
// (bypasses priority — self-defense)
|
||||
// Distance bonus (applied once per actor)
|
||||
const float Dist = FVector::Dist(OwnerLoc, ThreatTarget->GetActorLocation());
|
||||
Entry.Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 6000.0f), FVector2D(20.0f, 0.0f), Dist);
|
||||
}
|
||||
|
||||
// Accumulate sense scores from this RawActor (weapon heard + pawn seen → both count)
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(Actor, Info))
|
||||
if (GetActorsPerception(RawActor, Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
@ -241,30 +414,77 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
||||
|
||||
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Damage>())
|
||||
{
|
||||
Score += 500.0f; // Self-defense: always prioritize attacker
|
||||
Entry.Score += 500.0f;
|
||||
}
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>())
|
||||
{
|
||||
Score += 10.0f;
|
||||
if (PS_AI_Behavior_Tags_Internal::IsGunfire(Stimulus.Tag))
|
||||
{
|
||||
Entry.Score += 400.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Score += 5.0f; // Hearing
|
||||
Entry.Score += 5.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance: closer targets score higher (0-20 range)
|
||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 6000.0f), FVector2D(20.0f, 0.0f), Dist);
|
||||
|
||||
if (Score > BestScore)
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
||||
{
|
||||
BestScore = Score;
|
||||
BestThreat = Actor;
|
||||
Entry.Score += 10.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pick best actor from accumulated scores ─────────────────────
|
||||
// Target persistence: get current target from BB to avoid flickering
|
||||
AActor* CurrentTarget = nullptr;
|
||||
float CurrentTargetScore = -1.0f;
|
||||
if (AIC)
|
||||
{
|
||||
if (UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||
{
|
||||
CurrentTarget = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& Pair : ScoreMap)
|
||||
{
|
||||
const FActorScore& Entry = Pair.Value;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Target '%s': type=%d, hostile=%d, score=%.0f"),
|
||||
*Owner->GetName(), *Entry.Actor->GetName(),
|
||||
static_cast<int32>(Entry.ActorType), Entry.bIsHostile ? 1 : 0, Entry.Score);
|
||||
|
||||
if (Entry.Score > BestScore)
|
||||
{
|
||||
BestScore = Entry.Score;
|
||||
BestThreat = Entry.Actor;
|
||||
}
|
||||
|
||||
// Track current target's score for persistence check
|
||||
if (Entry.Actor == CurrentTarget)
|
||||
{
|
||||
CurrentTargetScore = Entry.Score;
|
||||
}
|
||||
}
|
||||
|
||||
// Target persistence: keep current target if its score is within 20% of the best
|
||||
// This prevents flickering between targets with nearly identical scores
|
||||
if (CurrentTarget && CurrentTargetScore > 0.0f && BestThreat != CurrentTarget)
|
||||
{
|
||||
if (CurrentTargetScore >= BestScore * 0.8f)
|
||||
{
|
||||
BestThreat = CurrentTarget;
|
||||
BestScore = CurrentTargetScore;
|
||||
}
|
||||
}
|
||||
|
||||
if (BestThreat)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] → Best target: '%s' (score=%.0f%s)"),
|
||||
*Owner->GetName(), *BestThreat->GetName(), BestScore,
|
||||
(BestThreat == CurrentTarget) ? TEXT(" [kept]") : TEXT(""));
|
||||
}
|
||||
|
||||
return BestThreat;
|
||||
}
|
||||
@ -284,39 +504,81 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
||||
const AAIController* AIC = Cast<AAIController>(Owner);
|
||||
APawn* MyPawn = AIC ? AIC->GetPawn() : Cast<APawn>(const_cast<AActor*>(Owner));
|
||||
|
||||
for (AActor* Actor : PerceivedActors)
|
||||
for (AActor* RawActor : PerceivedActors)
|
||||
{
|
||||
if (!Actor) continue;
|
||||
if (!RawActor) continue;
|
||||
|
||||
// Find the owning Pawn (for team/attitude checks)
|
||||
APawn* OwningPawn = FindOwningPawn(RawActor);
|
||||
if (!OwningPawn) continue;
|
||||
|
||||
// Get the threat target for position-based calculations
|
||||
AActor* ThreatTarget = GetThreatTarget(OwningPawn);
|
||||
if (!ThreatTarget) ThreatTarget = OwningPawn;
|
||||
|
||||
// Determine hostility and check for gunshot stimuli
|
||||
bool bIsHostile = false;
|
||||
bool bHasGunshot = false;
|
||||
|
||||
// Only count Hostile actors as threats (skip Friendly and Neutral)
|
||||
if (AIC)
|
||||
{
|
||||
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*Actor);
|
||||
if (Attitude != ETeamAttitude::Hostile)
|
||||
// Attitude check against the PAWN (has TeamId)
|
||||
const ETeamAttitude::Type Attitude = AIC->GetTeamAttitudeTowards(*OwningPawn);
|
||||
bIsHostile = (Attitude == ETeamAttitude::Hostile);
|
||||
|
||||
// Same exact team (same NPCType + same Faction) → always skip
|
||||
// Allied teams (Civilian ↔ Protector) → allow gunfire through
|
||||
if (AIC->GetGenericTeamId().GetId() == GetActorTeamId(OwningPawn))
|
||||
{
|
||||
continue; // Only Hostile actors generate threat
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// For non-hostile actors, check if they have a gunfire hearing stimulus
|
||||
if (!bIsHostile)
|
||||
{
|
||||
FActorPerceptionBlueprintInfo GunInfo;
|
||||
if (GetActorsPerception(RawActor, GunInfo))
|
||||
{
|
||||
for (const FAIStimulus& S : GunInfo.LastSensedStimuli)
|
||||
{
|
||||
if (S.IsValid() &&
|
||||
S.Type == UAISense::GetSenseID<UAISense_Hearing>() &&
|
||||
PS_AI_Behavior_Tags_Internal::IsGunfire(S.Tag))
|
||||
{
|
||||
bHasGunshot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip actors that are neither hostile nor gunshot sources
|
||||
if (!bIsHostile && !bHasGunshot)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip invalid targets (dead, despawning, etc.) via interface
|
||||
if (MyPawn && MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor))
|
||||
if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, ThreatTarget))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
float ActorThreat = 0.0f;
|
||||
const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation());
|
||||
const float Dist = FVector::Dist(OwnerLoc, ThreatTarget->GetActorLocation());
|
||||
|
||||
// Closer = more threatening
|
||||
if (bIsHostile)
|
||||
{
|
||||
// ─── Normal hostile threat calculation ──────────────────────
|
||||
ActorThreat += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 5000.0f), FVector2D(0.5f, 0.05f), Dist);
|
||||
|
||||
// Sense-based multiplier
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(Actor, Info))
|
||||
if (GetActorsPerception(RawActor, Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
@ -324,7 +586,7 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
||||
|
||||
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Damage>())
|
||||
{
|
||||
ActorThreat += 0.6f; // Being hit = big threat spike
|
||||
ActorThreat += 0.6f;
|
||||
}
|
||||
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
||||
{
|
||||
@ -336,6 +598,15 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (bHasGunshot)
|
||||
{
|
||||
// ─── Gunshot from non-hostile actor ────────────────────────
|
||||
// Generates significant threat regardless of team affiliation
|
||||
ActorThreat += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, 5000.0f), FVector2D(0.4f, 0.05f), Dist);
|
||||
ActorThreat += 0.3f; // Gunshot hearing boost
|
||||
}
|
||||
|
||||
TotalThreat += ActorThreat;
|
||||
}
|
||||
@ -375,3 +646,31 @@ bool UPS_AI_Behavior_PerceptionComponent::GetThreatLocation(FVector& OutLocation
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_PerceptionComponent::GetGunShotStimulusLocation(FVector& OutLocation)
|
||||
{
|
||||
TArray<AActor*> PerceivedActors;
|
||||
GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
|
||||
|
||||
for (AActor* Actor : PerceivedActors)
|
||||
{
|
||||
if (!Actor) continue;
|
||||
|
||||
FActorPerceptionBlueprintInfo Info;
|
||||
if (GetActorsPerception(Actor, Info))
|
||||
{
|
||||
for (const FAIStimulus& Stimulus : Info.LastSensedStimuli)
|
||||
{
|
||||
if (Stimulus.IsValid() &&
|
||||
Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>() &&
|
||||
PS_AI_Behavior_Tags_Internal::IsGunfire(Stimulus.Tag))
|
||||
{
|
||||
OutLocation = Stimulus.StimulusLocation;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTDecorator.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTDecorator_CheckCombatType.generated.h"
|
||||
|
||||
/**
|
||||
* BT Decorator: Checks the Pawn's combat type via IPS_AI_Behavior_Interface.
|
||||
* Use to route: ranged NPCs → cover-shoot cycle, melee NPCs → rush attack.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Check Combat Type"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTDecorator_CheckCombatType : public UBTDecorator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTDecorator_CheckCombatType();
|
||||
|
||||
/** The combat type this decorator requires to pass. */
|
||||
UPROPERTY(EditAnywhere, Category = "Combat Type")
|
||||
EPS_AI_Behavior_CombatType RequiredType = EPS_AI_Behavior_CombatType::Ranged;
|
||||
|
||||
protected:
|
||||
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -4,11 +4,16 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTTask_Attack.generated.h"
|
||||
|
||||
/**
|
||||
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
||||
*
|
||||
* Queries IPS_AI_Behavior_Interface for CombatType and OptimalAttackRange:
|
||||
* - Melee: rush toward target, stop at optimal range.
|
||||
* - Ranged: maintain optimal distance — back away if too close, advance if too far.
|
||||
*
|
||||
* Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and
|
||||
* BehaviorStopAttack() on abort. The Pawn handles the actual combat
|
||||
* (aim, fire, melee, etc.) via its own systems.
|
||||
@ -24,10 +29,14 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_Attack();
|
||||
|
||||
/** How close the NPC tries to get to the target (cm). */
|
||||
/** Fallback move radius if the Pawn doesn't implement GetBehaviorOptimalAttackRange(). */
|
||||
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0"))
|
||||
float AttackMoveRadius = 300.0f;
|
||||
|
||||
/** Cooldown between reposition attempts (seconds). Prevents constant re-pathing. */
|
||||
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "0.5", ClampMax = "5.0"))
|
||||
float RepositionCooldown = 1.5f;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
@ -39,7 +48,12 @@ private:
|
||||
struct FAttackMemory
|
||||
{
|
||||
bool bMovingToTarget = false;
|
||||
bool bAttacking = false;
|
||||
bool bAttacking = false; // true when BehaviorStartAttack is active
|
||||
bool bInRange = false; // true when within MaxRange of target
|
||||
EPS_AI_Behavior_CombatType CombatType = EPS_AI_Behavior_CombatType::Melee;
|
||||
float MinRange = 100.0f; // backs away if closer
|
||||
float MaxRange = 300.0f; // advances if farther
|
||||
float RepositionTimer = 0.0f;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); }
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTTask_CoverShootCycle.generated.h"
|
||||
|
||||
class APS_AI_Behavior_CoverPoint;
|
||||
|
||||
/**
|
||||
* BT Task: Cover-shoot cycle for ranged combat.
|
||||
*
|
||||
* State machine: Engaging → AtCover → Peeking → AtCover → ... → Advancing → AtCover ...
|
||||
*
|
||||
* The NPC moves to the cover position from Blackboard, then alternates between
|
||||
* ducking (AtCover) and shooting (Peeking). After MaxCyclesBeforeAdvance peek/duck
|
||||
* cycles, advances to a closer cover point toward the threat.
|
||||
*
|
||||
* Personality traits modulate timing:
|
||||
* - Aggressivity → shorter cover duration, advances sooner
|
||||
* - Caution → longer cover duration, shorter peek
|
||||
* - Courage < 0.3 → never advances (stays in cover)
|
||||
*
|
||||
* Writes CombatSubState to BB for animation sync.
|
||||
* Stays InProgress — Decorator Observer Aborts pulls out on state change.
|
||||
*
|
||||
* Prerequisites: BTTask_FindCover must run first to write CoverLocation/CoverPoint to BB.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Cover Shoot Cycle"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_CoverShootCycle : public UBTTaskNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTTask_CoverShootCycle();
|
||||
|
||||
// ─── Timing ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Minimum time (seconds) spent peeking/shooting. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||
float PeekDurationMin = 2.0f;
|
||||
|
||||
/** Maximum time (seconds) spent peeking/shooting. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||
float PeekDurationMax = 5.0f;
|
||||
|
||||
/** Minimum time (seconds) spent ducked behind cover. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||
float CoverDurationMin = 1.0f;
|
||||
|
||||
/** Maximum time (seconds) spent ducked behind cover. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||
float CoverDurationMax = 3.0f;
|
||||
|
||||
// ─── Advancement ────────────────────────────────────────────────────
|
||||
|
||||
/** Number of peek/duck cycles before advancing to a closer cover. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "1", ClampMax = "10"))
|
||||
int32 MaxCyclesBeforeAdvance = 3;
|
||||
|
||||
/** Search radius for the next cover point when advancing (cm). */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "200.0"))
|
||||
float AdvanceSearchRadius = 1200.0f;
|
||||
|
||||
/** Advancement bias when searching for the next cover (0=neutral, 1=strongly toward threat). */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AdvancementBias = 0.7f;
|
||||
|
||||
/** Cover point type to search when advancing. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement")
|
||||
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
|
||||
private:
|
||||
struct FCoverShootMemory
|
||||
{
|
||||
EPS_AI_Behavior_CombatSubState SubState = EPS_AI_Behavior_CombatSubState::Engaging;
|
||||
float Timer = 0.0f;
|
||||
float PhaseDuration = 0.0f;
|
||||
int32 CycleCount = 0;
|
||||
bool bMoveRequested = false;
|
||||
|
||||
// Effective durations (modulated by personality)
|
||||
float EffPeekMin = 2.0f;
|
||||
float EffPeekMax = 5.0f;
|
||||
float EffCoverMin = 1.0f;
|
||||
float EffCoverMax = 3.0f;
|
||||
int32 EffMaxCycles = 3;
|
||||
bool bCanAdvance = true;
|
||||
};
|
||||
|
||||
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FCoverShootMemory); }
|
||||
|
||||
/** Find an advancing cover point closer to the threat. */
|
||||
APS_AI_Behavior_CoverPoint* FindAdvancingCover(
|
||||
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
|
||||
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const;
|
||||
};
|
||||
@ -62,6 +62,14 @@ public:
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points")
|
||||
bool bUseManualPointsOnly = false;
|
||||
|
||||
/**
|
||||
* Bias toward covers that advance toward the threat (0 = none, 1 = strong).
|
||||
* Used for cover-to-cover progression during combat.
|
||||
* Covers closer to the threat than the NPC's current position score higher.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Advancement", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float AdvancementBias = 0.0f;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
class UArrowComponent;
|
||||
class UBillboardComponent;
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* A manually placed strategic point in the level.
|
||||
@ -128,4 +129,13 @@ protected:
|
||||
|
||||
private:
|
||||
void UpdateVisualization();
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/** Cached editor sprite textures (loaded once in constructor). */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTexture2D> CoverSpriteTexture;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTexture2D> HidingSpotSpriteTexture;
|
||||
#endif
|
||||
};
|
||||
|
||||
@ -70,6 +70,24 @@ enum class EPS_AI_Behavior_CoverPointType : uint8
|
||||
HidingSpot UMETA(DisplayName = "Hiding Spot", ToolTip = "Civilian hiding place (under desks, in closets, behind cars)"),
|
||||
};
|
||||
|
||||
/** Combat style — determines engagement behavior (cover cycle vs rush). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_CombatType : uint8
|
||||
{
|
||||
Melee UMETA(DisplayName = "Melee", ToolTip = "Close-range: rush the target"),
|
||||
Ranged UMETA(DisplayName = "Ranged", ToolTip = "Long-range: use cover, maintain distance"),
|
||||
};
|
||||
|
||||
/** Sub-state within Combat for the cover-shoot cycle. Written to BB for animation sync. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_CombatSubState : uint8
|
||||
{
|
||||
Engaging UMETA(DisplayName = "Engaging", ToolTip = "Moving to combat position"),
|
||||
AtCover UMETA(DisplayName = "At Cover", ToolTip = "Ducked behind cover"),
|
||||
Peeking UMETA(DisplayName = "Peeking", ToolTip = "Leaning out, shooting"),
|
||||
Advancing UMETA(DisplayName = "Advancing", ToolTip = "Moving to next cover"),
|
||||
};
|
||||
|
||||
/** Personality trait axes — each scored 0.0 (low) to 1.0 (high). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPS_AI_Behavior_TraitAxis : uint8
|
||||
@ -81,6 +99,77 @@ enum class EPS_AI_Behavior_TraitAxis : uint8
|
||||
Discipline UMETA(DisplayName = "Discipline", ToolTip = "0 = undisciplined, 1 = disciplined"),
|
||||
};
|
||||
|
||||
// ─── TeamId Encoding ────────────────────────────────────────────────────────
|
||||
//
|
||||
// TeamId encodes NPCType + Faction in a single uint8:
|
||||
// High nibble = NPCType (0=Civilian, 1=Enemy, 2=Protector)
|
||||
// Low nibble = Faction (0-15)
|
||||
//
|
||||
// Examples:
|
||||
// Civilian → 0x00 (TeamId 0)
|
||||
// Enemy faction 0 → 0x10 (TeamId 16)
|
||||
// Enemy faction 1 → 0x11 (TeamId 17)
|
||||
// Protector → 0x20 (TeamId 32)
|
||||
//
|
||||
|
||||
namespace PS_AI_Behavior_Team
|
||||
{
|
||||
/** Build a TeamId from NPCType + Faction. */
|
||||
inline uint8 MakeTeamId(EPS_AI_Behavior_NPCType Type, uint8 Faction = 0)
|
||||
{
|
||||
uint8 Base;
|
||||
switch (Type)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: Base = 0x00; break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: Base = 0x10; break;
|
||||
case EPS_AI_Behavior_NPCType::Protector: Base = 0x20; break;
|
||||
default: return 255; // NoTeam
|
||||
}
|
||||
return Base | (Faction & 0x0F);
|
||||
}
|
||||
|
||||
/** Extract the NPCType from a TeamId. */
|
||||
inline EPS_AI_Behavior_NPCType GetNPCType(uint8 InTeamId)
|
||||
{
|
||||
switch (InTeamId & 0xF0)
|
||||
{
|
||||
case 0x00: return EPS_AI_Behavior_NPCType::Civilian;
|
||||
case 0x10: return EPS_AI_Behavior_NPCType::Enemy;
|
||||
case 0x20: return EPS_AI_Behavior_NPCType::Protector;
|
||||
default: return EPS_AI_Behavior_NPCType::Any;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract the Faction from a TeamId. */
|
||||
inline uint8 GetFaction(uint8 InTeamId)
|
||||
{
|
||||
return InTeamId & 0x0F;
|
||||
}
|
||||
|
||||
/** TeamId used for disguised enemies (same as Civilian faction 0). */
|
||||
inline constexpr uint8 DisguisedTeamId = 0x00;
|
||||
}
|
||||
|
||||
// ─── Stimulus Tags ──────────────────────────────────────────────────────────
|
||||
|
||||
namespace PS_AI_Behavior_Tags
|
||||
{
|
||||
/** Tag for enemy gunfire noise events. */
|
||||
inline const FName EnemyFire = TEXT("EnemyFire");
|
||||
|
||||
/** Tag for player/protector gunfire noise events. */
|
||||
inline const FName PlayerFire = TEXT("PlayerFire");
|
||||
}
|
||||
|
||||
namespace PS_AI_Behavior_Tags_Internal
|
||||
{
|
||||
/** Returns true if the tag is any gunfire tag (EnemyFire or PlayerFire). */
|
||||
inline bool IsGunfire(const FName& Tag)
|
||||
{
|
||||
return Tag == PS_AI_Behavior_Tags::EnemyFire || Tag == PS_AI_Behavior_Tags::PlayerFire;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Blackboard Key Names ───────────────────────────────────────────────────
|
||||
|
||||
namespace PS_AI_Behavior_BB
|
||||
@ -95,4 +184,5 @@ namespace PS_AI_Behavior_BB
|
||||
inline const FName HomeLocation = TEXT("HomeLocation");
|
||||
inline const FName CurrentSpline = TEXT("CurrentSpline");
|
||||
inline const FName SplineProgress = TEXT("SplineProgress");
|
||||
inline const FName CombatSubState = TEXT("CombatSubState");
|
||||
}
|
||||
|
||||
@ -141,4 +141,30 @@ public:
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
bool CanBehaviorAttack(AActor* Target) const;
|
||||
|
||||
// ─── Combat Style ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get this NPC's combat type (Melee or Ranged).
|
||||
* Depends on the Pawn's current weapon — implement on your Character.
|
||||
* Melee NPCs rush the target; Ranged NPCs use cover and maintain distance.
|
||||
* Default: Melee.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
EPS_AI_Behavior_CombatType GetBehaviorCombatType() const;
|
||||
|
||||
// ─── Actor Resolution ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the actor that represents the threat source for this Pawn.
|
||||
* Called by the perception system to resolve weapons, VR tracked actors, etc.
|
||||
* to the actual actor that NPCs should target and flee from.
|
||||
*
|
||||
* Default: returns Self (the Pawn itself).
|
||||
* Override in VR to return the tracked body actor instead of the static Pawn root.
|
||||
*
|
||||
* @return The actor to use as threat source / target.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
AActor* GetBehaviorThreatActor() const;
|
||||
};
|
||||
|
||||
@ -53,6 +53,17 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Perception")
|
||||
bool GetThreatLocation(FVector& OutLocation);
|
||||
|
||||
/**
|
||||
* Get the location of a perceived gunshot stimulus (any team affiliation).
|
||||
* Used to set ThreatLocation when a gunshot is heard but the shooter isn't hostile.
|
||||
* @param OutLocation Filled with the gunshot location if found.
|
||||
* @return True if a gunshot stimulus was found.
|
||||
*/
|
||||
bool GetGunShotStimulusLocation(FVector& OutLocation);
|
||||
|
||||
/** Extract an actor's TeamId (checking controller, then TeamComponent). Returns NoTeam if unresolvable. */
|
||||
static uint8 GetActorTeamId(const AActor* Actor);
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
|
||||
@ -32,6 +32,21 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality")
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian;
|
||||
|
||||
/**
|
||||
* Faction index within the same NPCType.
|
||||
* Used to create rival groups of the same type (e.g. two enemy gangs).
|
||||
*
|
||||
* Same NPCType + same Faction → Friendly (allies)
|
||||
* Same NPCType + different Faction → Hostile (rivals)
|
||||
* Civilian ↔ Protector → always Friendly (regardless of faction)
|
||||
* Everything else → Hostile
|
||||
*
|
||||
* Example: "Gang A" Enemy profile → Faction 0, "Gang B" Enemy profile → Faction 1.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality",
|
||||
meta = (ClampMin = "0", ClampMax = "15"))
|
||||
uint8 Faction = 0;
|
||||
|
||||
// ─── Trait Scores ───────────────────────────────────────────────────
|
||||
|
||||
/** Personality trait scores. Each axis ranges from 0.0 to 1.0. */
|
||||
@ -80,6 +95,21 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat")
|
||||
TArray<EPS_AI_Behavior_TargetType> TargetPriority;
|
||||
|
||||
/**
|
||||
* Minimum attack range (cm). NPC backs away if target is closer than this.
|
||||
* Melee: ~100cm. Ranged: ~600cm.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat", meta = (ClampMin = "0.0"))
|
||||
float MinAttackRange = 100.0f;
|
||||
|
||||
/**
|
||||
* Maximum attack range (cm). NPC advances if target is farther than this.
|
||||
* Between Min and Max, the NPC holds position and attacks.
|
||||
* Melee: ~300cm. Ranged: ~1500cm.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat", meta = (ClampMin = "50.0"))
|
||||
float MaxAttackRange = 300.0f;
|
||||
|
||||
// ─── Movement Speed per State ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@ -94,12 +94,23 @@ bool FPS_AI_Behavior_SplineEdMode::HandleClick(
|
||||
return false; // No ground hit
|
||||
}
|
||||
|
||||
FVector ClickLocation = Hit.ImpactPoint;
|
||||
// Use the ray trace impact point directly — no re-snap needed, this IS the ground
|
||||
FVector ClickLocation = Hit.ImpactPoint + FVector(0, 0, 5.0f); // Slight offset above ground
|
||||
|
||||
// Ctrl+Click on existing spline → select for extension
|
||||
if (Click.IsControlDown())
|
||||
// ─── Without Ctrl: pass click through to editor (select/move actors) ─
|
||||
if (!Click.IsControlDown())
|
||||
{
|
||||
return false; // Let the editor handle selection/dragging
|
||||
}
|
||||
|
||||
// ─── Ctrl+Click: place or extend ─────────────────────────────────────
|
||||
switch (ActiveTool)
|
||||
{
|
||||
case EPS_AI_Behavior_EdModeTool::Spline:
|
||||
{
|
||||
// Ctrl+Click near an existing spline → select for extension
|
||||
if (!ActiveSpline)
|
||||
{
|
||||
// Check if we hit a SplinePath
|
||||
AActor* HitActor = Hit.GetActor();
|
||||
APS_AI_Behavior_SplinePath* HitSpline = Cast<APS_AI_Behavior_SplinePath>(HitActor);
|
||||
|
||||
@ -123,25 +134,16 @@ bool FPS_AI_Behavior_SplineEdMode::HandleClick(
|
||||
SelectSplineForExtension(HitSpline);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snap to ground
|
||||
if (bSnapToGround)
|
||||
{
|
||||
SnapToGround(ClickLocation);
|
||||
}
|
||||
|
||||
// ─── Route to active tool ───────────────────────────────────────────
|
||||
switch (ActiveTool)
|
||||
{
|
||||
case EPS_AI_Behavior_EdModeTool::Spline:
|
||||
// Ctrl+Click elsewhere → add spline point
|
||||
AddPointToSpline(ClickLocation);
|
||||
}
|
||||
break;
|
||||
|
||||
case EPS_AI_Behavior_EdModeTool::CoverPoint:
|
||||
{
|
||||
// Ctrl+Click → place cover point
|
||||
// Cover point faces toward the camera (typical workflow)
|
||||
const FVector CamLoc = InViewportClient->GetViewLocation();
|
||||
const FVector DirToCamera = (CamLoc - ClickLocation).GetSafeNormal2D();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user