# 2. Mood Definitions and Stats Signals Part of [[Music Mood Mixer]]. Previous: [[1. Horizontal vs Vertical Adaptive Music]]. Next: [[3. Snapshot Blends Stem Remixing and Scheduling]]. > [!note] Verification status > ScriptableObject and event patterns follow [[ScriptableObject Architecture]] and the Type Object / Observer chapters of [[Game Programming Patterns (Review)]]. Mixer name lookup uses `AudioMixer.FindSnapshot` (exact match) from the 6000.5 reference. **None of the code has been compiled or play-tested in Unity.** The director should not know how your game stores health. Combat scripts should not know the name of a mixer snapshot. That split is Observer plus Type Object: gameplay publishes *signals*, assets describe *moods*, and one component in the middle decides transitions. ## 2.1 Mood as data (Type Object) Create the enum and the asset. One asset per mood, the same way [[Stat-Driven RPG Systems]] keeps stat definitions as assets and runtime values on components. ```csharp using UnityEngine; using UnityEngine.Audio; public enum MusicMoodId { Explore = 0, Tension = 1, Combat = 2, Victory = 3, LowHealth = 4 } /// Data asset describing one adaptive-music mood. /// Runtime state (current mood, timers) does not live here. [CreateAssetMenu(menuName = "Audio/Music Mood Definition", fileName = "Mood_")] public class MusicMoodDefinition : ScriptableObject { public MusicMoodId id = MusicMoodId.Explore; [Tooltip("Exact snapshot name on the mixer (FindSnapshot is an exact match).")] public string snapshotName = "Explore"; [Tooltip("Seconds used when TransitionTo this mood.")] public float fadeInSeconds = 1.25f; [Tooltip("How long the request must stay valid before we enter this mood.")] public float enterHoldSeconds = 0.35f; [Tooltip("How long the request must stay invalid before we leave this mood.")] public float exitHoldSeconds = 1.5f; [Range(0f, 1f)] [Tooltip("Health at or below this value (normalized) requests LowHealth. Unused on other moods.")] public float lowHealthEnter = 0.3f; [Range(0f, 1f)] [Tooltip("Health must rise above this value to drop LowHealth. Should be higher than lowHealthEnter.")] public float lowHealthExit = 0.45f; [Tooltip("Optional clip used if you also horizontally resequence into this mood.")] public AudioClip sectionClip; public AudioMixerSnapshot ResolveSnapshot(AudioMixer mixer) { if (mixer == null || string.IsNullOrEmpty(snapshotName)) return null; return mixer.FindSnapshot(snapshotName); } } ``` > [!warning] Code not compiled in Unity > Not compiled. Author five assets (`Mood_Explore`, `Mood_Tension`, `Mood_Combat`, `Mood_Victory`, `Mood_LowHealth`) and type snapshot names so they match the mixer exactly. Do not write the current mood back into the asset at runtime; that is the "definitions are assets, state is a component" rule from [[Stat-Driven RPG Systems]] and [[ScriptableObject Architecture]]. Suggested starting values: | Mood | Fade in | Enter hold | Exit hold | Notes | |---|---|---|---|---| | Explore | 1.5 s | 0 s | n/a (fallback) | Default / rest state | | Tension | 1.0 s | 0.4 s | 2.0 s | Nearby enemies or high wanted level, no shots fired | | Combat | 0.6 s | 0.2 s | 2.5 s | Short enter, long exit so a reload does not drop the drums | | Victory | 0.15 s | 0 s | 0 s (timed) | Treated as a one-shot overlay; return to Explore when the sting ends | | LowHealth | 0.8 s | 0.1 s | 1.0 s | Enter at 30% health, leave at 45% (hysteresis band) | The hysteresis band on health is the same idea as a thermostat: without it, a player hovering around 30% will flutter the mix on every health-orb tick. ## 2.2 Signals, not queries (Observer) Gameplay code raises facts. The director listens. This is the Observer pattern from [[Game Programming Patterns (Review)]]; if many systems spam the same fact (three enemies all reporting `InCombat`), an Event Queue that coalesces "combat requested" once per frame is a cheap upgrade, and Nystrom's Event Queue chapter even uses audio as the example. ```csharp using System; using UnityEngine; /// Scene-level façade other systems publish music-relevant facts into. /// The director observes these values; publishers never touch the mixer. public class PlayerMusicSignals : MonoBehaviour { [Range(0f, 1f)] public float healthNormalized = 1f; public bool inCombat; public bool inTension; // e.g. seen by an enemy, or wanted level >= 1 [Min(0)] public int wantedLevel; public bool victoryRequested; public event Action SignalsChanged; public void SetHealth(float normalized) { normalized = Mathf.Clamp01(normalized); if (Mathf.Approximately(healthNormalized, normalized)) return; healthNormalized = normalized; SignalsChanged?.Invoke(); } public void SetCombat(bool value) { if (inCombat == value) return; inCombat = value; SignalsChanged?.Invoke(); } public void SetTension(bool value) { if (inTension == value) return; inTension = value; SignalsChanged?.Invoke(); } public void SetWantedLevel(int level) { level = Mathf.Max(0, level); if (wantedLevel == level) return; wantedLevel = level; if (level >= 1) inTension = true; if (level == 0 && !inCombat) inTension = false; SignalsChanged?.Invoke(); } public void RequestVictory() { victoryRequested = true; SignalsChanged?.Invoke(); } public void ClearVictory() { if (!victoryRequested) return; victoryRequested = false; SignalsChanged?.Invoke(); } } ``` > [!warning] Code not compiled in Unity > Not compiled. Keep a single instance in the gameplay scene and drag it into combat, health and trigger components. Prefer a serialized reference over a singleton; [[Game Programming Patterns (Review)]] is blunt about `static Instance` managers. ### Who publishes what | Publisher | Signal | Existing tutorial to hook | |---|---|---| | Health / damage | `SetHealth(current / max)` | [[Stat-Driven RPG Systems]], [[High Scores, Saves and Player Stats]], the health blend already sketched in [[3. Character Audio Effects Chain]] | | Enemy AI | `SetCombat(true)` while any enemy is in an attack state; `SetTension(true)` while in alert/search | [[Enemy AI State Machine]], [[Organic Enemy Sensing]] | | Wanted-level / heat | `SetWantedLevel(n)` | Your own encounter manager; treat `n >= 1` as tension and `n >= 2` as a reason to bias toward combat even before shots are fired | | Location | A trigger sets tension or swaps a *location* bed (biome change is horizontal; "you entered the spooky woods" can also raise Tension) | Scene triggers; see `MusicLocationZone` below | | Encounter resolved | `RequestVictory()` | The same place you grant XP or open a reward chest ([[Level Up! (Review)]] Level 13) | | Movement (optional) | Sprint, airborne or stealth as extra tension modifiers | [[Character Controllers]] events such as `Stepped` / `Jumped` are for footsteps ([[4. Footsteps and Surface Audio]]); use a separate "is sprinting / is seen" flag for music | A location zone is just another publisher: ```csharp using UnityEngine; /// Raise tension (or another signal) while the player stays inside a trigger. [RequireComponent(typeof(Collider))] public class MusicLocationZone : MonoBehaviour { [SerializeField] PlayerMusicSignals signals; [SerializeField] bool raiseTension = true; void Reset() { var col = GetComponent<Collider>(); col.isTrigger = true; } void OnTriggerEnter(Collider other) { if (!other.CompareTag("Player") || signals == null) return; if (raiseTension) signals.SetTension(true); } void OnTriggerExit(Collider other) { if (!other.CompareTag("Player") || signals == null) return; if (raiseTension) signals.SetTension(false); } } ``` > [!warning] Code not compiled in Unity > Not compiled. Tag your player `Player` or change the compare. If several overlapping zones can fight over `inTension`, move "how many zones am I in?" up to the signals object (a ref-count) instead of letting the last exit turn tension off. ## 2.3 Choosing a requested mood The director will apply hysteresis, but it needs a pure function that says "what would we like *right now*?" Priority, high to low: 1. **Victory** if a sting was requested (and has not finished). 2. **LowHealth** if health is in the low band (using enter/exit thresholds). 3. **Combat** if `inCombat` (or wanted level is high enough that you have decided gunfire is implied). 4. **Tension** if `inTension` or `wantedLevel >= 1`. 5. **Explore** otherwise. Low-health sitting above combat is a design choice, not a law. Rogers' clean-screen note (Level 8) argues that low health should be *felt*; letting it colour the combat mix (muffled bed, heartbeat already in your `SFX/Player/Heartbeat` group from Audio 1) usually beats switching to a separate "sad piano" cue in the middle of a fight. If you would rather stay in Combat and *blend* LowHealth on top, Part 3 shows a weighted two-snapshot blend for exactly that - the same pattern as `SnapshotDirector.BlendHealth` in [[2. Audio Mixer Snapshots and Exposed Parameters]]. ## 2.4 Hysteresis, in plain language Without hold times, this sequence will shred a mix: - Enemy opens a door (Tension) -> player fires (Combat) -> enemy ducks for 0.4 s (Explore) -> player fires again (Combat). With hold times: - Tension must be requested for `enterHoldSeconds` before the snapshot moves. A single frame of "I thought I saw something" does nothing. - Combat, once entered, stays until `exitHoldSeconds` have passed with *no* combat request. Reloads and line-of-sight breaks do not drop the drums. - LowHealth enters at 30% and leaves at 45%, so picking up a small kit at 29% does not immediately restore the bright explore mix. Victory is the exception: it is a timed overlay. The director enters it immediately, plays the sting (horizontally, or as a one-shot on `Music/Stingers`), then returns to whatever the pure function now says - usually Explore. Part 3 turns these rules into a working director, stem player and optional `PlayScheduled` section switcher.