Enhance audio management and documentation
CI / build-test (push) Successful in 1m10s

Added a MasterVolume property to the AudioManager for unified control over sound effects and music volume. Updated the Play method to incorporate MasterVolume adjustments. Enhanced documentation in CLAUDE.md and architecture.md to reflect changes in audio management and overall engine architecture. Introduced a new test project for audio functionalities in the solution file.
This commit is contained in:
Leonid Pershin
2026-06-12 08:41:39 +03:00
parent 3f3c0200a8
commit ef1111bcb6
8 changed files with 358 additions and 7 deletions
+17 -1
View File
@@ -19,7 +19,23 @@ public sealed class AudioManager : IDisposable
set => _soundVolume = Math.Clamp(value, 0f, 1f);
}
/// <summary>
/// Master volume 0..1 applied on top of <see cref="SoundVolume"/> for sound effects and
/// mirrored onto <see cref="MusicPlayer.Volume"/>, so a single knob (e.g. a settings
/// slider) controls both effects and music.
/// </summary>
public float MasterVolume
{
get => _masterVolume;
set
{
_masterVolume = Math.Clamp(value, 0f, 1f);
Music.Volume = _masterVolume;
}
}
private float _soundVolume = 1f;
private float _masterVolume = 1f;
/// <summary>Plays a sound effect (fire and forget).</summary>
/// <param name="sound">The loaded sound effect.</param>
@@ -27,7 +43,7 @@ public sealed class AudioManager : IDisposable
/// <param name="pitch">Pitch offset in octaves, -1..1.</param>
/// <param name="pan">Stereo pan, -1 (left) .. 1 (right).</param>
public void Play(SoundEffect sound, float volume = 1f, float pitch = 0f, float pan = 0f) =>
sound.Play(Math.Clamp(volume, 0f, 1f) * _soundVolume, pitch, pan);
sound.Play(Math.Clamp(volume, 0f, 1f) * _soundVolume * _masterVolume, pitch, pan);
/// <inheritdoc />
public void Dispose() => Music.Dispose();
+144
View File
@@ -0,0 +1,144 @@
namespace MrGameEng.Core;
/// <summary>
/// Discrete game-speed control layered over <see cref="GameClock.TimeScale"/>: a pause plus an
/// ordered list of speed multipliers (1×, 3×, 6× by default). Pausing remembers the current
/// running step so <see cref="Resume"/> restores it. <see cref="Changed"/> fires on every
/// transition so UI (speed buttons, indicators) can refresh. Deterministic and GPU-free;
/// registered as a service via <see cref="GameSpeedEngineExtensions.UseGameSpeed"/>.
/// </summary>
public sealed class GameSpeed
{
private readonly GameClock _clock;
private readonly float[] _steps;
private int _stepIndex;
private bool _paused;
/// <summary>
/// Creates a controller that writes <see cref="CurrentSpeed"/> to
/// <paramref name="clock"/>. <paramref name="steps"/> are the running multipliers in
/// ascending order; each must be positive. Empty defaults to 1×, 3×, 6×.
/// </summary>
public GameSpeed(GameClock clock, params float[] steps)
{
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_steps = steps is { Length: > 0 } ? (float[])steps.Clone() : [1f, 3f, 6f];
foreach (var step in _steps)
{
if (step <= 0f)
{
throw new ArgumentOutOfRangeException(
nameof(steps),
"Speed steps must be positive."
);
}
}
Apply();
}
/// <summary>The ordered running speeds (excludes the pause state).</summary>
public IReadOnlyList<float> Steps => _steps;
/// <summary>Index of the active running step within <see cref="Steps"/>.</summary>
public int StepIndex => _stepIndex;
/// <summary>True while gameplay is paused (clock time scale is 0).</summary>
public bool IsPaused => _paused;
/// <summary>Active multiplier: 0 while paused, otherwise <c>Steps[StepIndex]</c>.</summary>
public float CurrentSpeed => _paused ? 0f : _steps[_stepIndex];
/// <summary>Raised after any change to the pause state or the active step.</summary>
public event Action? Changed;
/// <summary>Pauses gameplay, remembering the current step for <see cref="Resume"/>.</summary>
public void Pause()
{
if (_paused)
{
return;
}
_paused = true;
Apply();
}
/// <summary>Resumes gameplay at the remembered step.</summary>
public void Resume()
{
if (!_paused)
{
return;
}
_paused = false;
Apply();
}
/// <summary>Toggles between paused and running.</summary>
public void TogglePause()
{
_paused = !_paused;
Apply();
}
/// <summary>Selects a running step by index (clamped to the valid range) and unpauses.</summary>
public void SetStep(int index)
{
_stepIndex = Math.Clamp(index, 0, _steps.Length - 1);
_paused = false;
Apply();
}
/// <summary>Steps to the next faster speed (clamped to the fastest) and unpauses.</summary>
public void Faster() => SetStep(_stepIndex + 1);
/// <summary>Steps to the next slower speed (clamped to the slowest) and unpauses.</summary>
public void Slower() => SetStep(_stepIndex - 1);
/// <summary>
/// Cycles through states: pause → slowest step → … → fastest step → pause. Handy for a
/// single "next speed" key or button.
/// </summary>
public void Cycle()
{
if (_paused)
{
_paused = false;
_stepIndex = 0;
}
else if (_stepIndex + 1 < _steps.Length)
{
_stepIndex++;
}
else
{
_paused = true;
}
Apply();
}
private void Apply()
{
_clock.TimeScale = CurrentSpeed;
Changed?.Invoke();
}
}
/// <summary>Wires the game-speed controller into the engine.</summary>
public static class GameSpeedEngineExtensions
{
/// <summary>
/// Creates a <see cref="GameSpeed"/> bound to the context's clock and registers it as a
/// service. Call once at startup. <paramref name="steps"/> are the running multipliers
/// (defaults to 1×, 3×, 6× when empty).
/// </summary>
public static GameSpeed UseGameSpeed(this EngineContext context, params float[] steps)
{
var speed = new GameSpeed(context.Clock, steps);
context.Services.Add(speed);
return speed;
}
}