Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef1111bcb6 |
@@ -27,13 +27,15 @@ is the living showcase — new engine features are demonstrated there.
|
||||
|
||||
Engine libraries (each feature is a namespaced subfolder of its host):
|
||||
|
||||
- **`Core`** — game loop, ECS world, scenes, time, plus **Input** (`MrGameEng.Input`:
|
||||
`InputManager`, `ActionMap`, `InputSystem`, in `Core/Input/`). Depends only on
|
||||
MonoGame and Friflo.Engine.ECS.
|
||||
- **`Core`** — game loop, ECS world, scenes, time (`GameClock` with `TimeScale`; `GameSpeed`
|
||||
for discrete pause/1×/3×/6× speed control over the clock, `context.UseGameSpeed(...)`),
|
||||
plus **Input** (`MrGameEng.Input`: `InputManager`, `ActionMap`, `InputSystem`, in
|
||||
`Core/Input/`). Depends only on MonoGame and Friflo.Engine.ECS.
|
||||
- **`Graphics`** — custom batched renderer, camera, sprites, plus **Tilemaps**
|
||||
(`MrGameEng.Tilemaps`: code-built tile grids rendered through the batcher,
|
||||
`scene.UseTilemaps()` after `UseRenderer2D()`, in `Graphics/Tilemaps/`). → `Core`.
|
||||
- **`Audio`** — ogg playback (NVorbis). → `Core`.
|
||||
- **`Audio`** — ogg playback (NVorbis); `AudioManager` with `SoundVolume`/`MasterVolume`
|
||||
(one knob for effects + music). → `Core`.
|
||||
- **`Content`** — the asset/content pipeline: **Assets** (`MrGameEng.Assets`: runtime
|
||||
loading, no MGCB pipeline; FontStash), **Atlases** (`MrGameEng.Atlases`: texture-atlas
|
||||
builder + runtime loader, CLI wrapper in `tools/MrGameEng.AtlasTool`; StbImage), and
|
||||
|
||||
@@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Simulation.Tests"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "tests\MrGameEng.UI.Tests\MrGameEng.UI.Tests.csproj", "{7F7D9641-2409-40CB-88A9-56BCE8C90A45}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{4407F6E6-0B65-41A3-ADFA-B78684A9B918}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -215,6 +217,18 @@ Global
|
||||
{7F7D9641-2409-40CB-88A9-56BCE8C90A45}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7F7D9641-2409-40CB-88A9-56BCE8C90A45}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7F7D9641-2409-40CB-88A9-56BCE8C90A45}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -234,5 +248,6 @@ Global
|
||||
{6A47DB40-FD1F-4DD5-A82E-1E526D3E1C39} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{C34F2AFF-F2B0-4AC4-A6F9-B114A06FB272} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{7F7D9641-2409-40CB-88A9-56BCE8C90A45} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
|
||||
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
||||
|------------------------------|------------------------------------------------------------|
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время, жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер |
|
||||
| `MrGameEng.Audio` | Звуковые эффекты и музыка (NVorbis) |
|
||||
| `MrGameEng.Audio` | Звуковые эффекты и музыка (NVorbis); `AudioManager` с `SoundVolume`/`MasterVolume` (одна ручка на эффекты и музыку) |
|
||||
| `MrGameEng.Content` | Пайплайн контента. **Assets** (`MrGameEng.Assets`): runtime-загрузка без Content Pipeline, кэш, `AssetRef<T>`. **Atlases** (`MrGameEng.Atlases`): текстурные атласы — сборка из дерева картинок (`AtlasBuilder`) и рантайм-загрузка (`TextureAtlas`), CLI `tools/MrGameEng.AtlasTool`. **Mods** (`MrGameEng.Mods`): система модов — порядок загрузки, JSON-дефы, локализация, слияние деревьев контента |
|
||||
| `MrGameEng.Simulation` | Детерминированные геймплей-примитивы без данных мира. **Pathfinding** (`MrGameEng.Pathfinding`): A*, Dijkstra, BFS, flow fields по гриду. **AI** (`MrGameEng.AI`): utility-ИИ — кривые отклика, соображения, действия, выбор (`UtilityAi<TContext>`), `Blackboard`. **Collisions** (`MrGameEng.Collisions`): компонент `Collider`, spatial hash, пары/запросы/raycast |
|
||||
| `MrGameEng.UI` | Игровой UI на [Myra](https://github.com/rds1983/Myra): `Desktop` на сцену, виджеты, скининг. **DevConsole** (`MrGameEng.DevConsole`): ингейм-консоль — логи `Log`, команды, история, автодополнение |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MrGameEng.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Audio.Tests;
|
||||
|
||||
public class AudioManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void MasterVolume_Default_IsOne()
|
||||
{
|
||||
using var audio = new AudioManager();
|
||||
|
||||
Assert.Equal(1f, audio.MasterVolume);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-0.5f, 0f)]
|
||||
[InlineData(0.4f, 0.4f)]
|
||||
[InlineData(1.7f, 1f)]
|
||||
public void MasterVolume_Clamps_AndMirrorsToMusic(float input, float expected)
|
||||
{
|
||||
using var audio = new AudioManager();
|
||||
|
||||
audio.MasterVolume = input;
|
||||
|
||||
Assert.Equal(expected, audio.MasterVolume);
|
||||
Assert.Equal(expected, audio.Music.Volume);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1f, 0f)]
|
||||
[InlineData(0.25f, 0.25f)]
|
||||
[InlineData(2f, 1f)]
|
||||
public void SoundVolume_Clamps(float input, float expected)
|
||||
{
|
||||
using var audio = new AudioManager();
|
||||
|
||||
audio.SoundVolume = input;
|
||||
|
||||
Assert.Equal(expected, audio.SoundVolume);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,115 @@
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class GameSpeedTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_DefaultSteps_RunsAtFirstStep()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
|
||||
var speed = new GameSpeed(clock);
|
||||
|
||||
Assert.Equal(new[] { 1f, 3f, 6f }, speed.Steps);
|
||||
Assert.False(speed.IsPaused);
|
||||
Assert.Equal(1f, speed.CurrentSpeed);
|
||||
Assert.Equal(1f, clock.TimeScale);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonPositiveStep_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new GameSpeed(new GameClock(), 1f, 0f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pause_SetsClockToZero_ResumeRestoresStep()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var speed = new GameSpeed(clock, 1f, 3f, 6f);
|
||||
speed.SetStep(2);
|
||||
|
||||
speed.Pause();
|
||||
|
||||
Assert.True(speed.IsPaused);
|
||||
Assert.Equal(0f, clock.TimeScale);
|
||||
Assert.Equal(0f, speed.CurrentSpeed);
|
||||
|
||||
speed.Resume();
|
||||
|
||||
Assert.False(speed.IsPaused);
|
||||
Assert.Equal(6f, clock.TimeScale);
|
||||
Assert.Equal(2, speed.StepIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetStep_OutOfRange_Clamps()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var speed = new GameSpeed(clock, 1f, 3f, 6f);
|
||||
|
||||
speed.SetStep(99);
|
||||
Assert.Equal(2, speed.StepIndex);
|
||||
Assert.Equal(6f, clock.TimeScale);
|
||||
|
||||
speed.SetStep(-5);
|
||||
Assert.Equal(0, speed.StepIndex);
|
||||
Assert.Equal(1f, clock.TimeScale);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FasterAndSlower_ClampAtEnds_AndUnpause()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var speed = new GameSpeed(clock, 1f, 3f, 6f);
|
||||
|
||||
speed.Faster();
|
||||
Assert.Equal(1, speed.StepIndex);
|
||||
speed.Faster();
|
||||
speed.Faster(); // clamps at fastest
|
||||
Assert.Equal(2, speed.StepIndex);
|
||||
|
||||
speed.Pause();
|
||||
speed.Slower(); // unpauses and steps down
|
||||
Assert.False(speed.IsPaused);
|
||||
Assert.Equal(1, speed.StepIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cycle_GoesPauseThroughStepsBackToPause()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var speed = new GameSpeed(clock, 1f, 3f, 6f);
|
||||
speed.Pause();
|
||||
|
||||
speed.Cycle(); // -> step 0
|
||||
Assert.False(speed.IsPaused);
|
||||
Assert.Equal(0, speed.StepIndex);
|
||||
|
||||
speed.Cycle(); // -> step 1
|
||||
Assert.Equal(1, speed.StepIndex);
|
||||
|
||||
speed.Cycle(); // -> step 2 (fastest)
|
||||
Assert.Equal(2, speed.StepIndex);
|
||||
|
||||
speed.Cycle(); // -> pause
|
||||
Assert.True(speed.IsPaused);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Changed_FiresOnTransitions()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var speed = new GameSpeed(clock, 1f, 3f, 6f);
|
||||
var count = 0;
|
||||
speed.Changed += () => count++;
|
||||
|
||||
speed.Pause();
|
||||
speed.Resume();
|
||||
speed.Faster();
|
||||
|
||||
Assert.Equal(3, count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user