// Assets/_Project/Scripts/Audio/AudioVolumeSettings.cs using UnityEngine; namespace TD.Audio { /// /// Player-facing volume mix, persisted in and shared by /// every audio consumer in the game. Two buses: /// /// Music — the looping scene track played by . /// SFX — everything routed through /// (spells, tower fire, build/land, enemy death, sell, UI clicks). /// /// /// /// Why not named AudioSettings. UnityEngine.AudioSettings already /// exists, so a TD.Audio.AudioSettings would be an ambiguous reference in any /// file that has both using UnityEngine; and using TD.Audio;. /// /// Units. Public values are ints 0..100 — the numbers the settings slider /// shows. Consumers multiply by / , /// which apply a squared taper so the slider's lower half isn't perceptually dead /// (linear amplitude drops off much faster than perceived loudness). At 100 the /// scalar is exactly 1, so the default mix is byte-identical to the pre-settings /// behaviour. /// /// Live updates. fires on every mutation. /// Continuous sources (music) subscribe and re-apply; one-shots read the scalar at /// play time, so a mid-flight SFX keeps the volume it started with. That's fine — /// they're all short. /// public static class AudioVolumeSettings { public const int MinVolume = 0; public const int MaxVolume = 100; private const int DefaultVolume = 100; private const string MusicKey = "td.audio.music"; private const string SfxKey = "td.audio.sfx"; /// Raised whenever music or SFX volume changes. public static event System.Action OnChanged; // -1 = not yet read from PlayerPrefs. Statics are cleared on domain reload // (entering play mode in the editor), so the lazy load re-runs each session. private static int music = -1; private static int sfx = -1; public static int MusicVolume { get { if (music < 0) music = PlayerPrefs.GetInt(MusicKey, DefaultVolume); return music; } set => Set(ref music, MusicKey, value); } public static int SfxVolume { get { if (sfx < 0) sfx = PlayerPrefs.GetInt(SfxKey, DefaultVolume); return sfx; } set => Set(ref sfx, SfxKey, value); } /// Amplitude multiplier (0..1) for the music bus. public static float MusicScalar => ToScalar(MusicVolume); /// Amplitude multiplier (0..1) for the sound-effects bus. public static float SfxScalar => ToScalar(SfxVolume); /// /// Flushes PlayerPrefs to disk. Setters only write the in-memory pref (cheap /// enough to call on every frame of a slider drag); call this once when the /// settings UI closes so the choice survives a crash. /// public static void Flush() => PlayerPrefs.Save(); private static void Set(ref int field, string key, int value) { int clamped = Mathf.Clamp(value, MinVolume, MaxVolume); if (field == clamped) return; field = clamped; PlayerPrefs.SetInt(key, clamped); OnChanged?.Invoke(); } private static float ToScalar(int volume) { float linear = Mathf.Clamp01(volume / (float)MaxVolume); return linear * linear; } } }