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

@ -11,6 +11,12 @@ namespace TD.Audio
{
public AudioCategory category;
public int maxVoices;
// NOTE: currently inert. It seeds each pooled AudioSource at init, but every
// Play() call overwrites src.volume with (per-sound volume × SFX slider), so
// this value never reaches the mix. Left as-is rather than "fixed" because
// multiplying it in would halve every existing sound (authored scenes use 0.5).
// Wire it up only alongside a pass to re-level the per-sound volumes.
[Range(0f, 1f)]
public float volume;
}
@ -58,6 +64,16 @@ namespace TD.Audio
}
}
/// <summary>
/// Plays a one-shot on the given category's voice pool. <paramref name="volume"/> is
/// the per-sound authored level; it's scaled by the player's SFX slider
/// (<see cref="AudioVolumeSettings.SfxScalar"/>) on the way in.
/// </summary>
/// <remarks>
/// The scalar is sampled at play time, so a sound already in flight keeps the volume
/// it started with when the slider moves. Every SFX here is short, so that's
/// imperceptible and avoids having to track live voices.
/// </remarks>
public void Play(AudioClip clip, AudioCategory category, float pitch = 1f, float volume = 1f)
{
if (clip == null) return;
@ -66,7 +82,7 @@ namespace TD.Audio
int idx = indices[category];
var src = pool[idx % pool.Length];
src.pitch = pitch;
src.volume = volume;
src.volume = volume * AudioVolumeSettings.SfxScalar;
src.clip = clip;
src.Play();
indices[category] = idx + 1;

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;
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4d2f94f5f8df24e8aa1f47d9f0e3ecf4

View file

@ -7,6 +7,12 @@ namespace TD.Audio
/// Plays a single looping music track for the scene it lives in.
/// Add to a GameObject in the scene, assign a clip, and it starts on load.
/// </summary>
/// <remarks>
/// Volume is the authored <see cref="volume"/> scaled by the player's music slider
/// (<see cref="AudioVolumeSettings.MusicScalar"/>). Unlike one-shot SFX, the track is
/// continuous, so this subscribes to <see cref="AudioVolumeSettings.OnChanged"/> and
/// re-applies live while the slider is dragged.
/// </remarks>
[RequireComponent(typeof(AudioSource))]
public class MusicPlayer : MonoBehaviour
{
@ -14,14 +20,36 @@ namespace TD.Audio
[Range(0f, 1f)]
[SerializeField] private float volume = 1f;
private AudioSource source;
private void Awake()
{
source = GetComponent<AudioSource>();
}
private void OnEnable()
{
AudioVolumeSettings.OnChanged += ApplyVolume;
}
private void OnDisable()
{
AudioVolumeSettings.OnChanged -= ApplyVolume;
}
private void Start()
{
var src = GetComponent<AudioSource>();
src.clip = clip;
src.volume = volume;
src.loop = true;
src.playOnAwake = false;
src.Play();
source.clip = clip;
source.loop = true;
source.playOnAwake = false;
ApplyVolume();
source.Play();
}
private void ApplyVolume()
{
if (source == null) return;
source.volume = volume * AudioVolumeSettings.MusicScalar;
}
}
}