# 148 — Haptics Controller (`BPC_HapticsController`) > **Blueprint-Only Implementation** — UE 5.5–5.7 fully supports controller haptics/force feedback from Blueprints. No C++ required. This component wraps UE5's `Play Force Feedback`, `Set Haptics By Value`, and DualSense adaptive trigger APIs behind a GameplayTag-driven event system. --- ## Purpose Abstraction layer for all controller haptic feedback and force feedback effects. Gameplay systems trigger haptics by GameplayTag (e.g., `Haptic.Damage.Heavy`) rather than calling raw UE5 haptic APIs. This component handles platform detection (Xbox rumble vs PS5 DualSense adaptive triggers vs generic PC gamepad), respects accessibility settings (`BPC_AccessibilitySettings.bHapticsEnabled`), and manages effect priority/conflict resolution. ## Dependencies - **Requires:** [`DA_HapticProfile`](docs/blueprints/14-data-assets/121_DA_HapticProfile.md) (effect definitions), [`BPC_AccessibilitySettings`](docs/blueprints/12-settings/104_BPC_AccessibilitySettings.md) (master toggle), [`BPC_StateManager`](docs/blueprints/16-state/130_BPC_StateManager.md) (heart rate for heartbeat haptic), [`BPC_PlatformServiceAbstraction`](docs/blueprints/01-core/150_BPC_PlatformServiceAbstraction.md) (platform detection — replaces own platform enum) - **Required By:** `BPC_HealthSystem` (damage haptics), `BPC_FirearmSystem` (weapon fire kick), `BPC_MeleeSystem` (melee impact), `BPC_PhysicsDragSystem` (grab/release), `BPC_ScareEventSystem` (tension rumble), `BP_ItemPickup` (pickup pulse), `BPC_MovementStateSystem` (footstep rumble via GASP notifies) - **Engine/Plugin Requirements:** Enhanced Input Plugin (controller detection), PlayStation 5 Controller Plugin (DualSense adaptive triggers — optional, graceful fallback) ## Class Info | Property | Value | |----------|-------| | **Parent Class** | `ActorComponent` | | **Class Type** | Blueprint Component | | **Asset Path** | `Content/Framework/Settings/BPC_HapticsController` | | **Implements Interfaces** | None | | **Attachment** | Player Controller (`PC_CoreController`) | --- ## 1. Enums ### `EHapticEvent` | Value | Description | |-------|-------------| | `None = 0` | No haptic effect | | `Damage = 1` | Player takes damage (intensity scales with amount) | | `HeavyDamage = 2` | Critical/major damage hit | | `Heartbeat = 3` | Heartbeat pulse (tempo from BPC_StateManager heart rate) | | `WeaponFire = 4` | Weapon fire kick | | `WeaponReload = 5` | Reload action feedback | | `MeleeImpact = 6` | Melee weapon hit/kill | | `Footstep = 7` | Footstep surface-dependent rumble | | `Explosion = 8` | Nearby explosion or environmental blast | | `PickupItem = 9` | Item picked up | | `DropItem = 10` | Item dropped/discarded | | `GrabObject = 11` | Physics object grabbed | | `ReleaseObject = 12` | Physics object released/thrown | | `ScareEvent = 13` | Jump scare / tension event | | `AmbientPulse = 14` | Low-level ambient tension rumble | | `UI_Confirm = 15` | Menu confirm/select haptic click | | `UI_Navigate = 16` | Menu navigation tick | | `LowHealth = 17` | Health-critical warning pulse | | `StaminaExhausted = 18` | Stamina depleted heavy pulse | | `Death = 19` | Player death rumble | ### `EHapticMotor` | Value | Description | |-------|-------------| | `Left = 0` | Left (low-frequency) motor only | | `Right = 1` | Right (high-frequency) motor only | | `Both = 2` | Both motors simultaneously | ### `EControllerPlatform` *(deprecated — use EPlatformFamily from BPC_PlatformServiceAbstraction. This enum is mapped internally from the unified platform enum.)* | Value | Description | |-------|-------------| | `Unknown = 0` | Platform not yet detected | | `PC_Generic = 1` | PC with generic gamepad (XInput) | | `Xbox = 2` | Xbox Series X\|S / Xbox One controller | | `PS5_DualSense = 3` | PlayStation 5 DualSense controller | | `PS4_DualShock = 4` | PlayStation 4 DualShock 4 controller | --- ## 2. Structs ### `S_HapticRequest` | Field | Type | Description | |-------|------|-------------| | `ProfileTag` | `FGameplayTag` | Which DA_HapticProfile to use | | `EventType` | `EHapticEvent` | Event category | | `IntensityMultiplier` | `Float` | Scale intensity (0.0–2.0, 1.0 = default) | | `DurationOverride` | `Float` | Override duration (-1 = use profile default) | | `Priority` | `Int32` | Higher interrupts lower (0–100) | | `RequestTime` | `Float` | Game time when requested (for cooldown) | --- ## 3. Variables ### Configuration (Instance Editable) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `HapticProfileMap` | `TMap` | `Empty` | `Config` | All haptic profiles loaded at startup | | `bHapticsEnabled` | `Bool` | `true` | `Config` | Master toggle (synced from BPC_AccessibilitySettings) | | `bEnableDualSenseTriggers` | `Bool` | `true` | `Config` | Enable adaptive trigger effects on PS5 | | `bEnableSpeakerAudio` | `Bool` | `true` | `Config` | Enable controller speaker audio on PS5 | | `MinTimeBetweenEffects` | `Float` | `0.05` | `Config` | Minimum seconds between any two effects (prevents rumble spam) | | `HapticIntensityScale` | `Float` | `1.0` | `Config` | Global intensity multiplier (0.0 = off, 1.0 = full) | | `HeartbeatMinBPM` | `Float` | `40.0` | `Config` | Minimum BPM for heartbeat haptic | | `HeartbeatMaxBPM` | `Float` | `180.0` | `Config` | Maximum BPM for heartbeat haptic | ### Internal (Private) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `CurrentPlatform` | `EControllerPlatform` | `Unknown` | `State` | Detected controller platform | | `bIsInitialized` | `Bool` | `false` | `State` | Whether Initialize has completed | | `CachedPlayerController` | `APlayerController` | `None` | `Cache` | Cached owner PlayerController reference | | `ActiveHapticEffect` | `UForceFeedbackEffect` | `None` | `State` | Currently playing effect asset | | `LastPlayTime` | `Float` | `0.0` | `State` | Game time of last played effect | | `PendingRequest` | `S_HapticRequest` | `Empty` | `State` | Currently queued request | | `bHeartbeatActive` | `Bool` | `false` | `State` | Whether heartbeat loop is active | --- ## 4. Functions ### Public Functions #### `Initialize` → `void` - **Description:** Detects controller platform, loads all `DA_HapticProfile` instances into `HapticProfileMap`, caches PlayerController. - **Parameters:** None - **Blueprint Authority:** Local Client Only - **Flow:** 1. Get Owner → Cast to `APlayerController` → Store as `CachedPlayerController` 2. Detect platform: Check `UGameplayStatics::GetPlatformName()` + connected input devices 3. Set `CurrentPlatform` (Xbox, PS5_DualSense, PS4_DualShock, or PC_Generic) 4. Load all `DA_HapticProfile` assets from `Content/Framework/DataAssets/Haptics/` into `HapticProfileMap` 5. If `BPC_AccessibilitySettings` exists on owner → bind to `OnHapticsToggleChanged` 6. Bind to `BPC_StateManager.OnHeartRateChanged` for heartbeat haptic 7. Set `bIsInitialized = true` 8. Broadcast `OnHapticsControllerInitialized` #### `PlayHapticByTag` → `void` - **Description:** Play a haptic effect by its GameplayTag. Main entry point for all gameplay systems. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `ProfileTag` | `FGameplayTag` | Tag matching a DA_HapticProfile | | `IntensityMultiplier` | `Float` | Scale intensity (default 1.0) | - **Blueprint Authority:** Local Client Only - **Flow:** 1. Validate `bHapticsEnabled` and `bIsInitialized` — if disabled, return 2. Check `MinTimeBetweenEffects` cooldown — if too soon, queue or skip 3. Look up `DA_HapticProfile` from `HapticProfileMap` by `ProfileTag` 4. If not found: log warning, return 5. Build `S_HapticRequest` with profile data 6. Call `PlayHapticInternal()` with the request 7. Broadcast `OnHapticPlayed` with tag #### `PlayHapticByEvent` → `void` - **Description:** Play a haptic effect by event type. Convenience wrapper for systems that don't know the exact tag. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `EventType` | `EHapticEvent` | Which event to trigger | | `IntensityMultiplier` | `Float` | Scale intensity (default 1.0) | - **Flow:** Converts `EHapticEvent` to default tag (e.g., `Damage` → `Haptic.Damage.Default`) and calls `PlayHapticByTag`. #### `StopHaptic` → `void` - **Description:** Stop all currently playing haptic effects. - **Flow:** 1. If `CachedPlayerController` valid: call `Stop Force Feedback` node 2. Set `ActiveHapticEffect = None` 3. If `bHeartbeatActive`: call `StopHeartbeatHaptic()` 4. Broadcast `OnHapticStopped` #### `PlayHeartbeatHaptic` → `void` - **Description:** Start or update the continuous heartbeat pulse haptic at the given BPM. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `BeatsPerMinute` | `Float` | Heart rate in BPM (from BPC_StateManager) | - **Flow:** 1. Clamp BPM to `HeartbeatMinBPM` — `HeartbeatMaxBPM` 2. Calculate pulse interval: `Interval = 60.0 / BPM` 3. Set `bHeartbeatActive = true` 4. Start a looping timer at `Interval` seconds 5. Each tick: call `PlayHapticByTag(Haptic.Heartbeat)` with intensity from BPM 6. If BPM changes: update timer interval #### `StopHeartbeatHaptic` → `void` - **Description:** Stop the continuous heartbeat haptic loop. - **Flow:** Clear heartbeat timer, set `bHeartbeatActive = false`. #### `SetHapticsEnabled` → `void` - **Description:** Enable or disable all haptic output. Called by accessibility toggle. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `bEnabled` | `Bool` | New state | - **Flow:** 1. Set `bHapticsEnabled = bEnabled` 2. If disabled: call `StopHaptic()` 3. Broadcast `OnHapticsEnabledChanged` #### `SetControllerPlatform` → `void` - **Description:** Force a specific controller platform (for testing or hot-swap). - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Platform` | `EControllerPlatform` | Target platform | #### `GetControllerPlatform` → `EControllerPlatform` - **Description:** Returns the detected controller platform. Read-only. #### `IsDualSenseConnected` → `Bool` - **Description:** Returns true if a PS5 DualSense controller is detected. - **Flow:** Returns `CurrentPlatform == PS5_DualSense` #### `SetDualSenseTriggerEffect` → `void` - **Description:** Set adaptive trigger resistance on a specific DualSense trigger. No-op on non-DualSense platforms. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `TriggerSide` | `Name` | "Left" or "Right" | | `EffectType` | `Name` | "Resistance", "Vibration", "WeaponFire", "BowDraw", etc. | | `StartPosition` | `Int32` | Trigger position where effect begins (0–9) | | `Strength` | `Int32` | Effect strength (0–8) | - **Blueprint Authority:** Local Client Only - **Flow:** 1. If `CurrentPlatform != PS5_DualSense`: return (graceful no-op) 2. If `bEnableDualSenseTriggers == false`: return 3. Call platform-specific DualSense trigger API via `PlayerController` 4. Log effect for debugging ### Protected / Private Functions #### `PlayHapticInternal` → `void` - **Description:** Core haptic playback logic. Resolves platform, selects the right effect asset, handles priority conflicts. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Request` | `S_HapticRequest` | Full haptic request | - **Flow:** 1. Check priority: if `Request.Priority < PendingRequest.Priority` and `PendingRequest` is active → skip 2. If `Request.Priority >= PendingRequest.Priority`: interrupt current effect 3. Resolve platform: select `UForceFeedbackEffect` from `DA_HapticProfile` for `CurrentPlatform` 4. If no platform-specific asset: fall back to generic PC effect 5. Apply `IntensityMultiplier` to `HapticIntensityScale` 6. Call `Play Force Feedback` node on `CachedPlayerController`: - Input: `ForceFeedbackEffect`, `IntensityMultiplier`, `bLooping=false`, `bIgnoreTimeDilation=true` 7. Store as `ActiveHapticEffect` 8. Set `LastPlayTime = GameTimeInSeconds` 9. Broadcast `OnHapticPlayed` #### `DetectControllerPlatform` → `void` - **Description:** Queries the input system to determine which controller is connected. - **Flow:** 1. Check `UGameplayStatics::GetPlatformName()` 2. If platform contains "PS5" → `PS5_DualSense` 3. If platform contains "PS4" → `PS4_DualShock` 4. If platform contains "Xbox" or "XboxOne" or "XSX" → `Xbox` 5. Otherwise: check connected devices → if gamepad found → `PC_Generic`, else → `Unknown` --- ## 5. Event Dispatchers | Dispatcher | Parameters | Bind Access | Description | |------------|-----------|-------------|-------------| | `OnHapticsControllerInitialized` | — | `Public` | Fired after Initialize completes | | `OnHapticPlayed` | `FGameplayTag ProfileTag`, `EHapticEvent EventType`, `Float Intensity` | `Public` | Fired when any haptic effect starts | | `OnHapticStopped` | — | `Public` | Fired when haptics stop (manual or effect ends) | | `OnHapticsEnabledChanged` | `Bool bEnabled` | `Public` | Fired when master toggle changes | | `OnControllerPlatformChanged` | `EControllerPlatform NewPlatform` | `Public` | Fired on controller hot-swap | | `OnDualSenseTriggerActivated` | `Name TriggerSide`, `Name EffectType` | `Public` | Fired when adaptive trigger effect applied (PS5 only) | --- ## 6. Overridden Events / Custom Events ### Event: `BeginPlay` - **Description:** Startup. Calls `Initialize()`. - **Flow:** 1. Call `Initialize()` 2. If `CachedPlayerController` is null: log error, return 3. Register for controller connection/disconnection events ### Event: `EndPlay` - **Description:** Cleanup on component destruction. - **Flow:** 1. Call `StopHaptic()` (stop all effects) 2. Call `StopHeartbeatHaptic()` 3. Unbind from all dispatchers 4. Clear cached references --- ## 7. Blueprint Graph Logic Flow ```mermaid flowchart TD A[BeginPlay] --> B[Initialize] B --> C{Detect Controller Platform} C --> D[Set CurrentPlatform] D --> E[Load DA_HapticProfile Map] E --> F[Bind to AccessibilitySettings.OnHapticsToggleChanged] F --> G[Bind to StateManager.OnHeartRateChanged] G --> H[Broadcast OnInitialized] I[PlayHapticByTag] --> J{bHapticsEnabled?} J -->|No| K[Return] J -->|Yes| L{Cooldown check} L -->|TooSoon| M[Queue or skip] L -->|OK| N[Lookup DA_HapticProfile] N --> O{Found?} O -->|No| P[Log Warning] O -->|Yes| Q[Build S_HapticRequest] Q --> R[PlayHapticInternal] R --> S{Platform == PS5?} S -->|Yes| T[Play Force Feedback PS5 Profile] S -->|No| U[Play Force Feedback Generic] T --> V[Broadcast OnHapticPlayed] U --> V W[PlayHeartbeatHaptic] --> X[Clamp BPM] X --> Y[Calculate Interval = 60/BPM] Y --> Z[Start Looping Timer] Z --> AA[Each Tick: PlayHapticByTag Haptic.Heartbeat] ``` --- ## 8. Communication Matrix | Who Talks | How | What Is Sent | |-----------|-----|-------------| | `BPC_HealthSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.Damage.Heavy, Intensity)` on damage taken | | `BPC_FirearmSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.WeaponFire.Pistol)` on fire | | `BPC_MeleeSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.MeleeImpact)` on hit | | `BPC_PhysicsDragSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.Grab)` / `Haptic.Release` | | `BPC_ScareEventSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.ScareEvent)` on scare trigger | | `BPC_ReloadSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.WeaponReload)` on reload complete | | `BP_ItemPickup` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.PickupItem)` on collect | | `BPC_MovementStateSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.Footstep. + SurfaceTag)` via GASP notify | | `BPC_StateManager` | `Dispatcher` | `OnHeartRateChanged(BPM)` → `BPC_HapticsController::PlayHeartbeatHaptic(BPM)` | | `BPC_AccessibilitySettings` | `Dispatcher` | `OnHapticsToggleChanged(bEnabled)` → `BPC_HapticsController::SetHapticsEnabled()` | | `BPC_DeathHandlingSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.Death)` on death | | `BPC_DamageReceptionSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.Explosion)` on area damage | | `BPC_StaminaSystem` | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.StaminaExhausted)` on empty | | `BPC_HealthSystem` (low) | `Function Call` | `BPC_HapticsController::PlayHapticByTag(Haptic.LowHealth)` when <25% HP | --- ## 9. Validation / Testing Checklist - [ ] `Initialize` correctly detects Xbox controller: `CurrentPlatform = Xbox` - [ ] `Initialize` correctly detects PS5 DualSense: `CurrentPlatform = PS5_DualSense` - [ ] `PlayHapticByTag` with valid tag plays force feedback on controller - [ ] `PlayHapticByTag` with invalid tag logs warning but does not crash - [ ] `StopHaptic` immediately stops all controller vibration - [ ] `bHapticsEnabled = false` causes all `PlayHapticByTag` calls to be skipped - [ ] Heartbeat haptic loops at correct interval (60 BPM → 1 pulse/second) - [ ] Heartbeat BPM changes dynamically when `PlayHeartbeatHaptic(120)` called - [ ] DualSense trigger resistance activates on PS5 (R2 stiffens when aiming) - [ ] Non-DualSense platforms gracefully skip trigger effects (no crash, no log spam) - [ ] Priority conflict: higher priority effect interrupts lower (damage overrides footstep) - [ ] `MinTimeBetweenEffects` prevents rapid-fire rumble spam - [ ] Edge case: `PlayHapticByTag` before `Initialize` logs warning and returns - [ ] Edge case: Controller disconnected mid-effect — `StopHaptic` called safely - [ ] Edge case: Multiple rapid `PlayHapticByTag` calls — only highest priority plays - [ ] Edge case: Platform hot-swap (Xbox → PS5) fires `OnControllerPlatformChanged` --- ## 10. Reuse Notes - Attach this component to the **Player Controller** (not the Pawn). Haptics are per-controller, per-player. - All gameplay systems trigger haptics via GameplayTag — never hardcode `Play Force Feedback` nodes. - The `DA_HapticProfile` Data Asset stores platform-specific `UForceFeedbackEffect` curves. Designers author these in the Content Browser without touching Blueprints. - For multiplayer: haptics are **local client only** — never replicated. The server doesn't need to know about controller vibration. - Heartbeat haptic is the only continuous/looping effect. All others are one-shot. - DualSense adaptive triggers require the **PS5 Controller Plugin** enabled. Framework gracefully degrades on other platforms. - The `Haptic.` GameplayTag namespace is documented in `DT_Tags_Player.csv` and `DA_GameTagRegistry`. - Accessibility: `bHapticsEnabled` syncs with `BPC_AccessibilitySettings` so players can disable all vibration from settings. - For platform profiles: create one `DA_HapticProfile` instance per event per platform, then reference all three in the profile map. `BPC_HapticsController` selects the right one at runtime. --- ## 11. Manual Implementation Guide > **This section is for a human implementer building the Blueprint manually in UE5.** > Follow these steps in order. Each function is broken down into specific UE5 Blueprint nodes. ### 11.1 Class Setup 1. Create a new Blueprint Class: - Parent Class: `ActorComponent` - Name: `BPC_HapticsController` - Path: `Content/Framework/Settings/` 2. Add all variables from Section 3 to the Class Defaults. - Configuration variables: set `Instance Editable` ✓ - Internal variables: set to `Private` (no expose) 3. Add the Event Dispatchers from Section 5. ### 11.2 Variable Initialization (BeginPlay) ``` Event BeginPlay ├─ Get Owner → Cast to PlayerController → Store as CachedPlayerController │ └─ If NOT valid: Print String "BPC_HapticsController: Owner is not a PlayerController!" → Return ├─ Call DetectControllerPlatform → Set CurrentPlatform ├─ Load Asset Registry → Get All Assets of Class (DA_HapticProfile) │ └─ ForEach: Add to HapticProfileMap [ProfileTag → Asset] ├─ Get Owner → Get Component by Class (BPC_AccessibilitySettings) │ └─ If valid: Bind Event OnHapticsToggleChanged → SetHapticsEnabled │ └─ If valid: Read initial bHapticsEnabled value ├─ Get Owner Pawn → Get Component by Class (BPC_StateManager) │ └─ If valid: Bind Event OnHeartRateChanged → PlayHeartbeatHaptic ├─ Set bIsInitialized = true └─ Call OnHapticsControllerInitialized ``` ### 11.3 Function Implementations #### `PlayHapticByTag` **Input Pins:** `ProfileTag` (GameplayTag), `IntensityMultiplier` (Float) **Output Pins:** None **Node-by-Node Logic:** ``` [Function: PlayHapticByTag] Step 1: Branch on bHapticsEnabled → False: Return Step 2: Get Game Time in Seconds → Subtract LastPlayTime → Compare to MinTimeBetweenEffects Branch → Too soon: Return (or queue if needed) Step 3: HapticProfileMap → Find (ProfileTag) → Store as FoundProfile Step 4: Branch on FoundProfile valid? True → Step 4a: Make S_HapticRequest: - ProfileTag = ProfileTag - EventType = FoundProfile.EventType - IntensityMultiplier = IntensityMultiplier - DurationOverride = -1 (use profile default) - Priority = FoundProfile.Priority Step 4b: Call PlayHapticInternal(S_HapticRequest) Step 4c: Call OnHapticPlayed(ProfileTag, FoundProfile.EventType, IntensityMultiplier) False → Step 4d: Print String Warning: "No haptic profile found for tag: {ProfileTag}" ``` **Nodes Used:** `Branch`, `FindGameplayTag`, `Map Find`, `Make Struct (S_HapticRequest)`, `Call Function`, `Print String` #### `PlayHapticInternal` **Input Pins:** `Request` (S_HapticRequest) **Output Pins:** None **Node-by-Node Logic:** ``` [Function: PlayHapticInternal] Step 1: If ActiveHapticEffect is valid → Call StopHaptic (interrupt current) Step 2: Break S_HapticRequest → get ProfileTag Step 3: Look up DA_HapticProfile from HapticProfileMap Step 4: Switch on CurrentPlatform: Case PS5_DualSense: Get PS5_ForceFeedbackCurve from profile Case Xbox: Get Xbox_ForceFeedbackCurve from profile Default: Get Generic_ForceFeedbackCurve from profile Step 5: Branch on selected curve valid? True → Step 5a: CachedPlayerController → Play Force Feedback - Force Feedback Effect: selected curve asset - Intensity Multiplier: Request.IntensityMultiplier * HapticIntensityScale - bLooping: false - bIgnore Time Dilation: true Step 5b: Set ActiveHapticEffect = selected curve False → Step 5c: Print String Warning: "No ForceFeedbackEffect for platform {CurrentPlatform}" Step 6: Set LastPlayTime = Get Game Time in Seconds ``` **Nodes Used:** `Switch on EControllerPlatform`, `Break S_HapticRequest`, `Map Find`, `Play Force Feedback (PlayerController)`, `Branch` #### `PlayHeartbeatHaptic` **Input Pins:** `BeatsPerMinute` (Float) **Output Pins:** None **Node-by-Node Logic:** ``` [Function: PlayHeartbeatHaptic] Step 1: Clamp (BeatsPerMinute, HeartbeatMinBPM, HeartbeatMaxBPM) → Store as ClampedBPM Step 2: Divide 60.0 / ClampedBPM → Store as PulseInterval Step 3: If bHeartbeatActive: True → Clear Timer by Handle (HeartbeatTimerHandle) Step 4: Set bHeartbeatActive = true Step 5: Set Timer by Event: - Event: Custom Event (HeartbeatPulse) - Time: PulseInterval - Looping: true - Store handle as HeartbeatTimerHandle [Custom Event: HeartbeatPulse] → Call PlayHapticByTag(Haptic.Heartbeat, 1.0) ``` **Nodes Used:** `Clamp (float)`, `Divide`, `Set Timer by Event`, `Clear Timer by Handle` #### `SetHapticsEnabled` **Input Pins:** `bEnabled` (Bool) **Output Pins:** None ``` [Function: SetHapticsEnabled] Step 1: Set bHapticsEnabled = bEnabled Step 2: Branch: False → Call StopHaptic Step 3: Call OnHapticsEnabledChanged(bEnabled) ``` #### `DetectControllerPlatform` ``` [Function: DetectControllerPlatform] Step 1: Get Platform Name → Store as PlatformStr Step 2: String Contains (PlatformStr, "PS5") → True: Set CurrentPlatform = PS5_DualSense → Return Step 3: String Contains (PlatformStr, "PS4") → True: Set CurrentPlatform = PS4_DualShock → Return Step 4: String Contains (PlatformStr, "Xbox") → True: Set CurrentPlatform = Xbox → Return Step 5: Get Input Device Type → Switch on Type: Gamepad → Set CurrentPlatform = PC_Generic Default → Set CurrentPlatform = Unknown ``` **Nodes Used:** `Get Platform Name`, `Contains (string)`, `Switch on String`, `Get Input Device Type` ### 11.4 Event Dispatcher Bindings (Inbound Listeners) | Bind to Dispatcher | Custom Event to Create | What it Does | |--------------------------------------|------------------------|--------------| | `BPC_StateManager.OnHeartRateChanged` | `OnHeartRateChanged_Handler` | `PlayHeartbeatHaptic(CurrentHeartRate)` | | `BPC_AccessibilitySettings.OnHapticsToggleChanged` | `OnHapticsToggle_Handler` | `SetHapticsEnabled(bEnabled)` | ### 11.5 Multiplayer Networking - This component is **local client only**. No replication needed. - Haptics play only on the local player's controller. - No `HasAuthority()` gates needed — haptics are cosmetic. - For listen server hosts: `IsLocalPlayerController()` check before playing effects. ### 11.6 Quick Node Reference | Node | Where to Find | Used For | |------|---------------|----------| | `Play Force Feedback` | Right-click → "Play Force Feedback" | Playing rumble effect on controller | | `Stop Force Feedback` | Right-click → "Stop Force Feedback" | Stopping all active vibration | | `Set Timer by Event` | Right-click → "Set Timer by Event" | Looping heartbeat pulse | | `Get Platform Name` | Right-click → "Get Platform Name" | Detecting Xbox/PS5/PC | | `Clamp (float)` | Right-click → "Clamp" | Clamping BPM range | | `Make Struct` | Right-click → "Make S_HapticRequest" | Building haptic request | | `Map Find` | Right-click → "Find" | Looking up profile by tag | | `Get Game Time in Seconds` | Right-click → "Get Game Time" | Cooldown tracking | --- ## 12. Blueprint Build Checklist - [ ] Create Blueprint class: `BPC_HapticsController` (parent: `ActorComponent`) - [ ] Add all variables from Section 3 with correct types and defaults - [ ] Create `EHapticEvent`, `EHapticMotor`, `EControllerPlatform` enums (in Content Browser) - [ ] Create `S_HapticRequest` struct (in Content Browser) - [ ] Build `BeginPlay` event → `Initialize` chain - [ ] Implement `DetectControllerPlatform` function - [ ] Implement `PlayHapticInternal` with Platform Switch - [ ] Implement `PlayHapticByTag` (public entry point) - [ ] Implement `PlayHapticByEvent` (convenience wrapper) - [ ] Implement `StopHaptic` / `StopHeartbeatHaptic` - [ ] Implement `PlayHeartbeatHaptic` with looping timer - [ ] Implement `SetHapticsEnabled` / `SetDualSenseTriggerEffect` - [ ] Create all 6 event dispatchers - [ ] Bind to `BPC_StateManager.OnHeartRateChanged` - [ ] Bind to `BPC_AccessibilitySettings.OnHapticsToggleChanged` - [ ] Create at least one `DA_HapticProfile` instance for `Haptic.Damage.Default` - [ ] Test: PlayHapticByTag triggers vibration on Xbox controller - [ ] Test: PlayHapticByTag triggers vibration on PS5 DualSense - [ ] Test: bHapticsEnabled=false blocks all effects - [ ] Test: Heartbeat BPM changes pitch/speed of pulse - [ ] Test: Rapid-fire calls respect MinTimeBetweenEffects --- *Blueprint Spec: Haptics Controller. Conforms to TEMPLATE.md v2.0 — part of the UE5 Modular Game Framework, SETTINGS layer.*