# 15 — Player Metrics Tracker (`BPC_PlayerMetricsTracker`) ## Purpose Records and exposes player behaviour telemetry throughout a play session. Metrics feed into the adaptive difficulty system, achievement tracking, end-game stats screen, and narrative pacing adjustments. This is a pure data-gathering component — it never blocks or modifies gameplay directly. ## Dependencies - **Requires:** `GI_GameFramework` (session start/end events), `DA_GameDifficulty` (adaptive thresholds) - **Required By:** `BPC_AdaptiveDifficulty` (consumes metrics), `WBP_StatsScreen` (end-game display), `GM_CoreGameMode` (death/checkpoint metrics) - **Engine/Plugin Requirements:** `FTimerHandle` for play time tracking ## Class Info | Property | Value | |----------|-------| | **Parent Class** | `ActorComponent` | | **Class Type** | Blueprint Component | | **Asset Path** | `Content/Framework/Player/BPC_PlayerMetricsTracker` | | **Implements Interfaces** | `I_Persistable` (save/load metrics) | --- ## 1. Enums ### `E_MetricEventType` | Value | Description | |-------|-------------| | `Death = 0` | Player died | | `HideStarted = 1` | Player entered a hide spot | | `HideEnded = 2` | Player exited a hide spot | | `ItemCollected = 3` | Player picked up an item | | `EnemyEncountered = 4` | Player entered an enemy detection range | | `EnemyEvaded = 5` | Player escaped enemy detection | | `StressPeakReached = 6` | Stress tier exceeded Distressed | | `AreaExplored = 7` | Player entered a new area/chapter | | `NarrativeEvent = 8` | Story beat triggered | | `CombatEvent = 9` | Player used a weapon or was attacked | --- ## 2. Structs ### `S_BehaviourEvent` | Field | Type | Description | |-------|------|-------------| | `Timestamp` | `Float` | Game time in seconds when event occurred | | `EventType` | `E_MetricEventType` | What happened | | `ContextTag` | `GameplayTag` | Tagged context for filtering | | `FloatValue` | `Float` | Numeric payload (e.g. distance, intensity) | | `StringValue` | `String` | Text payload (e.g. location name, enemy ID) | | `Location` | `Vector` | World location where event occurred | ### `S_MetricsSnapshot` | Field | Type | Description | |-------|------|-------------| | `TotalPlayTime` | `Float` | Total seconds played | | `TimesDied` | `Integer` | Total deaths this session | | `TimesHid` | `Integer` | Total hide events | | `TotalDistanceWalked` | `Float` | Cumulative walking distance (cm) | | `TotalDistanceSprinted` | `Float` | Cumulative sprinting distance (cm) | | `ItemsCollected` | `Integer` | Total items picked up | | `UniqueItemsCollected` | `Set` | Names of unique items collected | | `EnemiesEncountered` | `Integer` | Total unique enemy encounters | | `EnemiesEvaded` | `Integer` | Total successful evasions | | `PeakStressTier` | `Float` | Highest stress tier reached (as float) | | `StressPeakCount` | `Integer` | Times stress exceeded Distressed threshold | | `NarrativeBeatsCompleted` | `Integer` | Story triggers activated | | `AreasExplored` | `Set` | Unique areas/chapters visited | | `CombatEvents` | `Integer` | Total combat interactions | ### `S_TimeSegment` | Field | Type | Description | |-------|------|-------------| | `SegmentLabel` | `String` | Label for this time segment | | `StartTime` | `Float` | Game time at segment start | | `EndTime` | `Float` | Game time at segment end | | `StressAverage` | `Float` | Average stress during segment | | `MetricsDelta` | `S_MetricsSnapshot` | Metrics accumulated in this segment | --- ## 3. Variables ### Configuration (Instance Editable, Expose On Spawn) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `bTrackDetailedEvents` | `Boolean` | `true` | `Metrics Config` | If true, store full `S_BehaviourEvent` history | | `MaxEventHistory` | `Integer` | `5000` | `Metrics Config` | Cap on stored events to prevent memory bloat | | `bAutoSaveMetrics` | `Boolean` | `true` | `Metrics Config` | Auto-save metrics via I_Persistable on quit | | `SnapshotInterval` | `Float` | `60.0` | `Metrics Config` | Seconds between automatic snapshot writes | ### Computed / Runtime (Public, Blueprint Read Only) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `CurrentSnapshot` | `S_MetricsSnapshot` | `-` | `Runtime Metrics` | Live accumulated metrics this session | | `bIsTrackingSession` | `Boolean` | `false` | `Runtime Metrics` | Whether currently in a tracked session | | `TimeSegmentHistory` | `Array` | `[]` | `Runtime Metrics` | Historical segments for analysis | | `CurrentSegment` | `S_TimeSegment` | `-` | `Runtime Metrics` | Active time segment | ### Internal (Private / Protected, No Expose) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `SessionStartTime` | `Float` | `0.0` | `Timing` | Game time when session started | | `CachedEventHistory` | `Array` | `[]` | `History` | Full event history (if tracking enabled) | | `LastPosition` | `Vector` | `(0,0,0)` | `Tracking` | Previous frame position for distance calc | | `SnapshotTimerHandle` | `FTimerHandle` | `-` | `Timing` | Timer for automatic snapshots | ### Replicated (if multiplayer) | Variable | Type | Condition | Description | |----------|------|-----------|-------------| | `CurrentSnapshot` | `S_MetricsSnapshot` | `Replicated` | Synced session metrics | --- ## 4. Functions ### Public Functions #### `StartSession` → `void` - **Description:** Begins a new metrics tracking session. Resets snapshot. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `SegmentLabel` | `String` | Label for initial segment (e.g. "Chapter 1") | - **Blueprint Authority:** Any - **Flow:** 1. Set SessionStartTime = Current Game Time 2. Reset CurrentSnapshot to zero values 3. Initialize CurrentSegment with label and start time 4. Set bIsTrackingSession = true 5. Start snapshot timer 6. Fire OnSessionStarted #### `EndSession` → `S_MetricsSnapshot` - **Description:** Ends the tracking session and returns final snapshot. - **Parameters:** None - **Flow:** 1. Finalize CurrentSegment end time 2. Finalize CurrentSnapshot.PlayTime 3. Add CurrentSegment to TimeSegmentHistory 4. Set bIsTrackingSession = false 5. Clear snapshot timer 6. Fire OnSessionEnded 7. Return CurrentSnapshot #### `RecordEvent` → `void` - **Description:** Records a single behaviour event into history and updates snapshot. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `EventType` | `E_MetricEventType` | Type of event | | `ContextTag` | `GameplayTag` | Filtering tag | | `FloatValue` | `Float` | Numeric payload | | `StringValue` | `String` | Text payload | - **Flow:** 1. If not bIsTrackingSession: return 2. Create S_BehaviourEvent with current timestamp 3. If bTrackDetailedEvents and event count < MaxEventHistory: - Add event to CachedEventHistory 4. Update CurrentSnapshot based on EventType: - Death: TimesDied++ - HideStarted: TimesHid++ - ItemCollected: ItemsCollected++, add name to UniqueItemsCollected - EnemyEncountered: EnemiesEncountered++ - EnemyEvaded: EnemiesEvaded++ - StressPeakReached: StressPeakCount++, update PeakStressTier if higher - NarrativeEvent: NarrativeBeatsCompleted++ - AreaExplored: add to AreasExplored - CombatEvent: CombatEvents++ 5. Fire OnEventRecorded #### `RecordDistanceTraveled` → `void` - **Description:** Called by movement system or tick to log distance. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `DistanceDelta` | `Float` | Distance moved this frame | | `bWasSprinting` | `Boolean` | Whether player was sprinting | - **Flow:** 1. If bWasSprinting: CurrentSnapshot.TotalDistanceSprinted += Delta 2. Else: CurrentSnapshot.TotalDistanceWalked += Delta #### `GetMetricsSnapshot` → `S_MetricsSnapshot` - **Description:** Returns a copy of the current live snapshot. - **Parameters:** None - **Flow:** Return CurrentSnapshot #### `GetEventHistory` → `Array` - **Description:** Returns recorded event history. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `bFiltered` | `Boolean` | If true, apply filter | | `FilterType` | `E_MetricEventType` | Only return events of this type | - **Flow:** 1. If bFiltered: return filtered CachedEventHistory by FilterType 2. Else: return CachedEventHistory #### `GetPlayTimeFormatted` → `String` - **Description:** Returns total play time as formatted string (HH:MM:SS). - **Parameters:** None - **Flow:** Format SecondsToTimeString(SessionDuration) ### Protected / Private Functions #### `OnTickDistance (Tick)` → `void` - **Description:** Each tick, compute distance from LastPosition. Only active when tracking. - **Flow:** 1. If not bIsTrackingSession: return 2. Get current actor location 3. Delta = Distance(LastPosition, CurrentLocation) 4. If Delta < 10.0: skip (noise threshold) 5. Check movement mode from BPC_MovementStateSystem 6. Call RecordDistanceTraveled(Delta, bWasSprinting) 7. Update LastPosition = CurrentLocation #### `CreateSnapshot` → `void` - **Description:** Timer callback to create periodic snapshots. - **Flow:** 1. Copy CurrentSnapshot as snapshot 2. Finalize CurrentSegment end time 3. Store snapshot delta in CurrentSegment.MetricsDelta 4. Add CurrentSegment to TimeSegmentHistory 5. Start new CurrentSegment --- ## 5. Event Dispatchers | Dispatcher | Parameters | Bind Access | Description | |------------|-----------|-------------|-------------| | `OnSessionStarted` | `String SegmentLabel` | `Public` | Fired when a tracking session begins | | `OnSessionEnded` | `S_MetricsSnapshot FinalSnapshot` | `Public` | Fired when a tracking session ends | | `OnEventRecorded` | `S_BehaviourEvent Event` | `Public` | Fired each time an event is recorded | | `OnMetricsThresholdReached` | `E_MetricEventType EventType`, `int CurrentValue`, `int Threshold` | `Public` | Fired when a metric crosses a threshold | | `OnSnapshotCreated` | `S_TimeSegment Segment` | `Public` | Fired when a periodic snapshot is written | --- ## 6. Overridden Events / Custom Events ### Event: `BeginPlay` - **Description:** Cache owner reference, start session if game has started. - **Flow:** 1. Get owning pawn 2. Bind to GI_GameFramework.OnGamePhaseChanged 3. If game phase is Playing: StartSession("Initial") ### Custom Event: `OnGamePhaseChangedHandler` - **Description:** Auto-start/stop session based on game phase. - **Flow:** 1. If NewPhase == Playing: StartSession("Resume") 2. If NewPhase == Menu / Transition / Ended: EndSession() --- ## 7. Blueprint Graph Logic Flow ```mermaid flowchart TD A[RecordEvent called] --> B{Is session active?} B -->|No| C[Return] B -->|Yes| D[Build S_BehaviourEvent] D --> E{Has history space?} E -->|Yes| F[Append to CachedEventHistory] E -->|No| G[Skip history store] G --> H[Update CurrentSnapshot counters] F --> H H --> I{Threshold crossed?} I -->|Yes| J[Fire OnMetricsThresholdReached] I -->|No| K[Fire OnEventRecorded] J --> K L[Tick: RecordDistanceTraveled] --> M[Get current location] M --> N[Distance from LastPosition] N --> O{Delta > 10 cm?} O -->|No| P[Skip] O -->|Yes| Q[Update snapshot distance] Q --> R[Set LastPosition] S[SnapshotTimer fires] --> T[Copy CurrentSnapshot] T --> U[Write segment to history] U --> V[Fire OnSnapshotCreated] ``` --- ## 8. Communication Matrix | Who Talks | How | What Is Sent | |-----------|-----|-------------| | `BPC_PlayerMetricsTracker` | `Direct call (other)` | `GetMetricsSnapshot` -> `BPC_AdaptiveDifficulty` | | `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnEventRecorded` -> `BPC_AdaptiveDifficulty` (real-time feed) | | `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnSessionEnded` -> `WBP_StatsScreen` (end-game display) | | `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnMetricsThresholdReached` -> `GM_CoreGameMode` (achievements) | | `BPC_HealthSystem` | `Direct` | Calls `RecordEvent(Death)` on player death | | `BPC_HidingSystem` | `Direct` | Calls `RecordEvent(HideStarted/HideEnded)` | | `BPC_StressSystem` | `Direct` | Calls `RecordEvent(StressPeakReached)` | | `GI_GameFramework` | `Dispatcher` | `OnGamePhaseChanged` -> `BPC_PlayerMetricsTracker` (start/stop) | | `BPC_NarrativeManager` | `Direct` | Calls `RecordEvent(NarrativeEvent)` on story beat | | `BPC_WeaponSystem` | `Direct` | Calls `RecordEvent(CombatEvent)` on attack | | `BPC_InventoryManager` | `Direct` | Calls `RecordEvent(ItemCollected)` on pickup | --- ## 9. Validation / Testing Checklist - [ ] StartSession resets all counters to zero - [ ] Distance tracking correctly accumulates walking vs sprinting - [ ] Events are capped at MaxEventHistory and do not overflow - [ ] Snapshot timer creates periodic snapshots at correct interval - [ ] EndSession returns accurate final snapshot - [ ] Filtered event history returns only matching event types - [ ] Threshold dispatchers fire at correct metric values - [ ] I_Persistable save/load preserves snapshot across sessions - [ ] Edge case: Rapid events within one frame all recorded - [ ] Edge case: Session ends with zero events — snapshot is valid zero-state - [ ] Edge case: Distance tracking ignores teleportation (delta > 5000 cm) - [ ] Formatted play time displays correctly for all durations --- ## 10. Reuse Notes - The metrics snapshot is designed to be consumed by `BPC_AdaptiveDifficulty` for dynamic difficulty adjustment. - Event history can be serialised to disk via `I_Persistable` for cross-session analytics. - The same `BPC_PlayerMetricsTracker` can be attached to any character blueprint — not just the player. - Threshold values are not defined here; they live in `DA_GameDifficulty` or `DA_AdaptiveSettings`. - For debugging, call `GetEventHistory(false)` to dump all events to log. --- *Blueprint Spec: Player Metrics Tracker. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*