Add settings menu to control audio and return to main menu
This commit is contained in:
parent
c88d3fc625
commit
0b7bc936ef
13 changed files with 697 additions and 22 deletions
|
|
@ -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;
|
||||
|
|
|
|||
97
Assets/_Project/Scripts/Audio/AudioVolumeSettings.cs
Normal file
97
Assets/_Project/Scripts/Audio/AudioVolumeSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4d2f94f5f8df24e8aa1f47d9f0e3ecf4
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue