Compare commits
4 Commits
69b9844a4b
...
d471714fbd
| Author | SHA1 | Date | |
|---|---|---|---|
| d471714fbd | |||
| af93194f48 | |||
| f5897b46cc | |||
| de3a5310f4 |
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_AIController.h"
|
||||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||||
|
#include "PS_AI_Behavior_PersonalityProfile.h"
|
||||||
#include "PS_AI_Behavior_Statics.h"
|
#include "PS_AI_Behavior_Statics.h"
|
||||||
#include "PS_AI_Behavior_Settings.h"
|
#include "PS_AI_Behavior_Settings.h"
|
||||||
#include "PS_AI_Behavior_Definitions.h"
|
#include "PS_AI_Behavior_Definitions.h"
|
||||||
@ -34,6 +35,8 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||||
|
|
||||||
// Debug: check what perception sees
|
// Debug: check what perception sees
|
||||||
TArray<AActor*> PerceivedActors;
|
TArray<AActor*> PerceivedActors;
|
||||||
Perception->GetCurrentlyPerceivedActors(nullptr, 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);
|
const float StoredThreat = BB->GetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel);
|
||||||
|
|
||||||
// Apply decay when no threat, or take the max of new vs decayed
|
// Apply decay when no threat, or take the max of new vs decayed
|
||||||
const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
|
// Decay rate comes from PersonalityProfile (per-archetype), fallback to global Settings
|
||||||
const float DecayedThreat = FMath::Max(0.0f, StoredThreat - Settings->ThreatDecayRate * DeltaSeconds);
|
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);
|
const float FinalThreat = FMath::Max(RawThreat, DecayedThreat);
|
||||||
|
|
||||||
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
|
||||||
@ -261,7 +269,6 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sync to PersonalityComponent
|
// Sync to PersonalityComponent
|
||||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
|
||||||
if (Personality)
|
if (Personality)
|
||||||
{
|
{
|
||||||
Personality->PerceivedThreatLevel = FinalThreat;
|
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)
|
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
|
||||||
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
|
||||||
|
|
||||||
|
// 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
|
// Check if already in range
|
||||||
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
|
||||||
if (DistToTarget <= Memory->MaxRange)
|
if (DistToTarget <= Memory->MaxRange)
|
||||||
{
|
{
|
||||||
Memory->bInRange = true;
|
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
|
// 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
|
// Face the threat while in cover
|
||||||
AIC->SetFocus(Target);
|
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)
|
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||||
{
|
{
|
||||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||||
|
|||||||
@ -148,34 +148,64 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
|
|||||||
|
|
||||||
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
|
||||||
{
|
{
|
||||||
// Walk to spline first via NavMesh
|
// Try to reach the closest spline point. If blocked, sample other points along the spline.
|
||||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
const float SplineLength = ClosestSpline->GetSplineLength();
|
||||||
SplinePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
|
const int32 NumSamples = 8;
|
||||||
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
|
const float SampleStep = SplineLength / NumSamples;
|
||||||
/*bCanStrafe=*/false);
|
|
||||||
|
|
||||||
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 float D = FMath::Fmod(DistAlongSpline + i * SampleStep, SplineLength);
|
||||||
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
DistancesToTry.Add(D);
|
||||||
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
|
||||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
|
||||||
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
|
||||||
return EBTNodeResult::Succeeded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
float ReachableDist = -1.0f;
|
||||||
|
FVector ReachablePoint = FVector::ZeroVector;
|
||||||
|
|
||||||
|
for (float TestDist : DistancesToTry)
|
||||||
{
|
{
|
||||||
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
|
const FVector TestPoint = ClosestSpline->GetWorldLocationAtDistance(TestDist);
|
||||||
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
|
const EPathFollowingRequestResult::Type TestResult = AIC->MoveToLocation(
|
||||||
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
|
TestPoint, AcceptanceRadius, true, true, true, false);
|
||||||
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
|
|
||||||
return EBTNodeResult::Succeeded;
|
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->CurrentSpline = ClosestSpline;
|
||||||
Follower->CurrentDistance = DistAlongSpline;
|
Follower->CurrentDistance = ReachableDist;
|
||||||
|
|
||||||
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
|
||||||
Memory->bMovingToSpline = true;
|
Memory->bMovingToSpline = true;
|
||||||
|
|||||||
@ -207,21 +207,51 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask(
|
|||||||
|
|
||||||
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
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;
|
Memory->bMoveRequested = false;
|
||||||
|
|
||||||
// Crouch at cover if the point requires it
|
// Crouch at cover if the point requires it
|
||||||
APawn* Pawn = AIC->GetPawn();
|
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>() && BB)
|
||||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
|
||||||
{
|
{
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
const APS_AI_Behavior_CoverPoint* CoverPt =
|
||||||
if (BB)
|
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||||
|
if (CoverPt && CoverPt->bCrouch)
|
||||||
{
|
{
|
||||||
const APS_AI_Behavior_CoverPoint* CoverPt =
|
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true);
|
||||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
|
||||||
if (CoverPt && CoverPt->bCrouch)
|
|
||||||
{
|
|
||||||
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->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
|
||||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
|
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
|
||||||
static_cast<uint8>(EPS_AI_Behavior_State::Idle));
|
static_cast<uint8>(EPS_AI_Behavior_State::Idle));
|
||||||
|
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,14 +368,22 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType
|
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType + Faction
|
||||||
if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>())
|
if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
{
|
{
|
||||||
const EPS_AI_Behavior_NPCType OtherNPCType =
|
const EPS_AI_Behavior_NPCType OtherNPCType =
|
||||||
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn));
|
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn));
|
||||||
if (OtherNPCType != EPS_AI_Behavior_NPCType::Any)
|
if (OtherNPCType != EPS_AI_Behavior_NPCType::Any)
|
||||||
{
|
{
|
||||||
OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType);
|
uint8 OtherFaction = 0;
|
||||||
|
if (const auto* OtherPersonality = OtherPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
|
||||||
|
{
|
||||||
|
if (OtherPersonality->Profile)
|
||||||
|
{
|
||||||
|
OtherFaction = OtherPersonality->Profile->Faction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType, OtherFaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
|
|||||||
return ActorAsPawn;
|
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;
|
AActor* Current = Actor;
|
||||||
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
|
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
|
||||||
{
|
{
|
||||||
@ -108,14 +108,29 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
|
|||||||
|
|
||||||
// Try Owner
|
// Try Owner
|
||||||
AActor* OwnerActor = Current->GetOwner();
|
AActor* OwnerActor = Current->GetOwner();
|
||||||
if (!OwnerActor || OwnerActor == Current) break;
|
if (OwnerActor && OwnerActor != Current)
|
||||||
|
|
||||||
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
|
|
||||||
{
|
{
|
||||||
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.
|
// Fallback: could not resolve to a Pawn.
|
||||||
|
|||||||
@ -44,10 +44,9 @@ void UPS_AI_Behavior_PersonalityComponent::BeginPlay()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Defaults — all traits at 0.5
|
// Defaults — all traits at 0.5
|
||||||
for (uint8 i = 0; i <= static_cast<uint8>(EPS_AI_Behavior_TraitAxis::Discipline); ++i)
|
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f);
|
||||||
{
|
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f);
|
||||||
RuntimeTraits.Add(static_cast<EPS_AI_Behavior_TraitAxis>(i), 0.5f);
|
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f);
|
||||||
}
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] No PersonalityProfile assigned — using default traits."),
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] No PersonalityProfile assigned — using default traits."),
|
||||||
*GetOwner()->GetName());
|
*GetOwner()->GetName());
|
||||||
}
|
}
|
||||||
@ -94,13 +93,32 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
PerceivedThreatLevel, Aggressivity, Courage, Caution,
|
PerceivedThreatLevel, Aggressivity, Courage, Caution,
|
||||||
EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh);
|
EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh);
|
||||||
|
|
||||||
// Decision cascade
|
// Decision cascade — with hysteresis to prevent state flickering.
|
||||||
if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
|
// 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;
|
return EPS_AI_Behavior_State::Fleeing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.3f)
|
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;
|
return EPS_AI_Behavior_State::Combat;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,7 @@ UPS_AI_Behavior_PersonalityProfile::UPS_AI_Behavior_PersonalityProfile()
|
|||||||
// Initialize all trait axes to a neutral 0.5
|
// Initialize all trait axes to a neutral 0.5
|
||||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f);
|
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f);
|
||||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f);
|
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f);
|
||||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Loyalty, 0.5f);
|
|
||||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f);
|
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f);
|
||||||
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Discipline, 0.5f);
|
|
||||||
|
|
||||||
// Default target priority: Protector first, then Player, then Civilian
|
// Default target priority: Protector first, then Player, then Civilian
|
||||||
TargetPriority.Add(EPS_AI_Behavior_TargetType::Protector);
|
TargetPriority.Add(EPS_AI_Behavior_TargetType::Protector);
|
||||||
|
|||||||
@ -13,9 +13,11 @@ class UEnvQuery;
|
|||||||
/**
|
/**
|
||||||
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
* BT Task: Move toward the threat actor and delegate combat to the Pawn.
|
||||||
*
|
*
|
||||||
* Queries IPS_AI_Behavior_Interface for CombatType and OptimalAttackRange:
|
* Queries IPS_AI_Behavior_Interface for CombatType (Melee/Ranged).
|
||||||
* - Melee: rush toward target, stop at optimal range.
|
* Attack ranges come from PersonalityProfile (MinAttackRange/MaxAttackRange),
|
||||||
* - Ranged: maintain optimal distance — back away if too close, advance if too far.
|
* with fallback to AttackMoveRadius if no profile is available.
|
||||||
|
* - Melee: rush toward target, continuous pursuit within MinRange.
|
||||||
|
* - Ranged: maintain distance — back away if closer than MinRange, advance if farther than MaxRange.
|
||||||
*
|
*
|
||||||
* Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and
|
* Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and
|
||||||
* BehaviorStopAttack() on abort. The Pawn handles the actual combat
|
* BehaviorStopAttack() on abort. The Pawn handles the actual combat
|
||||||
@ -32,7 +34,7 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
|
|||||||
public:
|
public:
|
||||||
UPS_AI_Behavior_BTTask_Attack();
|
UPS_AI_Behavior_BTTask_Attack();
|
||||||
|
|
||||||
/** Fallback move radius if the Pawn doesn't implement GetBehaviorOptimalAttackRange(). */
|
/** Fallback attack range (cm) used when PersonalityProfile is unavailable. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0"))
|
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0"))
|
||||||
float AttackMoveRadius = 300.0f;
|
float AttackMoveRadius = 300.0f;
|
||||||
|
|
||||||
|
|||||||
@ -43,19 +43,19 @@ public:
|
|||||||
|
|
||||||
// ─── Timing ─────────────────────────────────────────────────────────
|
// ─── Timing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Minimum time (seconds) spent peeking/shooting. */
|
/** Base minimum time (seconds) spent peeking/shooting. Modulated at runtime: Aggressivity increases, Caution decreases. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||||
float PeekDurationMin = 2.0f;
|
float PeekDurationMin = 2.0f;
|
||||||
|
|
||||||
/** Maximum time (seconds) spent peeking/shooting. */
|
/** Base maximum time (seconds) spent peeking/shooting. Modulated at runtime: Aggressivity increases, Caution decreases. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||||
float PeekDurationMax = 5.0f;
|
float PeekDurationMax = 5.0f;
|
||||||
|
|
||||||
/** Minimum time (seconds) spent ducked behind cover. */
|
/** Base minimum time (seconds) spent ducked behind cover. Modulated at runtime: Caution increases, Aggressivity decreases. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||||
float CoverDurationMin = 1.0f;
|
float CoverDurationMin = 1.0f;
|
||||||
|
|
||||||
/** Maximum time (seconds) spent ducked behind cover. */
|
/** Base maximum time (seconds) spent ducked behind cover. Modulated at runtime: Caution increases, Aggressivity decreases. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||||
float CoverDurationMax = 3.0f;
|
float CoverDurationMax = 3.0f;
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ public:
|
|||||||
|
|
||||||
// ─── Advancement ────────────────────────────────────────────────────
|
// ─── Advancement ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Number of peek/duck cycles before advancing to a closer cover. */
|
/** Number of peek/duck cycles before attempting to advance to a closer cover point. Modulated by Aggressivity. */
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "1", ClampMax = "10"))
|
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "1", ClampMax = "10"))
|
||||||
int32 MaxCyclesBeforeAdvance = 3;
|
int32 MaxCyclesBeforeAdvance = 3;
|
||||||
|
|
||||||
|
|||||||
@ -51,8 +51,9 @@ public:
|
|||||||
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
|
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bonus score added to manual CoverPoints over procedural candidates.
|
* Score bonus added to manual CoverPoints (additive, 0-1 range).
|
||||||
* Higher = manual points are strongly preferred.
|
* A manual point with score 0.5 + bonus 0.3 = 0.8 total.
|
||||||
|
* Higher = manual points strongly preferred over procedural candidates.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float ManualPointBonus = 0.3f;
|
float ManualPointBonus = 0.3f;
|
||||||
|
|||||||
@ -33,15 +33,15 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Team ID — determines perception affiliation (Enemy/Friendly/Neutral).
|
* Team ID — determines perception affiliation (Enemy/Friendly/Neutral).
|
||||||
* Auto-assigned from NPCType at possession if left at 255 (NoTeam):
|
* Auto-assigned from NPCType + Faction at possession via MakeTeamId():
|
||||||
* - Civilian = Team 1
|
* - Encoding: high nibble = NPCType, low nibble = Faction
|
||||||
* - Enemy = Team 2
|
* - Civilian F0 = 0x00, Enemy F0 = 0x10, Protector F0 = 0x20
|
||||||
* - Neutral = 255 (NoTeam → perceived as Neutral by everyone)
|
* - Same NPCType + same Faction → Friendly
|
||||||
*
|
* - Same NPCType + different Faction → Hostile (rival gangs)
|
||||||
* Two NPCs with the SAME Team ID → Friendly (ignored by perception).
|
* - Civilian ↔ Protector → always Friendly
|
||||||
* Two NPCs with DIFFERENT Team IDs → Enemy (detected by perception).
|
* - Everything else → Hostile
|
||||||
* A NPC with Team ID 255 → Neutral to everyone.
|
|
||||||
*
|
*
|
||||||
|
* Disguised enemies (hostile=false) use DisguisedTeamId (0x01).
|
||||||
* You can override this in Blueprint or per-instance in the editor.
|
* You can override this in Blueprint or per-instance in the editor.
|
||||||
*/
|
*/
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Team")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Team")
|
||||||
|
|||||||
@ -27,11 +27,11 @@ public:
|
|||||||
|
|
||||||
// ─── Configuration ──────────────────────────────────────────────────
|
// ─── Configuration ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Maximum distance at which the NPC can attack (cm). */
|
/** Maximum distance at which ExecuteAttack() succeeds (cm). Independent from PersonalityProfile ranges used by BTTask_Attack. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "50.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "50.0"))
|
||||||
float AttackRange = 200.0f;
|
float AttackRange = 200.0f;
|
||||||
|
|
||||||
/** Cooldown between attacks (seconds). */
|
/** Minimum time between consecutive ExecuteAttack() calls (seconds). */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.1"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.1"))
|
||||||
float AttackCooldown = 1.5f;
|
float AttackCooldown = 1.5f;
|
||||||
|
|
||||||
|
|||||||
@ -94,11 +94,9 @@ enum class EPS_AI_Behavior_CombatSubState : uint8
|
|||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EPS_AI_Behavior_TraitAxis : uint8
|
enum class EPS_AI_Behavior_TraitAxis : uint8
|
||||||
{
|
{
|
||||||
Courage UMETA(DisplayName = "Courage", ToolTip = "0 = coward, 1 = fearless"),
|
Courage UMETA(DisplayName = "Courage", ToolTip = "0 = coward, 1 = fearless. Gates flee threshold and cover advancement."),
|
||||||
Aggressivity UMETA(DisplayName = "Aggressivity", ToolTip = "0 = peaceful, 1 = violent"),
|
Aggressivity UMETA(DisplayName = "Aggressivity", ToolTip = "0 = peaceful, 1 = violent. Drives attack threshold and Combat/Cover time ratio."),
|
||||||
Loyalty UMETA(DisplayName = "Loyalty", ToolTip = "0 = selfish, 1 = devoted"),
|
Caution UMETA(DisplayName = "Caution", ToolTip = "0 = reckless, 1 = prudent. Drives flee threshold, cover duration, and spline threat avoidance."),
|
||||||
Caution UMETA(DisplayName = "Caution", ToolTip = "0 = reckless, 1 = prudent"),
|
|
||||||
Discipline UMETA(DisplayName = "Discipline", ToolTip = "0 = undisciplined, 1 = disciplined"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── TeamId Encoding ────────────────────────────────────────────────────────
|
// ─── TeamId Encoding ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -79,6 +79,15 @@ public:
|
|||||||
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float AlertThreshold = 0.15f;
|
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) ───────────────────────────────────────
|
// ─── Target Priority (Combat) ───────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user