# 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.