Move settings to Settings.json and add master volume

Replace the former per-value PlayerPrefs storage with a shared human-readable
Settings.json store (GameSettings). MouseSensitivity keeps its public .Multiplier
API and only swaps its backing to the JSON store, so the camera controllers stay
untouched. Add a MasterVolume facade that drives the AudioMixer's exposed
"MasterVolume" parameter (loaded from Resources, linear<->dB mapping), applied at
startup and on change, plus a MasterVolumeSlider binder. The default is the level
currently authored on the mixer instead of an arbitrary constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-03 15:30:44 +02:00
1 parent f586ef7a15
commit ba7f3c2e89
9 files changed
+290 -27

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
using UnityEngine;
using UnityEngine.Audio;
/// Global master audio volume (0 = mute .. 1 = full), applied to the "MasterVolume" exposed
/// parameter of the AudioMixer (loaded from Resources). Persisted via GameSettings (Settings.json).
/// Applied once at startup via a runtime-init hook so the saved level takes effect before the
/// first scene's audio plays, and again on every change.
public static class MasterVolume
{
public const float Min = 0f;
public const float Max = 1f;
/// The default is the level currently authored on the mixer (its exposed-parameter value
/// converted back to linear), so a fresh install starts at the mixer's own setting instead of
/// an arbitrary constant. Falls back to FallbackDefault if the mixer/param is unavailable.
public static float Default => TryReadMixerLinear(out float linear) ? linear : .5f;
// Must match the exposed parameter name authored on the AudioMixer asset.
private const string ExposedParam = "MasterVolume";
// AudioMixer.mixer lives under a Resources/ folder -> load by name without extension.
private const string MixerResourcePath = "AudioMixer";
// The exposed param is the master group's attenuation in dB; below this the log curve dives
// to -inf, so -80 dB is the practical silence floor (matches the mixer's own lower bound).
private const float SilenceDb = -80f;
private static AudioMixer _mixer;
private static AudioMixer Mixer => _mixer ? _mixer : _mixer = Resources.Load<AudioMixer>(MixerResourcePath);
public static float Volume
{
get => Mathf.Clamp(GameSettings.MasterVolume, Min, Max);
set
{
float v = Mathf.Clamp(value, Min, Max);
GameSettings.MasterVolume = v;
Apply(v); // set unconditionally: keeps the live mixer in sync even if the stored value was unchanged
}
}
// A perceptual volume slider is linear 0..1; the mixer wants dB. 20*log10 maps 1 -> 0 dB and
// fades smoothly, with 0 pinned to the silence floor (log10(0) is -inf).
private static float LinearToDb(float linear) =>
linear <= Min ? SilenceDb : Mathf.Max(SilenceDb, Mathf.Log10(linear) * 20f);
// Inverse of LinearToDb: read the mixer's authored dB back into the slider's 0..1 range.
private static float DbToLinear(float db) =>
db <= SilenceDb ? Min : Mathf.Clamp(Mathf.Pow(10f, db / 20f), Min, Max);
// Reads the mixer's current exposed-parameter level (from the active snapshot) as linear 0..1.
// Returns false if the mixer isn't loaded or the parameter isn't exposed, so callers can fall back.
private static bool TryReadMixerLinear(out float linear)
{
var mixer = Mixer;
if (!mixer || !mixer.GetFloat(ExposedParam, out float db))
{
linear = .5f;
return false;
}
linear = DbToLinear(db);
return true;
}
private static void Apply(float linear)
{
var mixer = Mixer;
if (!mixer)
{
Debug.LogWarning($"[MasterVolume] AudioMixer '{MixerResourcePath}' not found under a Resources/ folder.");
return;
}
if (!mixer.SetFloat(ExposedParam, LinearToDb(linear)))
Debug.LogWarning($"[MasterVolume] Exposed parameter '{ExposedParam}' not found on the AudioMixer.");
}
// Push the saved level before any scene audio starts. SetFloat overrides the exposed parameter
// permanently (until ClearFloat), so the start snapshot's authored value is superseded.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void ApplyOnStartup()
{
Apply(Volume);
}
}