Add settings menu to control audio and return to main menu

This commit is contained in:
Ben Calegari 2026-07-28 22:01:28 -07:00
parent c88d3fc625
commit 0b7bc936ef
13 changed files with 697 additions and 22 deletions

View file

@ -0,0 +1,97 @@
// Assets/_Project/Scripts/Audio/AudioVolumeSettings.cs
using UnityEngine;
namespace TD.Audio
{
/// <summary>
/// Player-facing volume mix, persisted in <see cref="PlayerPrefs"/> and shared by
/// every audio consumer in the game. Two buses:
/// <list type="bullet">
/// <item><b>Music</b> — the looping scene track played by <see cref="MusicPlayer"/>.</item>
/// <item><b>SFX</b> — everything routed through <see cref="AudioManager"/>
/// (spells, tower fire, build/land, enemy death, sell, UI clicks).</item>
/// </list>
/// </summary>
/// <remarks>
/// <para><b>Why not named AudioSettings.</b> <c>UnityEngine.AudioSettings</c> already
/// exists, so a <c>TD.Audio.AudioSettings</c> would be an ambiguous reference in any
/// file that has both <c>using UnityEngine;</c> and <c>using TD.Audio;</c>.</para>
///
/// <para><b>Units.</b> Public values are ints 0..100 — the numbers the settings slider
/// shows. Consumers multiply by <see cref="MusicScalar"/> / <see cref="SfxScalar"/>,
/// 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.</para>
///
/// <para><b>Live updates.</b> <see cref="OnChanged"/> 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.</para>
/// </remarks>
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";
/// <summary>Raised whenever music or SFX volume changes.</summary>
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);
}
/// <summary>Amplitude multiplier (0..1) for the music bus.</summary>
public static float MusicScalar => ToScalar(MusicVolume);
/// <summary>Amplitude multiplier (0..1) for the sound-effects bus.</summary>
public static float SfxScalar => ToScalar(SfxVolume);
/// <summary>
/// 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.
/// </summary>
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;
}
}
}