Introduce a Calendar in Core that turns scaled clock time into whole in-game days plus a fraction-of-day, with a configurable SecondsPerDay. Pausing or changing TimeScale slows or stops it automatically. Pure read-side and deterministic, registered via context.UseCalendar(...), mirroring GameSpeed. Covered by xUnit tests; docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
53659450a6
commit
f791ee6c95
@@ -28,7 +28,8 @@ 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 (`GameClock` with `TimeScale`; `GameSpeed`
|
||||
for discrete pause/1×/3×/6× speed control over the clock, `context.UseGameSpeed(...)`),
|
||||
for discrete pause/1×/3×/6× speed control over the clock, `context.UseGameSpeed(...)`;
|
||||
`Calendar` turning scaled time into in-game days, `context.UseCalendar(secondsPerDay)`),
|
||||
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**
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
||||
|------------------------------|------------------------------------------------------------|
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер |
|
||||
| `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-дефы, локализация, слияние деревьев контента |
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// In-game calendar layered over <see cref="GameClock"/>: turns scaled elapsed time into whole
|
||||
/// days and a fraction-of-day (time of day). One in-game day spans <see cref="SecondsPerDay"/>
|
||||
/// seconds of scaled clock time, so pausing or changing <see cref="GameClock.TimeScale"/> slows
|
||||
/// or stops the calendar automatically. Pure read-side, GPU-free and deterministic; registered as
|
||||
/// a service via <see cref="CalendarEngineExtensions.UseCalendar"/>.
|
||||
/// </summary>
|
||||
public sealed class Calendar
|
||||
{
|
||||
private readonly GameClock _clock;
|
||||
private float _secondsPerDay;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a calendar reading <paramref name="clock"/>; one day spans
|
||||
/// <paramref name="secondsPerDay"/> seconds of scaled time (must be positive).
|
||||
/// </summary>
|
||||
public Calendar(GameClock clock, float secondsPerDay)
|
||||
{
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
SecondsPerDay = secondsPerDay;
|
||||
}
|
||||
|
||||
/// <summary>Scaled seconds per in-game day. Must be positive; raising it slows the calendar.</summary>
|
||||
public float SecondsPerDay
|
||||
{
|
||||
get => _secondsPerDay;
|
||||
set =>
|
||||
_secondsPerDay =
|
||||
value > 0f
|
||||
? value
|
||||
: throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
"Seconds per day must be positive."
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Total elapsed days as a continuous value (e.g. 3.5 = midday of day 4).</summary>
|
||||
public double TotalDays => _clock.TotalTime / _secondsPerDay;
|
||||
|
||||
/// <summary>The current day number, counting from 1.</summary>
|
||||
public int Day => (int)TotalDays + 1;
|
||||
|
||||
/// <summary>Progress through the current day in <c>[0, 1)</c> — 0 at dawn, ~0.5 at midday.</summary>
|
||||
public float DayProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
var days = TotalDays;
|
||||
return (float)(days - Math.Floor(days));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wires the in-game calendar into the engine.</summary>
|
||||
public static class CalendarEngineExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Calendar"/> bound to the context's clock and registers it as a
|
||||
/// service. Call once per world. <paramref name="secondsPerDay"/> is the scaled-time length
|
||||
/// of one in-game day (must be positive).
|
||||
/// </summary>
|
||||
public static Calendar UseCalendar(this EngineContext context, float secondsPerDay)
|
||||
{
|
||||
var calendar = new Calendar(context.Clock, secondsPerDay);
|
||||
context.Services.Add(calendar);
|
||||
return calendar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class CalendarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_NonPositiveSecondsPerDay_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Calendar(new GameClock(), 0f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TotalDays_TracksScaledClockTime()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var calendar = new Calendar(clock, secondsPerDay: 10f);
|
||||
|
||||
clock.Advance(5f); // half a day
|
||||
|
||||
Assert.Equal(0.5, calendar.TotalDays, 5);
|
||||
Assert.Equal(1, calendar.Day);
|
||||
Assert.Equal(0.5f, calendar.DayProgress, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Day_CountsFromOne_AndRollsOver()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var calendar = new Calendar(clock, secondsPerDay: 10f);
|
||||
|
||||
Assert.Equal(1, calendar.Day);
|
||||
|
||||
clock.Advance(10f); // exactly one day
|
||||
Assert.Equal(2, calendar.Day);
|
||||
Assert.Equal(0f, calendar.DayProgress, 5);
|
||||
|
||||
clock.Advance(15f); // total 25s = 2.5 days
|
||||
Assert.Equal(3, calendar.Day);
|
||||
Assert.Equal(0.5f, calendar.DayProgress, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pause_DoesNotAdvanceTheCalendar()
|
||||
{
|
||||
var clock = new GameClock { TimeScale = 0f };
|
||||
var calendar = new Calendar(clock, secondsPerDay: 10f);
|
||||
|
||||
clock.Advance(100f);
|
||||
|
||||
Assert.Equal(0.0, calendar.TotalDays, 5);
|
||||
Assert.Equal(1, calendar.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeScale_SpeedsUpTheCalendar()
|
||||
{
|
||||
var slow = new GameClock();
|
||||
var fast = new GameClock { TimeScale = 6f };
|
||||
var slowCal = new Calendar(slow, secondsPerDay: 10f);
|
||||
var fastCal = new Calendar(fast, secondsPerDay: 10f);
|
||||
|
||||
slow.Advance(10f);
|
||||
fast.Advance(10f);
|
||||
|
||||
Assert.Equal(6.0, fastCal.TotalDays / slowCal.TotalDays, 5);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user