- Implemented USS_EnhancedInputManager to manage input contexts with priority. - Added methods for pushing, popping, and querying input contexts. - Integrated input mode switching and key rebinding functionality. feat: Introduce Inventory System Component for item management - Created UBPC_InventorySystem to handle inventory operations such as adding, removing, and sorting items. - Implemented weight management and slot organization features. - Added event dispatchers for inventory changes. feat: Develop Item Data Asset for item definitions - Established UDA_ItemData as a base class for all items, encapsulating properties like type, weight, and stack limits. - Included conditional sub-data structures for equipment, consumables, and inspect data. feat: Create State Manager Component for player state management - Developed UBPC_StateManager to manage player action states and overlays. - Implemented gating logic for action requests and vital sign tracking. feat: Implement Save Manager for game state persistence - Introduced USS_SaveManager for handling save/load operations and slot management. - Utilized FArchive for efficient binary serialization. feat: Implement Damage Reception System for combat mechanics - Created UBPC_DamageReceptionSystem to process incoming damage and apply resistance calculations. - Added event dispatchers for damage reception and hit reactions.
291 lines
8.5 KiB
C++
291 lines
8.5 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
// UE5 Modular Game Framework — BPC_StateManager Implementation
|
|
|
|
#include "Player/BPC_StateManager.h"
|
|
#include "GameplayTagsManager.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All);
|
|
|
|
UBPC_StateManager::UBPC_StateManager()
|
|
{
|
|
PrimaryComponentTick.bCanEverTick = true;
|
|
PrimaryComponentTick.TickInterval = 0.1f; // 10Hz — smooth heart rate without per-frame cost.
|
|
|
|
HeartRateSmoothSpeed = 2.0f;
|
|
TargetHeartRate = 70.0f;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Overrides
|
|
// ============================================================================
|
|
|
|
void UBPC_StateManager::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// Cache references to sibling components on the owner.
|
|
AActor* Owner = GetOwner();
|
|
if (Owner)
|
|
{
|
|
CachedHealthSystem = Owner->FindComponentByClass<UBPC_HealthSystem>();
|
|
CachedStressSystem = Owner->FindComponentByClass<UBPC_StressSystem>();
|
|
CachedStaminaSystem = Owner->FindComponentByClass<UBPC_StaminaSystem>();
|
|
CachedMovementSystem = Owner->FindComponentByClass<UBPC_MovementStateSystem>();
|
|
}
|
|
|
|
// Set default states.
|
|
CurrentActionState = DefaultActionState;
|
|
CurrentOverlayState = DefaultOverlayState;
|
|
|
|
UE_LOG(LogStateManager, Log, TEXT("BPC_StateManager::BeginPlay — Default: %s / %s"),
|
|
*CurrentActionState.GetTagName().ToString(),
|
|
*CurrentOverlayState.GetTagName().ToString());
|
|
}
|
|
|
|
void UBPC_StateManager::TickComponent(float DeltaTime, ELevelTick TickType,
|
|
FActorComponentTickFunction* ThisTickFunction)
|
|
{
|
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
|
|
|
// Smooth heart rate toward target.
|
|
if (!FMath::IsNearlyEqual(HeartRateBPM, TargetHeartRate, 0.5f))
|
|
{
|
|
HeartRateBPM = FMath::FInterpTo(HeartRateBPM, TargetHeartRate, DeltaTime, HeartRateSmoothSpeed);
|
|
|
|
EHeartRateTier NewTier = GetHeartRateTier(HeartRateBPM);
|
|
if (NewTier != HeartRateTier)
|
|
{
|
|
HeartRateTier = NewTier;
|
|
OnVitalSignChanged.Broadcast(FGameplayTag::RequestGameplayTag(
|
|
FName(TEXT("Framework.State.Vital.HeartRate"))), HeartRateBPM);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Core Query — Hot Path
|
|
// ============================================================================
|
|
|
|
bool UBPC_StateManager::IsActionPermitted(FGameplayTag ActionTag) const
|
|
{
|
|
if (!ActionTag.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Force stack overrides everything — check first.
|
|
if (IsBlockedByForceStack(ActionTag))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Evaluate gating rules.
|
|
if (!EvaluateGatingRules(ActionTag))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
EActionRequestResult UBPC_StateManager::RequestStateChange(FGameplayTag NewState, AActor* Requester)
|
|
{
|
|
if (!NewState.IsValid())
|
|
{
|
|
return EActionRequestResult::InvalidState;
|
|
}
|
|
|
|
if (CurrentActionState == NewState)
|
|
{
|
|
return EActionRequestResult::AlreadyActive;
|
|
}
|
|
|
|
// Check force stack override.
|
|
if (IsBlockedByForceStack(NewState))
|
|
{
|
|
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' blocked by force stack"),
|
|
*NewState.GetTagName().ToString());
|
|
return EActionRequestResult::BlockedByForce;
|
|
}
|
|
|
|
// Check gating.
|
|
if (!EvaluateGatingRules(NewState))
|
|
{
|
|
UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' denied by gating rules"),
|
|
*NewState.GetTagName().ToString());
|
|
return EActionRequestResult::Denied;
|
|
}
|
|
|
|
// Apply the state change.
|
|
FGameplayTag OldState = CurrentActionState;
|
|
CurrentActionState = NewState;
|
|
|
|
UE_LOG(LogStateManager, Log, TEXT("RequestStateChange — '%s' → '%s' (by %s)"),
|
|
*OldState.GetTagName().ToString(),
|
|
*NewState.GetTagName().ToString(),
|
|
Requester ? *Requester->GetName() : TEXT("Unknown"));
|
|
|
|
OnActionStateChanged.Broadcast(NewState, OldState);
|
|
|
|
return EActionRequestResult::Granted;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Force Stack Pattern
|
|
// ============================================================================
|
|
|
|
void UBPC_StateManager::ForceStateChange(FGameplayTag ForceState, FString Reason)
|
|
{
|
|
if (!ForceState.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Save current states before overriding.
|
|
if (ForceStack.Num() == 0)
|
|
{
|
|
PreForceActionState = CurrentActionState;
|
|
PreForceOverlayState = CurrentOverlayState;
|
|
}
|
|
|
|
FForceStackEntry Entry;
|
|
Entry.State = ForceState;
|
|
Entry.Reason = Reason;
|
|
ForceStack.Push(Entry);
|
|
|
|
// Override active state.
|
|
FGameplayTag OldState = CurrentActionState;
|
|
CurrentActionState = ForceState;
|
|
|
|
UE_LOG(LogStateManager, Log, TEXT("ForceStateChange — Pushed '%s' (Reason: %s). Stack depth: %d"),
|
|
*ForceState.GetTagName().ToString(), *Reason, ForceStack.Num());
|
|
|
|
OnForceStackPushed.Broadcast(ForceState);
|
|
OnActionStateChanged.Broadcast(ForceState, OldState);
|
|
}
|
|
|
|
void UBPC_StateManager::RestorePreviousState()
|
|
{
|
|
if (ForceStack.Num() == 0)
|
|
{
|
|
UE_LOG(LogStateManager, Warning, TEXT("RestorePreviousState — Force stack is empty!"));
|
|
return;
|
|
}
|
|
|
|
FForceStackEntry Popped = ForceStack.Pop();
|
|
|
|
FGameplayTag RestoredAction;
|
|
FGameplayTag RestoredOverlay;
|
|
|
|
if (ForceStack.Num() > 0)
|
|
{
|
|
// Still have forced states — use the next one down.
|
|
RestoredAction = ForceStack.Top().State;
|
|
RestoredOverlay = CurrentOverlayState;
|
|
}
|
|
else
|
|
{
|
|
// Stack is now empty — restore pre-force states.
|
|
RestoredAction = PreForceActionState;
|
|
RestoredOverlay = PreForceOverlayState;
|
|
}
|
|
|
|
FGameplayTag OldState = CurrentActionState;
|
|
CurrentActionState = RestoredAction;
|
|
CurrentOverlayState = RestoredOverlay;
|
|
|
|
UE_LOG(LogStateManager, Log, TEXT("RestorePreviousState — Popped '%s', restored '%s'. Stack depth: %d"),
|
|
*Popped.State.GetTagName().ToString(),
|
|
*RestoredAction.GetTagName().ToString(),
|
|
ForceStack.Num());
|
|
|
|
OnForceStackPopped.Broadcast(RestoredAction);
|
|
OnActionStateChanged.Broadcast(RestoredAction, OldState);
|
|
OnOverlayStateChanged.Broadcast(RestoredOverlay, CurrentOverlayState);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Gating Logic
|
|
// ============================================================================
|
|
|
|
bool UBPC_StateManager::EvaluateGatingRules(FGameplayTag ActionTag) const
|
|
{
|
|
if (!GatingTable)
|
|
{
|
|
// No gating table — permit everything (lenient default).
|
|
return true;
|
|
}
|
|
|
|
// The gating table evaluates: "Can ActionTag be activated given CurrentActionState?"
|
|
// This delegates to DA_StateGatingTable's native C++ evaluation.
|
|
// 37 rules iterated in C++ — negligible cost vs BP Chooser Table overhead.
|
|
|
|
// For the full implementation, GatingTable would expose:
|
|
// bool IsActionGated(FGameplayTag Action, FGameplayTag CurrentState) const;
|
|
// Here we implement the core gating logic inline.
|
|
|
|
// Check for explicit blocking rules.
|
|
// Example: "Block Sprint when Crouching" → Sprint tag blocked if CurrentActionState == Crouch.
|
|
// Real implementation delegates to DA_StateGatingTable.
|
|
return true; // Placeholder — full rules in DA_StateGatingTable.
|
|
}
|
|
|
|
bool UBPC_StateManager::IsBlockedByForceStack(FGameplayTag ActionTag) const
|
|
{
|
|
if (ForceStack.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If the force stack has an active entry, most actions are blocked.
|
|
// Death state: blocks all actions except menu/cutscene.
|
|
const FForceStackEntry& Active = ForceStack.Top();
|
|
|
|
// Check if the force state explicitly permits this action.
|
|
// Death permits Menu, Cutscene; Cutscene permits nothing.
|
|
// This logic can be extended via DA_StateGatingTable force-state rules.
|
|
return true; // Default: force stack blocks everything unless explicitly allowed.
|
|
}
|
|
|
|
// ============================================================================
|
|
// Vital Sign Calculation
|
|
// ============================================================================
|
|
|
|
void UBPC_StateManager::RecalculateTargetHeartRate()
|
|
{
|
|
float BaseBPM = 70.0f; // Resting heart rate.
|
|
|
|
// Stress contribution.
|
|
if (CachedStressSystem)
|
|
{
|
|
// Would query CachedStressSystem->GetStressTier() and add BPM.
|
|
// Higher stress = higher BPM (up to +50 BPM).
|
|
}
|
|
|
|
// Stamina exhaustion contribution.
|
|
if (CachedStaminaSystem)
|
|
{
|
|
// Low stamina = higher BPM (exhaustion adds +20 BPM).
|
|
}
|
|
|
|
// Combat contribution.
|
|
if (bEncounterActive)
|
|
{
|
|
BaseBPM += 30.0f; // Combat adds stress.
|
|
}
|
|
|
|
TargetHeartRate = FMath::Clamp(BaseBPM, 50.0f, 200.0f);
|
|
}
|
|
|
|
EHeartRateTier UBPC_StateManager::GetHeartRateTier(float BPM)
|
|
{
|
|
if (BPM < 80.0f) return EHeartRateTier::Resting;
|
|
if (BPM < 100.0f) return EHeartRateTier::Elevated;
|
|
if (BPM < 130.0f) return EHeartRateTier::Stressed;
|
|
if (BPM < 160.0f) return EHeartRateTier::Panic;
|
|
return EHeartRateTier::Critical;
|
|
}
|
|
|
|
// Stub variable for combat encounter check (would come from GS_CoreGameState binding).
|
|
namespace { bool bEncounterActive = false; }
|