// GDnD wiki example // Demonstrates: one feedback component that turns gameplay events into feel (hit stop, camera // shake, particles, sound) using FeedbackPreset assets. Because every state raises events // instead of calling effects, a landing feels the same whether the player came from a jump, a // dash or a fall, and no state needs its own copy of the hit-stop code. // Related pages: [[state-pattern]], [[game-feel]], [[vfx-shape-and-timing]], [[observer-pattern]] // Unity 6 (6000.x). No packages required: camera shake is a UnityEvent, so wire it in the // Inspector to CinemachineImpulseSource.GenerateImpulse (Cinemachine 3, dynamic float) or to // your own shake script. // // Setup: add to the player (or a "Feedback" child), assign PlayerEvents, an AudioSource for SFX, // and a FeedbackPreset asset per slot. Untick Juice Enabled for the all-off playtest baseline. using System.Collections; using UnityEngine; using UnityEngine.Events; namespace GDnD.Patterns.Feedback { public class FeedbackPlayer : MonoBehaviour { [SerializeField] private PlayerEvents events; [Header("Presets (ScriptableObject assets)")] [SerializeField] private FeedbackPreset jumped; [SerializeField] private FeedbackPreset landed; [SerializeField] private FeedbackPreset lightHit; [SerializeField] private FeedbackPreset heavyHit; [SerializeField] private int heavyHitDamage = 20; // damage at or above this uses heavyHit [SerializeField] private float minLandingSpeed = 6f; // gentle landings get no feedback [Header("Outputs")] [SerializeField] private AudioSource sfxSource; [SerializeField] private UnityEvent shake; [SerializeField] private bool juiceEnabled = true; // A/B switch for playtests private float restoreScale = 1f; private Coroutine hitStop; private void OnEnable() { events.Jumped += OnJumped; events.Landed += OnLanded; events.HitLanded += OnHitLanded; } private void OnDisable() { events.Jumped -= OnJumped; events.Landed -= OnLanded; events.HitLanded -= OnHitLanded; // Coroutines stop with the component, so never leave the game frozen. if (hitStop != null) { Time.timeScale = restoreScale; hitStop = null; } } // The mapping from "what happened" to "which feel" lives here, and only here. private void OnJumped(Vector3 at) => Play(jumped, at); private void OnLanded(Vector3 at, float fallSpeed) { if (fallSpeed >= minLandingSpeed) Play(landed, at); } private void OnHitLanded(Vector3 at, int damage) => Play(damage >= heavyHitDamage ? heavyHit : lightHit, at); public void Play(FeedbackPreset preset, Vector3 at) { if (!juiceEnabled || preset == null) return; if (preset.sound != null && sfxSource != null) sfxSource.PlayOneShot(preset.sound); if (preset.particlesPrefab != null) { ParticleSystem burst = Instantiate(preset.particlesPrefab, at, Quaternion.identity); ParticleSystem.MainModule main = burst.main; Destroy(burst.gameObject, main.duration + main.startLifetime.constantMax); } if (preset.shakeForce > 0f) shake.Invoke(preset.shakeForce); if (preset.hitStopSeconds > 0f) StartHitStop(preset.hitStopSeconds); } private void StartHitStop(float seconds) { if (hitStop == null) restoreScale = Time.timeScale; // don't capture a running stop's 0 else StopCoroutine(hitStop); // a new hit restarts the timer hitStop = StartCoroutine(HitStopRoutine(seconds)); } private IEnumerator HitStopRoutine(float seconds) { Time.timeScale = 0f; yield return new WaitForSecondsRealtime(seconds); // WaitForSeconds never ends at timeScale 0 Time.timeScale = restoreScale; hitStop = null; } } }