Centralized vs. Distributed Event Dispatch
The Hapbeat SDK offers two patterns for how haptic events are invoked. There is no single correct answer — this is a design decision based on your project’s scale, team composition, and existing architecture.
| Pattern | Who invokes | Typical use |
|---|---|---|
| Trigger-first (distributed) | Per-zone Trigger components wired in Inspector | Showcase / small-to-medium projects / designers assembling in Inspector |
| Manager.Play (centralized) | Any script calls HapbeatManager.Instance.Play(eventId) directly | Retrofitting an existing game / managing the event catalog centrally in script |
Combining both patterns in the same project is also fine.
Trigger-first pattern (standard — used in Showcase)
Section titled “Trigger-first pattern (standard — used in Showcase)”Attach HapbeatXxxTrigger components to each zone; zone scripts hold the Trigger via [SerializeField] and call .Fire().
public class ChargeShooter : MonoBehaviour{ [SerializeField] private HapbeatUnityEventTrigger _trigger; [SerializeField] private AnimationCurve _gainCurve;
private void Release(float chargeT) { _trigger.GainMultiplier = _gainCurve.Evaluate(chargeT); _trigger.Fire(); }}- Inspector-complete — EventMap entry selection is just a dropdown
- Accessible to designers — event IDs can be changed without writing script
- All events are traceable from wiring — which object fires what is visible in the Hierarchy
- Inspector wiring becomes scattered as the event count grows
- Not suited for dynamically assembling event IDs in script
Manager.Play pattern (centralized)
Section titled “Manager.Play pattern (centralized)”Call HapbeatManager.Instance.Play(eventId, gain, displayName, target) directly. No Trigger components required.
public class GameHapticRouter : MonoBehaviour{ public static GameHapticRouter Instance { get; private set; }
[SerializeField] private HapbeatEventMap _eventMap; // optional: for entry lookup private void Awake() => Instance = this;
public void Play(string eventId, float gain = 1f, string target = null) { HapbeatManager.Instance.Play(eventId, gain, target: target ?? ""); }
public void OnEnemyHit() => Play("game.enemy_hit"); public void OnPickup() => Play("game.pickup", gain: 0.6f);}Call site:
GameHapticRouter.Instance.OnEnemyHit();- Manage the event catalog in one file — naming, gain, and target consolidated in one place
- Dynamic construction is easy — string assembly like
Play($"weapon.{weaponName}_fire") - Easy to retrofit into an existing game — introduce the SDK with a single singleton, no scattered Triggers
- The Inspector gives no visibility into what is being called (you must read the script)
- EventMap integration is manual (runtime existence checks appear, but unused entries cannot be detected in Inspector)
- Not covered by latency compensation:
HapbeatConfig.hapticDelaySeconds(audio delay compensation) is applied automatically only when going through Trigger / StateBehaviour. TheManager.Instance.Play()direct path bypasses EventMap entries, so if needed, the caller must add an equivalent delay usingInvoke/StartCoroutine. Be mindful of this when adopting the centralized pattern.
Guidance on choosing
Section titled “Guidance on choosing”- Small–medium scale / designer collaboration / learning via Showcase → Trigger-first
- Large projects that already have a game manager / event bus → Manager.Play centralized, or mixed
- Prioritizing debuggability → Trigger-first (wiring is visualized in the Hapbeat Event Logger menu)
- Many dynamically named events → Manager.Play
Mixing is fine
Section titled “Mixing is fine”For example, a natural split is “UI and scene objects use Trigger-first; systemic events fired deep in game logic use Manager.Play.” Internally both paths go through HapbeatManager, so mixing them has no impact on behavior.
See also
Section titled “See also”- Trigger Components — Triggers available for the Trigger-first approach
- EventMap Window — Entry management
- Integrating into Your Project — Initial setup