Files
UE5-Modular-Game-Framework/docs/blueprints/12-settings/148_BPC_HapticsController.md
Lefteris Notas 15d6e88780 feat: Implement BPC_PlatformServiceAbstraction for unified platform detection and SDK routing
- Added BPC_PlatformServiceAbstraction to centralize platform detection and SDK routing for achievements, cloud saves, and overlays.
- Updated dependencies across various systems to utilize the new platform service for consistent platform handling.
- Deprecated old platform enums in favor of a unified EPlatformFamily enum.
- Enhanced documentation for affected systems to reflect changes in platform handling and dependencies.
2026-05-22 18:22:42 +03:00

578 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 148 — Haptics Controller (`BPC_HapticsController`)
> **Blueprint-Only Implementation** — UE 5.55.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.02.0, 1.0 = default) |
| `DurationOverride` | `Float` | Override duration (-1 = use profile default) |
| `Priority` | `Int32` | Higher interrupts lower (0100) |
| `RequestTime` | `Float` | Game time when requested (for cooldown) |
---
## 3. Variables
### Configuration (Instance Editable)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `HapticProfileMap` | `TMap<FGameplayTag, DA_HapticProfile>` | `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 (09) |
| `Strength` | `Int32` | Effect strength (08) |
- **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.*