Compare commits
5 Commits
391a35ac2c
...
69b9844a4b
| Author | SHA1 | Date | |
|---|---|---|---|
| 69b9844a4b | |||
| 78149fffcd | |||
| b60086d107 | |||
| d6325b373d | |||
| 011bfcf62a |
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.
Binary file not shown.
@ -2,6 +2,8 @@
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_PerceptionComponent.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
@ -23,17 +25,32 @@ bool UPS_AI_Behavior_BTDecorator_IsCoverNeeded::CalculateRawConditionValue(
|
||||
AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!ThreatActor) return false;
|
||||
|
||||
// If the target doesn't implement the interface, assume dangerous (safe default)
|
||||
APawn* ThreatPawn = Cast<APawn>(ThreatActor);
|
||||
// Resolve ThreatActor to owning Pawn (AimTargetActor → Character)
|
||||
APawn* ThreatPawn = UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(ThreatActor);
|
||||
if (!ThreatPawn || !ThreatPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
return true;
|
||||
return true; // Can't resolve → assume dangerous (safe default)
|
||||
}
|
||||
|
||||
const EPS_AI_Behavior_NPCType TargetType =
|
||||
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(ThreatPawn);
|
||||
|
||||
return DangerousTargetTypes.Contains(TargetType);
|
||||
if (!DangerousTargetTypes.Contains(TargetType))
|
||||
{
|
||||
return false; // Target is not dangerous → no cover needed
|
||||
}
|
||||
|
||||
// Check personality-driven combat/cover cycle timer
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
if (const UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||
{
|
||||
return Personality->ShouldPreferCover();
|
||||
}
|
||||
}
|
||||
|
||||
return true; // No personality → default to cover
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTDecorator_IsCoverNeeded::GetStaticDescription() const
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTTask_FleeFrom.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
#include "NavigationSystem.h"
|
||||
@ -20,6 +21,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FleeFrom::ExecuteTask(
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return EBTNodeResult::Failed;
|
||||
|
||||
// Stand up if crouching (e.g. was hiding at a cover point before fleeing)
|
||||
APawn* FleePawn = AIC->GetPawn();
|
||||
if (FleePawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(FleePawn, false);
|
||||
}
|
||||
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_String.h"
|
||||
#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h"
|
||||
|
||||
APS_AI_Behavior_AIController::APS_AI_Behavior_AIController()
|
||||
{
|
||||
@ -184,6 +185,12 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
|
||||
ThreatPawnNameEntry.EntryName = PS_AI_Behavior_BB::ThreatPawnName;
|
||||
ThreatPawnNameEntry.KeyType = NewObject<UBlackboardKeyType_String>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(ThreatPawnNameEntry);
|
||||
|
||||
// PreferCover (bool: personality-driven combat/cover cycle)
|
||||
FBlackboardEntry PreferCoverEntry;
|
||||
PreferCoverEntry.EntryName = PS_AI_Behavior_BB::PreferCover;
|
||||
PreferCoverEntry.KeyType = NewObject<UBlackboardKeyType_Bool>(BlackboardAsset);
|
||||
BlackboardAsset->Keys.Add(PreferCoverEntry);
|
||||
}
|
||||
|
||||
UBlackboardComponent* RawBBComp = nullptr;
|
||||
@ -237,6 +244,18 @@ void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewSta
|
||||
}
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, NewVal);
|
||||
|
||||
// ─── Leaving cover/hiding: stand up ─────────────────────────
|
||||
const EPS_AI_Behavior_State OldState = static_cast<EPS_AI_Behavior_State>(OldVal);
|
||||
if ((OldState == EPS_AI_Behavior_State::Fleeing || OldState == EPS_AI_Behavior_State::TakingCover)
|
||||
&& NewState != EPS_AI_Behavior_State::Fleeing && NewState != EPS_AI_Behavior_State::TakingCover)
|
||||
{
|
||||
APawn* MyPawn = GetPawn();
|
||||
if (MyPawn && MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(MyPawn, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dead: shut down all AI systems ─────────────────────────
|
||||
if (NewState == EPS_AI_Behavior_State::Dead)
|
||||
{
|
||||
|
||||
@ -280,7 +280,8 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor(
|
||||
const AAIController* OwnerAIC = Cast<AAIController>(GetOwner());
|
||||
const APawn* OwnerPawn = OwnerAIC ? OwnerAIC->GetPawn() : nullptr;
|
||||
|
||||
if (OwnerPawn)
|
||||
// Only run on server — TActorIterator results differ on clients
|
||||
if (OwnerPawn && OwnerPawn->HasAuthority())
|
||||
{
|
||||
const EPS_AI_Behavior_TargetType TopPriority = TargetPriority[0];
|
||||
const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
|
||||
|
||||
@ -102,11 +102,6 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
|
||||
if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.3f)
|
||||
{
|
||||
// Cautious NPCs prefer cover over direct combat
|
||||
if (Caution > 0.6f)
|
||||
{
|
||||
return EPS_AI_Behavior_State::TakingCover;
|
||||
}
|
||||
return EPS_AI_Behavior_State::Combat;
|
||||
}
|
||||
|
||||
@ -132,7 +127,87 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
|
||||
EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
||||
{
|
||||
const EPS_AI_Behavior_State NewState = EvaluateReaction();
|
||||
// Only server can change replicated state
|
||||
if (!GetOwner() || !GetOwner()->HasAuthority())
|
||||
{
|
||||
return CurrentState;
|
||||
}
|
||||
|
||||
EPS_AI_Behavior_State NewState = EvaluateReaction();
|
||||
|
||||
// ─── Combat/Cover cycle timer ──────────────────────────────────
|
||||
// While in Combat, alternate bPreferCover flag based on personality ratio.
|
||||
// Writes PreferCover bool to Blackboard so BT decorators can react via observer aborts.
|
||||
if (NewState == EPS_AI_Behavior_State::Combat)
|
||||
{
|
||||
if (!bCombatCoverCycleActive)
|
||||
{
|
||||
// Start the cycle — begin with cover if cautious, attack if aggressive
|
||||
bCombatCoverCycleActive = true;
|
||||
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
const float CombatRatio = (Aggressivity + Caution > 0.0f)
|
||||
? Aggressivity / (Aggressivity + Caution) : 0.5f;
|
||||
bPreferCover = (CombatRatio <= 0.5f);
|
||||
CombatCoverTimer = CalculatePhaseDuration(
|
||||
bPreferCover ? EPS_AI_Behavior_State::TakingCover : EPS_AI_Behavior_State::Combat);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Combat/Cover cycle started: %s for %.1fs"),
|
||||
*GetOwner()->GetName(), bPreferCover ? TEXT("Cover") : TEXT("Attack"), CombatCoverTimer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tick down the timer (ApplyReaction is called every ~0.5s by the BT service)
|
||||
CombatCoverTimer -= 0.5f;
|
||||
|
||||
if (CombatCoverTimer <= 0.0f)
|
||||
{
|
||||
// Switch phase
|
||||
bPreferCover = !bPreferCover;
|
||||
CombatCoverTimer = CalculatePhaseDuration(
|
||||
bPreferCover ? EPS_AI_Behavior_State::TakingCover : EPS_AI_Behavior_State::Combat);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Combat/Cover cycle switch: → %s for %.1fs"),
|
||||
*GetOwner()->GetName(), bPreferCover ? TEXT("Cover") : TEXT("Attack"), CombatCoverTimer);
|
||||
}
|
||||
}
|
||||
|
||||
// Write to BB so Blackboard-based decorators can observe the change
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (AAIController* AIC = Cast<AAIController>(Cast<APawn>(Owner) ?
|
||||
Cast<APawn>(Owner)->GetController() : nullptr))
|
||||
{
|
||||
if (UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||
{
|
||||
BB->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, bPreferCover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Left combat → reset cycle
|
||||
if (bCombatCoverCycleActive)
|
||||
{
|
||||
bCombatCoverCycleActive = false;
|
||||
bPreferCover = false;
|
||||
CombatCoverTimer = 0.0f;
|
||||
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (AAIController* AIC = Cast<AAIController>(Cast<APawn>(Owner) ?
|
||||
Cast<APawn>(Owner)->GetController() : nullptr))
|
||||
{
|
||||
if (UBlackboardComponent* BB = AIC->GetBlackboardComponent())
|
||||
{
|
||||
BB->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NewState != CurrentState)
|
||||
{
|
||||
const EPS_AI_Behavior_State OldState = CurrentState;
|
||||
@ -149,8 +224,40 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
||||
return CurrentState;
|
||||
}
|
||||
|
||||
float UPS_AI_Behavior_PersonalityComponent::CalculatePhaseDuration(EPS_AI_Behavior_State Phase) const
|
||||
{
|
||||
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
const float CycleDuration = Profile ? Profile->CombatCoverCycleDuration : 15.0f;
|
||||
|
||||
const float Sum = Aggressivity + Caution;
|
||||
const float CombatRatio = (Sum > 0.0f) ? Aggressivity / Sum : 0.5f;
|
||||
|
||||
float Duration;
|
||||
if (Phase == EPS_AI_Behavior_State::Combat)
|
||||
{
|
||||
Duration = CycleDuration * CombatRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
Duration = CycleDuration * (1.0f - CombatRatio);
|
||||
}
|
||||
|
||||
// Clamp minimum 2s, add ±20% jitter
|
||||
Duration = FMath::Max(Duration, 2.0f);
|
||||
Duration *= FMath::RandRange(0.8f, 1.2f);
|
||||
|
||||
return Duration;
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_PersonalityComponent::ForceState(EPS_AI_Behavior_State NewState)
|
||||
{
|
||||
// Only server can change replicated state
|
||||
if (!GetOwner() || !GetOwner()->HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NewState != CurrentState)
|
||||
{
|
||||
const EPS_AI_Behavior_State OldState = CurrentState;
|
||||
|
||||
@ -189,4 +189,5 @@ namespace PS_AI_Behavior_BB
|
||||
inline const FName CombatSubState = TEXT("CombatSubState");
|
||||
inline const FName LastKnownTargetPosition = TEXT("LastKnownTargetPosition");
|
||||
inline const FName ThreatPawnName = TEXT("ThreatPawnName"); // Debug: name of the owning Pawn behind ThreatActor
|
||||
inline const FName PreferCover = TEXT("PreferCover"); // Bool: personality-driven cover preference cycle
|
||||
}
|
||||
|
||||
@ -109,6 +109,14 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
EPS_AI_Behavior_NPCType GetNPCType() const;
|
||||
|
||||
/**
|
||||
* Whether the NPC currently prefers cover over direct attack.
|
||||
* Driven by the Combat/Cover cycle timer based on Aggressivity vs Caution.
|
||||
* Used by the IsCoverNeeded BT decorator.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Personality")
|
||||
bool ShouldPreferCover() const { return bPreferCover; }
|
||||
|
||||
// ─── Replication ────────────────────────────────────────────────────
|
||||
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
@ -130,6 +138,20 @@ private:
|
||||
*/
|
||||
void HandleStateChanged(EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState);
|
||||
|
||||
// ─── Combat/Cover Cycle Timer ──────────────────────────────────────
|
||||
|
||||
/** Countdown timer for the current Combat or TakingCover phase. */
|
||||
float CombatCoverTimer = 0.0f;
|
||||
|
||||
/** Whether the combat/cover cycle timer is active. */
|
||||
bool bCombatCoverCycleActive = false;
|
||||
|
||||
/** Current cover preference — toggled by the cycle timer. */
|
||||
bool bPreferCover = false;
|
||||
|
||||
/** Calculate the duration for a Combat or TakingCover phase based on personality. */
|
||||
float CalculatePhaseDuration(EPS_AI_Behavior_State Phase) const;
|
||||
|
||||
/** Draw floating debug text above the NPC's head. */
|
||||
void DrawDebugInfo() const;
|
||||
};
|
||||
|
||||
@ -110,6 +110,18 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat", meta = (ClampMin = "50.0"))
|
||||
float MaxAttackRange = 300.0f;
|
||||
|
||||
// ─── Combat/Cover Cycle ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Base duration (seconds) of the Combat↔TakingCover cycle.
|
||||
* The actual time in each state is proportional to Aggressivity vs Caution:
|
||||
* Combat duration = CombatCoverCycleDuration × Aggressivity / (Aggressivity + Caution)
|
||||
* TakingCover duration = CombatCoverCycleDuration × Caution / (Aggressivity + Caution)
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Combat",
|
||||
meta = (ClampMin = "4.0", ClampMax = "60.0"))
|
||||
float CombatCoverCycleDuration = 15.0f;
|
||||
|
||||
// ─── Movement Speed per State ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user