Add personality-driven Combat/Cover cycle with BB PreferCover key
- Add CombatCoverCycleDuration to PersonalityProfile (configurable base duration) - PersonalityComponent cycles bPreferCover based on Aggressivity/Caution ratio - Combat duration = CycleDuration × Aggressivity/(Aggressivity+Caution) - Cover duration = CycleDuration × Caution/(Aggressivity+Caution) - Min 2s per phase, ±20% jitter - Write PreferCover bool to Blackboard for BT decorator observer aborts - IsCoverNeeded decorator checks both target type AND ShouldPreferCover() - Remove TakingCover state from EvaluateReaction — cover is now a sub-mode of Combat - BT uses Blackboard Condition on PreferCover with Observer Aborts=Both Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b60086d107
commit
78149fffcd
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.
@ -3,6 +3,7 @@
|
||||
#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"
|
||||
|
||||
@ -34,7 +35,22 @@ bool UPS_AI_Behavior_BTDecorator_IsCoverNeeded::CalculateRawConditionValue(
|
||||
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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -138,7 +133,81 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction()
|
||||
return CurrentState;
|
||||
}
|
||||
|
||||
const EPS_AI_Behavior_State NewState = EvaluateReaction();
|
||||
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;
|
||||
@ -155,6 +224,32 @@ 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
|
||||
|
||||
@ -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 ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user