Split the platform out of Core: new Host library + HeadlessHost
CI / build-test (push) Successful in 1m24s
CI / build-test (push) Successful in 1m24s
Core now depends only on Friflo.Engine.ECS — no MonoGame, no platform. The windowed MonoGame host moves to the new MrGameEng.Host library: GameHost, GameHostOptions, visual scene transitions (OverlayTransition, Transitions.Fade/Wipe, TransitionRenderer) and the Input feature (namespace MrGameEng.Input is unchanged). Transition timing stays in Core (SceneManager exposes ActiveTransition/TransitionCoverage/ TransitionPhase; the host draws the overlay). EngineContext loses its GraphicsDevice property: hosts publish the device as a service and graphics code reads it via context.GetGraphicsDevice() in Graphics. Core gains HeadlessHost: a fixed-timestep loop without a window or GPU (Tick/RunTicks, Run with wall-clock pacing and lag resync) for dedicated servers, batch simulation and tests. Graphics and Audio now carry their own MonoGame.Framework.DesktopGL reference instead of inheriting it from Core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
79d406a9f4
commit
2ac074004a
@@ -27,13 +27,18 @@ is the living showcase — new engine features are demonstrated there.
|
|||||||
|
|
||||||
Engine libraries (each feature is a namespaced subfolder of its host):
|
Engine libraries (each feature is a namespaced subfolder of its host):
|
||||||
|
|
||||||
- **`Core`** — game loop, ECS world, scenes, time (`GameClock` with `TimeScale`; `GameSpeed`
|
- **`Core`** — the platform-free kernel: ECS world, scenes and transition timing, time
|
||||||
for discrete pause/1×/3×/6× speed control over the clock, `context.UseGameSpeed(...)`;
|
(`GameClock` with `TimeScale`; `GameSpeed` for discrete pause/1×/3×/6× speed control over
|
||||||
`Calendar` turning scaled time into in-game days, `context.UseCalendar(secondsPerDay)`;
|
the clock, `context.UseGameSpeed(...)`; `Calendar` turning scaled time into in-game days,
|
||||||
`Climate` — continuous seasonal/daily temperature and season over the calendar,
|
`context.UseCalendar(secondsPerDay)`; `Climate` — continuous seasonal/daily temperature
|
||||||
`context.UseClimate(settings)`),
|
and season over the calendar, `context.UseClimate(settings)`), services, logging, and
|
||||||
plus **Input** (`MrGameEng.Input`: `InputManager`, `ActionMap`, `InputSystem`, in
|
**`HeadlessHost`** — a fixed-timestep loop without a window or GPU (dedicated servers,
|
||||||
`Core/Input/`). Depends only on MonoGame and Friflo.Engine.ECS.
|
batch simulation, tests). Depends only on Friflo.Engine.ECS — no MonoGame, no platform.
|
||||||
|
- **`Host`** — the windowed MonoGame host: `GameHost` (wraps `Game`, owns the window and
|
||||||
|
`GraphicsDeviceManager`, publishes `GraphicsDevice` as a service), visual scene
|
||||||
|
transitions (`OverlayTransition`, `Transitions.Fade/Wipe`, `TransitionRenderer`), plus
|
||||||
|
**Input** (`MrGameEng.Input`: `InputManager`, `ActionMap`, `InputSystem`, in
|
||||||
|
`Host/Input/`). Nothing depends on `Host` except the game itself. → `Core`.
|
||||||
- **`Graphics`** — custom batched renderer, camera, sprites, plus **Tilemaps**
|
- **`Graphics`** — custom batched renderer, camera, sprites, plus **Tilemaps**
|
||||||
(`MrGameEng.Tilemaps`: code-built tile grids rendered through the batcher,
|
(`MrGameEng.Tilemaps`: code-built tile grids rendered through the batcher,
|
||||||
`scene.UseTilemaps()` after `UseRenderer2D()`, in `Graphics/Tilemaps/`) and **Lighting**
|
`scene.UseTilemaps()` after `UseRenderer2D()`, in `Graphics/Tilemaps/`) and **Lighting**
|
||||||
@@ -70,9 +75,13 @@ Engine libraries (each feature is a namespaced subfolder of its host):
|
|||||||
netstandard2.0 analyzer.
|
netstandard2.0 analyzer.
|
||||||
|
|
||||||
Dependency rule: a library may depend only on `Core` and `Graphics`; `Core` depends only
|
Dependency rule: a library may depend only on `Core` and `Graphics`; `Core` depends only
|
||||||
on MonoGame and Friflo.Engine.ECS. Features grouped into one library share its package
|
on Friflo.Engine.ECS (no MonoGame — the simulation must run headless). MonoGame is pulled
|
||||||
set (e.g. `Content` carries both FontStash and StbImage) — keep optional/heavy deps
|
in by the platform/graphics libraries (`Host`, `Graphics`, `Audio`). Platform resources
|
||||||
(Myra, NVorbis) in their own library so the rest of the engine stays free of them.
|
(e.g. `GraphicsDevice`) are published by hosts as services in `EngineContext.Services`;
|
||||||
|
graphics code reaches the device via `context.GetGraphicsDevice()` (extension in
|
||||||
|
`Graphics`). Features grouped into one library share its package set (e.g. `Content`
|
||||||
|
carries both FontStash and StbImage) — keep optional/heavy deps (Myra, NVorbis) in their
|
||||||
|
own library so the rest of the engine stays free of them.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@@ -13,5 +12,4 @@
|
|||||||
<PropertyGroup Condition="$(MSBuildProjectName.StartsWith('MrGameEng.')) AND !$(MSBuildProjectName.EndsWith('.Tests')) AND !$(MSBuildProjectName.EndsWith('.Generator')) AND !$(MSBuildProjectName.EndsWith('.Sample'))">
|
<PropertyGroup Condition="$(MSBuildProjectName.StartsWith('MrGameEng.')) AND !$(MSBuildProjectName.EndsWith('.Tests')) AND !$(MSBuildProjectName.EndsWith('.Generator')) AND !$(MSBuildProjectName.EndsWith('.Sample'))">
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "tests
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{4407F6E6-0B65-41A3-ADFA-B78684A9B918}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{4407F6E6-0B65-41A3-ADFA-B78684A9B918}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "src\MrGameEng.Host\MrGameEng.Host.csproj", "{59818072-0D2B-4007-A50F-1343FA189EC6}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -229,6 +233,30 @@ Global
|
|||||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x86.Build.0 = Release|Any CPU
|
{4407F6E6-0B65-41A3-ADFA-B78684A9B918}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -249,5 +277,7 @@ Global
|
|||||||
{C34F2AFF-F2B0-4AC4-A6F9-B114A06FB272} = {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}
|
{7F7D9641-2409-40CB-88A9-56BCE8C90A45} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
{4407F6E6-0B65-41A3-ADFA-B78684A9B918} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
{4407F6E6-0B65-41A3-ADFA-B78684A9B918} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
|
{59818072-0D2B-4007-A50F-1343FA189EC6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
|
{B3EB5318-5EC0-4F23-8ECA-0EC5A0C4DFD2} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
+25
-13
@@ -33,7 +33,8 @@
|
|||||||
|
|
||||||
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
||||||
|------------------------------|------------------------------------------------------------|
|
|------------------------------|------------------------------------------------------------|
|
||||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`; `Climate` — непрерывная сезонная/суточная температура и сезон поверх календаря, `context.UseClimate(...)`), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
| `MrGameEng.Core` | Платформо-независимое ядро (зависит только от Friflo): `EngineContext`, `EntityStore`, `SystemRoot`, сцены и переходы (тайминг — `Transition`, `SceneManager`; визуал переходов — в `Host`), время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`; `Climate` — непрерывная сезонная/суточная температура и сезон поверх календаря, `context.UseClimate(...)`), жизненный цикл, `ServiceRegistry`, `Log`. **`HeadlessHost`** — цикл без окна и GPU на фиксированном тике (`Tick`/`RunTicks`/`Run` с realtime-пейсингом): дедикейтед-серверы, батч-симуляция, тесты |
|
||||||
|
| `MrGameEng.Host` | Оконный MonoGame-хост: `GameHost` (обёртка над `Game` — цикл, окно, `GraphicsDeviceManager`; публикует `GraphicsDevice` сервисом в контексте), визуальные переходы сцен (`OverlayTransition`, фабрики `Transitions.Fade/Wipe`, `TransitionRenderer`). **Input** (`MrGameEng.Input`, `Host/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер. **Lighting** (`MrGameEng.Lighting`, `Graphics/Lighting/`): амбиент день/ночь поверх `Calendar` → `Renderer2D.AmbientLight`, `scene.UseDayNight(renderer)`; по-клеточный лайтмап — `LightmapBuilder` (амбиент × окклюзия + точечные `PointLight` с трассировкой теней), накладывается multiply поверх мира через `scene.UseLighting(...)`, сэмплируется `Lighting.SampleAt` |
|
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер. **Lighting** (`MrGameEng.Lighting`, `Graphics/Lighting/`): амбиент день/ночь поверх `Calendar` → `Renderer2D.AmbientLight`, `scene.UseDayNight(renderer)`; по-клеточный лайтмап — `LightmapBuilder` (амбиент × окклюзия + точечные `PointLight` с трассировкой теней), накладывается multiply поверх мира через `scene.UseLighting(...)`, сэмплируется `Lighting.SampleAt` |
|
||||||
| `MrGameEng.Audio` | Звуковые эффекты и музыка (NVorbis); `AudioManager` с `SoundVolume`/`MasterVolume` (одна ручка на эффекты и музыку) |
|
| `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.Content` | Пайплайн контента. **Assets** (`MrGameEng.Assets`): runtime-загрузка без Content Pipeline, кэш, `AssetRef<T>`. **Atlases** (`MrGameEng.Atlases`): текстурные атласы — сборка из дерева картинок (`AtlasBuilder`) и рантайм-загрузка (`TextureAtlas`), CLI `tools/MrGameEng.AtlasTool`. **Mods** (`MrGameEng.Mods`): система модов — порядок загрузки, JSON-дефы, локализация, слияние деревьев контента |
|
||||||
@@ -69,19 +70,27 @@
|
|||||||
### Правило зависимостей
|
### Правило зависимостей
|
||||||
|
|
||||||
```
|
```
|
||||||
MrGameEng.Audio ─┐
|
MrGameEng.Host ─┐
|
||||||
MrGameEng.Graphics ─┼──► MrGameEng.Core ──► MonoGame.Framework.DesktopGL
|
MrGameEng.Audio ─┤
|
||||||
│ └──► Friflo.Engine.ECS
|
MrGameEng.Graphics ─┼──► MrGameEng.Core ──► Friflo.Engine.ECS
|
||||||
|
│
|
||||||
MrGameEng.Content ─┤
|
MrGameEng.Content ─┤
|
||||||
MrGameEng.Simulation ─┤ (Content — за Texture2DRegion,
|
MrGameEng.Simulation ─┤ (Content — за Texture2DRegion,
|
||||||
MrGameEng.UI ─┴──► MrGameEng.Graphics Simulation/UI — за Transform2D/рендер)
|
MrGameEng.UI ─┴──► MrGameEng.Graphics Simulation/UI — за Transform2D/рендер)
|
||||||
```
|
```
|
||||||
|
|
||||||
Библиотека зависит **только от `Core` и `Graphics`**. `Core` зависит только от MonoGame
|
Библиотека зависит **только от `Core` и `Graphics`**. `Core` зависит только от Friflo —
|
||||||
и Friflo. Если двум библиотекам нужен общий тип — он переезжает в `Core` (или, если это
|
ни MonoGame, ни другой платформы: благодаря этому симуляция запускается и без окна
|
||||||
графический тип, в `Graphics`). `Content` тянет `Graphics` (атласы выдают
|
(`HeadlessHost`). MonoGame (`MonoGame.Framework.DesktopGL`) тянут платформенные и
|
||||||
`Texture2DRegion`); `Simulation` тянет `Graphics` (`Collisions` использует `Transform2D`,
|
графические библиотеки: `Host`, `Graphics`, `Audio` (и транзитивно их потребители).
|
||||||
`RectF`); `UI` — только `Core` (Myra рисует своим SpriteBatch).
|
Платформенные ресурсы (например `GraphicsDevice`) хосты публикуют сервисами в
|
||||||
|
`EngineContext.Services`; графический код достаёт девайс через
|
||||||
|
`context.GetGraphicsDevice()` (расширение в `Graphics`). На `Host` не зависит никто,
|
||||||
|
кроме самой игры — это точка входа оконной платформы. Если двум библиотекам нужен общий
|
||||||
|
тип — он переезжает в `Core` (или, если это графический тип, в `Graphics`). `Content`
|
||||||
|
тянет `Graphics` (атласы выдают `Texture2DRegion`); `Simulation` тянет `Graphics`
|
||||||
|
(`Collisions` использует `Transform2D`, `RectF`); `UI` — только `Core` (Myra рисует
|
||||||
|
своим SpriteBatch).
|
||||||
|
|
||||||
Фичи, собранные в одну библиотеку, делят её набор пакетов (например, `Content` несёт и
|
Фичи, собранные в одну библиотеку, делят её набор пакетов (например, `Content` несёт и
|
||||||
FontStash, и StbImage). Тяжёлые/опциональные зависимости (Myra, NVorbis) держим в
|
FontStash, и StbImage). Тяжёлые/опциональные зависимости (Myra, NVorbis) держим в
|
||||||
@@ -345,12 +354,15 @@ CLI-обёртка: `dotnet run --project tools/MrGameEng.AtlasTool -- <исто
|
|||||||
|
|
||||||
- `SceneManager` владеет активной сценой; обычное переключение откладывается до начала
|
- `SceneManager` владеет активной сценой; обычное переключение откладывается до начала
|
||||||
следующего кадра (сцена никогда не выгружается посреди собственного кадра).
|
следующего кадра (сцена никогда не выгружается посреди собственного кадра).
|
||||||
- `Scenes.Switch(scene, Transition.Fade(0.5f))` — переключение с визуальным переходом:
|
- `Scenes.Switch(scene, Transitions.Fade(0.5f))` — переключение с визуальным переходом:
|
||||||
фаза закрытия (старая сцена живёт) → своп при полном покрытии → фаза открытия.
|
фаза закрытия (старая сцена живёт) → своп при полном покрытии → фаза открытия.
|
||||||
Тяжёлый `OnLoad` новой сцены скрыт за полностью закрытым экраном.
|
Тяжёлый `OnLoad` новой сцены скрыт за полностью закрытым экраном.
|
||||||
- Встроенные переходы: `Transition.Fade(duration, color)` и `Transition.Wipe(duration, color)`
|
- Тайминг-машина (`Transition` — длительности фаз, покрытие) живёт в `Core` и работает
|
||||||
(шторка). Свои — наследованием от `Transition` (рисование через `TransitionRenderer.Fill`
|
и в headless-контексте; визуал — в `Host`: встроенные `Transitions.Fade(duration, color)`
|
||||||
в нормализованных координатах экрана).
|
и `Transitions.Wipe(duration, color)` (шторка). Свои — наследованием от
|
||||||
|
`OverlayTransition` (рисование через `TransitionRenderer.Fill` в нормализованных
|
||||||
|
координатах экрана); `GameHost` рисует оверлей поверх сцены, читая
|
||||||
|
`Scenes.ActiveTransition`/`TransitionCoverage`/`TransitionPhase`.
|
||||||
- Переходы идут по **unscaled**-времени: работают при паузе геймплея (`TimeScale = 0`).
|
- Переходы идут по **unscaled**-времени: работают при паузе геймплея (`TimeScale = 0`).
|
||||||
- Повторный `Switch` во время перехода заменяет целевую сцену, не перезапуская переход.
|
- Повторный `Switch` во время перехода заменяет целевую сцену, не перезапуская переход.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NVorbis" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
||||||
</ItemGroup>
|
<PackageReference Include="NVorbis" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
<ItemGroup>
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||||
</Project>
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using FontStashSharp;
|
|||||||
using Microsoft.Xna.Framework.Audio;
|
using Microsoft.Xna.Framework.Audio;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
namespace MrGameEng.Assets;
|
namespace MrGameEng.Assets;
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ public sealed class AssetManager : IDisposable
|
|||||||
{
|
{
|
||||||
using var stream = File.OpenRead(path);
|
using var stream = File.OpenRead(path);
|
||||||
return Texture2D.FromStream(
|
return Texture2D.FromStream(
|
||||||
context.GraphicsDevice,
|
context.GetGraphicsDevice(),
|
||||||
stream,
|
stream,
|
||||||
DefaultColorProcessors.PremultiplyAlpha
|
DefaultColorProcessors.PremultiplyAlpha
|
||||||
);
|
);
|
||||||
@@ -123,7 +124,7 @@ public sealed class AssetManager : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Effect LoadEffect(EngineContext context, string path) =>
|
private static Effect LoadEffect(EngineContext context, string path) =>
|
||||||
new(context.GraphicsDevice, File.ReadAllBytes(path));
|
new(context.GetGraphicsDevice(), File.ReadAllBytes(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Wires the assets module into the engine.</summary>
|
/// <summary>Wires the assets module into the engine.</summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MrGameEng.Assets;
|
using MrGameEng.Assets;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
namespace MrGameEng.Atlases;
|
namespace MrGameEng.Atlases;
|
||||||
|
|
||||||
@@ -14,6 +15,6 @@ public static class AtlasesEngineExtensions
|
|||||||
public static void UseTextureAtlases(this EngineContext context)
|
public static void UseTextureAtlases(this EngineContext context)
|
||||||
{
|
{
|
||||||
var assets = context.Services.Get<AssetManager>();
|
var assets = context.Services.Get<AssetManager>();
|
||||||
assets.RegisterLoader((_, path) => TextureAtlas.Load(context.GraphicsDevice, path));
|
assets.RegisterLoader((_, path) => TextureAtlas.Load(context.GetGraphicsDevice(), path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
|
|
||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root object handed to scenes and systems: time, scene manager, services and graphics device.
|
/// Root object handed to scenes and systems: time, scene manager and services.
|
||||||
/// Created by <see cref="GameHost"/>; can also be created standalone for headless tests.
|
/// Created by a host (the MonoGame game host or <see cref="HeadlessHost"/>); can also be
|
||||||
|
/// created standalone for unit tests. Platform resources such as the graphics device are
|
||||||
|
/// published through <see cref="Services"/> by hosts that have them — the core itself has
|
||||||
|
/// no platform dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EngineContext
|
public sealed class EngineContext
|
||||||
{
|
{
|
||||||
@@ -17,38 +18,16 @@ public sealed class EngineContext
|
|||||||
/// <summary>Registry of module services (input, audio, assets, …).</summary>
|
/// <summary>Registry of module services (input, audio, assets, …).</summary>
|
||||||
public ServiceRegistry Services { get; } = new();
|
public ServiceRegistry Services { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Creates a context. Games normally never create one themselves — the host does.</summary>
|
||||||
/// The graphics device. Available once the host is initialized;
|
|
||||||
/// throws when accessed in a headless context (unit tests).
|
|
||||||
/// </summary>
|
|
||||||
public GraphicsDevice GraphicsDevice =>
|
|
||||||
_graphicsDevice
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"GraphicsDevice is not available (headless context)."
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>True when a graphics device is attached.</summary>
|
|
||||||
public bool HasGraphicsDevice => _graphicsDevice is not null;
|
|
||||||
|
|
||||||
private GraphicsDevice? _graphicsDevice;
|
|
||||||
|
|
||||||
/// <summary>Creates a context. Games normally never create one themselves — <see cref="GameHost"/> does.</summary>
|
|
||||||
public EngineContext()
|
public EngineContext()
|
||||||
{
|
{
|
||||||
Scenes = new SceneManager(this);
|
Scenes = new SceneManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AttachGraphicsDevice(GraphicsDevice device) => _graphicsDevice = device;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disposes everything the context owns: registered <see cref="IDisposable"/> services
|
/// Disposes everything the context owns: registered <see cref="IDisposable"/> services.
|
||||||
/// and the transition renderer. <paramref name="except"/> (the host itself, also a
|
/// Instances in <paramref name="except"/> (the host itself, platform resources the host
|
||||||
/// registered service) is skipped — it is being disposed by the caller already.
|
/// disposes on its own) are skipped. Called by hosts on shutdown.
|
||||||
/// Called by <see cref="GameHost.Dispose(bool)"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void DisposeOwnedResources(object except)
|
internal void DisposeOwnedResources(params object[] except) => Services.DisposeServices(except);
|
||||||
{
|
|
||||||
Scenes.DisposeRenderer();
|
|
||||||
Services.DisposeServices(except);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace MrGameEng.Core;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Engine time service: per-frame delta, total elapsed time, time scaling and frame counter.
|
/// Engine time service: per-frame delta, total elapsed time, time scaling and frame counter.
|
||||||
/// Advanced once per frame by <see cref="GameHost"/>.
|
/// Advanced once per frame (or per fixed tick) by the host.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GameClock
|
public sealed class GameClock
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A game-loop host without a window, GPU or any platform dependency: drives the active
|
||||||
|
/// scene's update phase on a fixed timestep. Suits dedicated servers, batch simulation and
|
||||||
|
/// tests. Scenes still own draw systems — they are simply never run, and platform services
|
||||||
|
/// (graphics device, window) are absent from <see cref="EngineContext.Services"/>, so only
|
||||||
|
/// platform-free modules can be used. The fixed step makes simulation time independent of
|
||||||
|
/// wall-clock jitter: N ticks always advance the world by exactly N ×
|
||||||
|
/// <see cref="FixedDeltaTime"/> seconds.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HeadlessHost : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>Engine context shared with scenes and systems.</summary>
|
||||||
|
public EngineContext Context { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Fixed simulation step in seconds: 1 / <see cref="HeadlessHostOptions.TicksPerSecond"/>.</summary>
|
||||||
|
public float FixedDeltaTime { get; }
|
||||||
|
|
||||||
|
/// <summary>Ticks completed since the host was created.</summary>
|
||||||
|
public long TickCount => Context.Clock.FrameCount;
|
||||||
|
|
||||||
|
// Отстав сильнее этого, Run ресинкается с настоящим временем вместо лавины тиков.
|
||||||
|
private const double MaxLagSeconds = 1.0;
|
||||||
|
|
||||||
|
private readonly HeadlessHostOptions _options;
|
||||||
|
|
||||||
|
/// <summary>Creates a host that starts with <paramref name="initialScene"/>. The scene
|
||||||
|
/// loads on the first tick, mirroring the windowed host's deferred switch.</summary>
|
||||||
|
public HeadlessHost(HeadlessHostOptions options, Scene initialScene)
|
||||||
|
{
|
||||||
|
if (options.TicksPerSecond <= 0f)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(options),
|
||||||
|
"TicksPerSecond must be positive."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_options = options;
|
||||||
|
FixedDeltaTime = 1f / options.TicksPerSecond;
|
||||||
|
Context.Scenes.Switch(initialScene);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Advances the world by exactly one fixed tick.</summary>
|
||||||
|
public void Tick()
|
||||||
|
{
|
||||||
|
Context.Clock.Advance(FixedDeltaTime);
|
||||||
|
Context.Scenes.Update(Context.Clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Advances the world by <paramref name="count"/> ticks as fast as possible.</summary>
|
||||||
|
public void RunTicks(long count)
|
||||||
|
{
|
||||||
|
for (long i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs until <paramref name="cancellationToken"/> is cancelled. With
|
||||||
|
/// <see cref="HeadlessHostOptions.Realtime"/> ticks are paced to the wall clock — the
|
||||||
|
/// loop sleeps when ahead and, having fallen more than a second behind, resyncs instead
|
||||||
|
/// of bursting a catch-up avalanche. Pacing relies on <see cref="Thread.Sleep(TimeSpan)"/>
|
||||||
|
/// and is accurate to a few milliseconds, not exact.
|
||||||
|
/// </summary>
|
||||||
|
public void Run(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!_options.Realtime)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallClock = Stopwatch.StartNew();
|
||||||
|
var nextTickAt = 0.0;
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Tick();
|
||||||
|
nextTickAt += FixedDeltaTime;
|
||||||
|
var ahead = nextTickAt - wallClock.Elapsed.TotalSeconds;
|
||||||
|
if (ahead > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(ahead));
|
||||||
|
}
|
||||||
|
else if (-ahead > MaxLagSeconds)
|
||||||
|
{
|
||||||
|
nextTickAt = wallClock.Elapsed.TotalSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Unloads the active scene and disposes context-owned services.</summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(null);
|
||||||
|
Context.Scenes.ApplyPending();
|
||||||
|
Context.DisposeOwnedResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
|
/// <summary>Loop settings for <see cref="HeadlessHost"/>.</summary>
|
||||||
|
public sealed class HeadlessHostOptions
|
||||||
|
{
|
||||||
|
/// <summary>Fixed simulation rate in ticks per second. Must be positive.</summary>
|
||||||
|
public float TicksPerSecond { get; set; } = 60f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <see cref="HeadlessHost.Run"/> paces ticks to the wall clock (a dedicated
|
||||||
|
/// server); when false it runs flat out (batch simulation). <see cref="HeadlessHost.RunTicks"/>
|
||||||
|
/// always runs flat out regardless of this setting.
|
||||||
|
/// </summary>
|
||||||
|
public bool Realtime { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
|
||||||
<PackageReference Include="Friflo.Engine.ECS" />
|
<PackageReference Include="Friflo.Engine.ECS" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="MrGameEng.Core.Tests" />
|
<InternalsVisibleTo Include="MrGameEng.Core.Tests" />
|
||||||
|
<InternalsVisibleTo Include="MrGameEng.Host" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -22,13 +22,24 @@ public sealed class SceneManager
|
|||||||
/// <summary>True while a transition is covering or revealing.</summary>
|
/// <summary>True while a transition is covering or revealing.</summary>
|
||||||
public bool IsTransitioning => _state != State.Idle;
|
public bool IsTransitioning => _state != State.Idle;
|
||||||
|
|
||||||
|
/// <summary>The transition currently covering or revealing, or null while idle.
|
||||||
|
/// Hosts that render overlays read this together with <see cref="TransitionCoverage"/>
|
||||||
|
/// and <see cref="TransitionPhase"/> during their draw phase.</summary>
|
||||||
|
public Transition? ActiveTransition => _state == State.Idle ? null : _transition;
|
||||||
|
|
||||||
|
/// <summary>Coverage of the active transition: 0 = scene fully visible, 1 = fully covered.</summary>
|
||||||
|
public float TransitionCoverage => Math.Clamp(_coverage, 0f, 1f);
|
||||||
|
|
||||||
|
/// <summary>Phase of the active transition. Meaningful only while <see cref="IsTransitioning"/>.</summary>
|
||||||
|
public TransitionPhase TransitionPhase =>
|
||||||
|
_state == State.CoveringOut ? TransitionPhase.Out : TransitionPhase.In;
|
||||||
|
|
||||||
private readonly EngineContext _context;
|
private readonly EngineContext _context;
|
||||||
private Scene? _pending;
|
private Scene? _pending;
|
||||||
private bool _hasPending;
|
private bool _hasPending;
|
||||||
private Transition? _transition;
|
private Transition? _transition;
|
||||||
private State _state;
|
private State _state;
|
||||||
private float _coverage;
|
private float _coverage;
|
||||||
private TransitionRenderer? _renderer;
|
|
||||||
|
|
||||||
internal SceneManager(EngineContext context) => _context = context;
|
internal SceneManager(EngineContext context) => _context = context;
|
||||||
|
|
||||||
@@ -100,27 +111,9 @@ public sealed class SceneManager
|
|||||||
Current?.Update(clock);
|
Current?.Update(clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Draws the active scene and the transition overlay on top. Called by the host.</summary>
|
/// <summary>Draws the active scene. Transition overlays are rendered by the host on top,
|
||||||
public void Draw(GameClock clock)
|
/// from <see cref="ActiveTransition"/> and <see cref="TransitionCoverage"/>.</summary>
|
||||||
{
|
public void Draw(GameClock clock) => Current?.Draw(clock);
|
||||||
Current?.Draw(clock);
|
|
||||||
|
|
||||||
if (_state == State.Idle || !_context.HasGraphicsDevice)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderer ??= new TransitionRenderer(_context.GraphicsDevice);
|
|
||||||
var phase = _state == State.CoveringOut ? TransitionPhase.Out : TransitionPhase.In;
|
|
||||||
_transition!.Draw(_renderer, Math.Clamp(_coverage, 0f, 1f), phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Disposes the lazily created transition renderer. Called on host shutdown.</summary>
|
|
||||||
internal void DisposeRenderer()
|
|
||||||
{
|
|
||||||
_renderer?.Dispose();
|
|
||||||
_renderer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ApplyPending()
|
internal void ApplyPending()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,16 +40,17 @@ public sealed class ServiceRegistry
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disposes every registered <see cref="IDisposable"/> service (each instance once, even
|
/// Disposes every registered <see cref="IDisposable"/> service (each instance once, even
|
||||||
/// when registered under several types) and clears the registry. <paramref name="except"/>
|
/// when registered under several types) and clears the registry. Instances in
|
||||||
/// is skipped. Called on host shutdown.
|
/// <paramref name="except"/> are skipped. Called on host shutdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void DisposeServices(object? except = null)
|
internal void DisposeServices(params object[] except)
|
||||||
{
|
{
|
||||||
|
var skipped = new HashSet<object>(except, ReferenceEqualityComparer.Instance);
|
||||||
var disposed = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
var disposed = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
||||||
foreach (var service in _services.Values)
|
foreach (var service in _services.Values)
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
!ReferenceEquals(service, except)
|
!skipped.Contains(service)
|
||||||
&& service is IDisposable disposable
|
&& service is IDisposable disposable
|
||||||
&& disposed.Add(service)
|
&& disposed.Add(service)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using Microsoft.Xna.Framework;
|
|
||||||
|
|
||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Core;
|
||||||
|
|
||||||
/// <summary>Phase of a scene transition.</summary>
|
/// <summary>Phase of a scene transition.</summary>
|
||||||
@@ -13,10 +11,12 @@ public enum TransitionPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visual transition between scenes. The scene switch itself happens at full coverage,
|
/// Timing of a visual transition between scenes. The scene switch itself happens at full
|
||||||
/// so a slow <c>OnLoad</c> of the next scene is hidden behind the overlay.
|
/// coverage, so a slow <c>OnLoad</c> of the next scene is hidden behind the overlay.
|
||||||
/// Transitions are stateless and reusable; progress is tracked by <see cref="SceneManager"/>.
|
/// Progress is tracked by <see cref="SceneManager"/> on unscaled time, so transitions work
|
||||||
/// Runs on unscaled time, so it works while gameplay is paused.
|
/// while gameplay is paused. The core only times the phases — how the overlay looks is
|
||||||
|
/// defined by the host (the MonoGame host's <c>OverlayTransition</c> and the
|
||||||
|
/// <c>Transitions</c> factories); headless hosts simply let transitions pass invisibly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Transition
|
public abstract class Transition
|
||||||
{
|
{
|
||||||
@@ -32,49 +32,4 @@ public abstract class Transition
|
|||||||
OutDuration = Math.Max(0f, outDuration);
|
OutDuration = Math.Max(0f, outDuration);
|
||||||
InDuration = Math.Max(0f, inDuration);
|
InDuration = Math.Max(0f, inDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draws the overlay. <paramref name="coverage"/> is 0 (scene fully visible) to
|
|
||||||
/// 1 (scene fully covered); <paramref name="phase"/> tells which side of the switch this is.
|
|
||||||
/// </summary>
|
|
||||||
public abstract void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase);
|
|
||||||
|
|
||||||
/// <summary>Fade through a solid color (black by default). Total duration is split between out and in.</summary>
|
|
||||||
public static Transition Fade(float duration = 0.6f, Color? color = null) =>
|
|
||||||
new FadeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
|
||||||
|
|
||||||
/// <summary>A curtain wiping across the screen (black by default). Total duration is split between out and in.</summary>
|
|
||||||
public static Transition Wipe(float duration = 0.6f, Color? color = null) =>
|
|
||||||
new WipeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
|
||||||
|
|
||||||
private sealed class FadeTransition(float outDuration, float inDuration, Color color)
|
|
||||||
: Transition(outDuration, inDuration)
|
|
||||||
{
|
|
||||||
public override void Draw(
|
|
||||||
TransitionRenderer renderer,
|
|
||||||
float coverage,
|
|
||||||
TransitionPhase phase
|
|
||||||
) => renderer.Fill(0f, 0f, 1f, 1f, color, coverage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class WipeTransition(float outDuration, float inDuration, Color color)
|
|
||||||
: Transition(outDuration, inDuration)
|
|
||||||
{
|
|
||||||
public override void Draw(
|
|
||||||
TransitionRenderer renderer,
|
|
||||||
float coverage,
|
|
||||||
TransitionPhase phase
|
|
||||||
)
|
|
||||||
{
|
|
||||||
// Out: шторка растёт слева направо; In: уезжает дальше вправо.
|
|
||||||
if (phase == TransitionPhase.Out)
|
|
||||||
{
|
|
||||||
renderer.Fill(0f, 0f, coverage, 1f, color);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
renderer.Fill(1f - coverage, 0f, coverage, 1f, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
|
namespace MrGameEng.Graphics;
|
||||||
|
|
||||||
|
/// <summary>Graphics-side accessors for the platform-free <see cref="EngineContext"/>.</summary>
|
||||||
|
public static class EngineContextGraphicsExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <see cref="GraphicsDevice"/> service published by a windowed host.
|
||||||
|
/// Throws in a headless context, where no graphics device exists.
|
||||||
|
/// </summary>
|
||||||
|
public static GraphicsDevice GetGraphicsDevice(this EngineContext context) =>
|
||||||
|
context.Services.GetOrDefault<GraphicsDevice>()
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"GraphicsDevice is not available: no windowed host has published it "
|
||||||
|
+ "(headless context, or graphics are not initialized yet)."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ public static class SceneLightmapExtensions
|
|||||||
Func<bool[]> occluders
|
Func<bool[]> occluders
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var device = scene.Context.GraphicsDevice;
|
var device = scene.Context.GetGraphicsDevice();
|
||||||
var lightmap = new Lightmap(device, width, height, cellSize, origin);
|
var lightmap = new Lightmap(device, width, height, cellSize, origin);
|
||||||
var lighting = new Lighting(lightmap);
|
var lighting = new Lighting(lightmap);
|
||||||
scene.Context.Services.Add(lighting);
|
scene.Context.Services.Add(lighting);
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="MrGameEng.Graphics.Tests" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
<InternalsVisibleTo Include="MrGameEng.Graphics.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public static class SceneGraphicsExtensions
|
|||||||
var renderer = services.GetOrDefault<Renderer2D>();
|
var renderer = services.GetOrDefault<Renderer2D>();
|
||||||
if (renderer is null)
|
if (renderer is null)
|
||||||
{
|
{
|
||||||
renderer = new Renderer2D(scene.Context.GraphicsDevice, options);
|
renderer = new Renderer2D(scene.Context.GetGraphicsDevice(), options);
|
||||||
services.Add(renderer);
|
services.Add(renderer);
|
||||||
}
|
}
|
||||||
else if (options is not null)
|
else if (options is not null)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Host;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The engine's game loop host. Wraps MonoGame's <see cref="Game"/>: owns the
|
/// The engine's windowed game-loop host. Wraps MonoGame's <see cref="Game"/>: owns the
|
||||||
/// <see cref="EngineContext"/>, advances the <see cref="GameClock"/> and drives the
|
/// <see cref="EngineContext"/>, advances the <see cref="GameClock"/>, drives the active
|
||||||
/// active scene's update and draw phases.
|
/// scene's update and draw phases and renders scene-transition overlays. The
|
||||||
|
/// <see cref="GraphicsDevice"/> is published as a service so graphics modules can reach it
|
||||||
|
/// through the context. For a loop without a window or GPU see
|
||||||
|
/// <see cref="HeadlessHost"/> in the core.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GameHost : Game
|
public class GameHost : Game
|
||||||
{
|
{
|
||||||
@@ -17,6 +22,7 @@ public class GameHost : Game
|
|||||||
|
|
||||||
private readonly GameHostOptions _options;
|
private readonly GameHostOptions _options;
|
||||||
private readonly Scene _initialScene;
|
private readonly Scene _initialScene;
|
||||||
|
private TransitionRenderer? _transitionRenderer;
|
||||||
|
|
||||||
/// <summary>Creates a host that starts with <paramref name="initialScene"/>.</summary>
|
/// <summary>Creates a host that starts with <paramref name="initialScene"/>.</summary>
|
||||||
public GameHost(GameHostOptions options, Scene initialScene)
|
public GameHost(GameHostOptions options, Scene initialScene)
|
||||||
@@ -48,7 +54,7 @@ public class GameHost : Game
|
|||||||
{
|
{
|
||||||
Window.Title = _options.Title;
|
Window.Title = _options.Title;
|
||||||
Window.AllowUserResizing = _options.AllowResizing;
|
Window.AllowUserResizing = _options.AllowResizing;
|
||||||
Context.AttachGraphicsDevice(GraphicsDevice);
|
Context.Services.Add(GraphicsDevice);
|
||||||
Context.Services.Add(Window);
|
Context.Services.Add(Window);
|
||||||
Context.Services.Add<Game>(this);
|
Context.Services.Add<Game>(this);
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -68,9 +74,25 @@ public class GameHost : Game
|
|||||||
{
|
{
|
||||||
GraphicsDevice.Clear(_options.ClearColor);
|
GraphicsDevice.Clear(_options.ClearColor);
|
||||||
Context.Scenes.Draw(Context.Clock);
|
Context.Scenes.Draw(Context.Clock);
|
||||||
|
DrawTransitionOverlay();
|
||||||
base.Draw(gameTime);
|
base.Draw(gameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawTransitionOverlay()
|
||||||
|
{
|
||||||
|
if (Context.Scenes.ActiveTransition is not OverlayTransition transition)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transitionRenderer ??= new TransitionRenderer(GraphicsDevice);
|
||||||
|
transition.Draw(
|
||||||
|
_transitionRenderer,
|
||||||
|
Context.Scenes.TransitionCoverage,
|
||||||
|
Context.Scenes.TransitionPhase
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnExiting(object sender, ExitingEventArgs args)
|
protected override void OnExiting(object sender, ExitingEventArgs args)
|
||||||
{
|
{
|
||||||
@@ -84,7 +106,11 @@ public class GameHost : Game
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
Context.DisposeOwnedResources(except: this);
|
_transitionRenderer?.Dispose();
|
||||||
|
_transitionRenderer = null;
|
||||||
|
// GraphicsDevice зарегистрирован как сервис, но им владеет MonoGame:
|
||||||
|
// base.Dispose сам его освобождает, реестру трогать нельзя.
|
||||||
|
Context.DisposeOwnedResources(this, GraphicsDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Host;
|
||||||
|
|
||||||
/// <summary>Window and loop settings for <see cref="GameHost"/>.</summary>
|
/// <summary>Window and loop settings for <see cref="GameHost"/>.</summary>
|
||||||
public sealed class GameHostOptions
|
public sealed class GameHostOptions
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="MrGameEng.Host.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
namespace MrGameEng.Core;
|
namespace MrGameEng.Host;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal overlay renderer handed to <see cref="Transition.Draw"/>: fills rectangles in
|
/// Minimal overlay renderer handed to <see cref="OverlayTransition.Draw"/>: fills rectangles
|
||||||
/// normalized screen coordinates (0..1 on both axes) over the rendered scene.
|
/// in normalized screen coordinates (0..1 on both axes) over the rendered scene.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TransitionRenderer : IDisposable
|
public sealed class TransitionRenderer : IDisposable
|
||||||
{
|
{
|
||||||
@@ -13,7 +13,7 @@ public sealed class TransitionRenderer : IDisposable
|
|||||||
private readonly BasicEffect _effect;
|
private readonly BasicEffect _effect;
|
||||||
private readonly VertexPositionColor[] _vertices = new VertexPositionColor[6];
|
private readonly VertexPositionColor[] _vertices = new VertexPositionColor[6];
|
||||||
|
|
||||||
/// <summary>Disposes the GPU effect. Called by <see cref="SceneManager"/> on shutdown.</summary>
|
/// <summary>Disposes the GPU effect. Called by <see cref="GameHost"/> on shutdown.</summary>
|
||||||
public void Dispose() => _effect.Dispose();
|
public void Dispose() => _effect.Dispose();
|
||||||
|
|
||||||
internal TransitionRenderer(GraphicsDevice device)
|
internal TransitionRenderer(GraphicsDevice device)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
|
namespace MrGameEng.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A scene transition that draws a full-screen overlay through a
|
||||||
|
/// <see cref="TransitionRenderer"/>. The timing state machine lives in
|
||||||
|
/// <see cref="SceneManager"/>; <see cref="GameHost"/> renders the overlay each draw while a
|
||||||
|
/// transition is active. Transitions are stateless and reusable.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class OverlayTransition : Transition
|
||||||
|
{
|
||||||
|
/// <summary>Creates a transition with explicit phase durations.</summary>
|
||||||
|
protected OverlayTransition(float outDuration, float inDuration)
|
||||||
|
: base(outDuration, inDuration) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws the overlay. <paramref name="coverage"/> is 0 (scene fully visible) to
|
||||||
|
/// 1 (scene fully covered); <paramref name="phase"/> tells which side of the switch this is.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Factories for the built-in visual scene transitions.</summary>
|
||||||
|
public static class Transitions
|
||||||
|
{
|
||||||
|
/// <summary>Fade through a solid color (black by default). Total duration is split between out and in.</summary>
|
||||||
|
public static Transition Fade(float duration = 0.6f, Color? color = null) =>
|
||||||
|
new FadeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||||
|
|
||||||
|
/// <summary>A curtain wiping across the screen (black by default). Total duration is split between out and in.</summary>
|
||||||
|
public static Transition Wipe(float duration = 0.6f, Color? color = null) =>
|
||||||
|
new WipeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||||
|
|
||||||
|
private sealed class FadeTransition(float outDuration, float inDuration, Color color)
|
||||||
|
: OverlayTransition(outDuration, inDuration)
|
||||||
|
{
|
||||||
|
public override void Draw(
|
||||||
|
TransitionRenderer renderer,
|
||||||
|
float coverage,
|
||||||
|
TransitionPhase phase
|
||||||
|
) => renderer.Fill(0f, 0f, 1f, 1f, color, coverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WipeTransition(float outDuration, float inDuration, Color color)
|
||||||
|
: OverlayTransition(outDuration, inDuration)
|
||||||
|
{
|
||||||
|
public override void Draw(
|
||||||
|
TransitionRenderer renderer,
|
||||||
|
float coverage,
|
||||||
|
TransitionPhase phase
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Out: шторка растёт слева направо; In: уезжает дальше вправо.
|
||||||
|
if (phase == TransitionPhase.Out)
|
||||||
|
{
|
||||||
|
renderer.Fill(0f, 0f, coverage, 1f, color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
renderer.Fill(1f - coverage, 0f, coverage, 1f, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using MrGameEng.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace MrGameEng.Core.Tests;
|
||||||
|
|
||||||
|
public class HeadlessHostTests
|
||||||
|
{
|
||||||
|
private sealed class CountingScene : Scene
|
||||||
|
{
|
||||||
|
public int LoadCount;
|
||||||
|
public int UnloadCount;
|
||||||
|
public int UpdateCount;
|
||||||
|
|
||||||
|
protected override void OnLoad() => LoadCount++;
|
||||||
|
|
||||||
|
protected override void OnUnload() => UnloadCount++;
|
||||||
|
|
||||||
|
public override void Update(GameClock clock)
|
||||||
|
{
|
||||||
|
UpdateCount++;
|
||||||
|
base.Update(clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RunTicks_AdvancesClock_ByExactFixedStep()
|
||||||
|
{
|
||||||
|
var scene = new CountingScene();
|
||||||
|
using var host = new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 10f }, scene);
|
||||||
|
|
||||||
|
host.RunTicks(30);
|
||||||
|
|
||||||
|
Assert.Equal(0.1f, host.FixedDeltaTime, 3);
|
||||||
|
Assert.Equal(30, host.TickCount);
|
||||||
|
Assert.Equal(3.0, host.Context.Clock.TotalTime, 3);
|
||||||
|
Assert.Equal(30, scene.UpdateCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstTick_LoadsTheInitialScene()
|
||||||
|
{
|
||||||
|
var scene = new CountingScene();
|
||||||
|
using var host = new HeadlessHost(new HeadlessHostOptions(), scene);
|
||||||
|
|
||||||
|
Assert.Equal(0, scene.LoadCount);
|
||||||
|
|
||||||
|
host.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(1, scene.LoadCount);
|
||||||
|
Assert.Same(scene, host.Context.Scenes.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispose_UnloadsTheActiveScene()
|
||||||
|
{
|
||||||
|
var scene = new CountingScene();
|
||||||
|
var host = new HeadlessHost(new HeadlessHostOptions(), scene);
|
||||||
|
host.Tick();
|
||||||
|
|
||||||
|
host.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(1, scene.UnloadCount);
|
||||||
|
Assert.Null(host.Context.Scenes.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Run_StopsWhenCancelled()
|
||||||
|
{
|
||||||
|
using var cancellation = new CancellationTokenSource();
|
||||||
|
var scene = new CancellingScene(cancellation, afterTicks: 5);
|
||||||
|
using var host = new HeadlessHost(new HeadlessHostOptions { Realtime = false }, scene);
|
||||||
|
|
||||||
|
host.Run(cancellation.Token);
|
||||||
|
|
||||||
|
Assert.Equal(5, scene.UpdateCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimeScale_StillApplies_OnTopOfFixedStep()
|
||||||
|
{
|
||||||
|
var scene = new CountingScene();
|
||||||
|
using var host = new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 10f }, scene);
|
||||||
|
host.Context.Clock.TimeScale = 3f;
|
||||||
|
|
||||||
|
host.RunTicks(10);
|
||||||
|
|
||||||
|
Assert.Equal(3.0, host.Context.Clock.TotalTime, 3);
|
||||||
|
Assert.Equal(1.0, host.Context.Clock.UnscaledTotalTime, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonPositiveTickRate_Throws()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||||
|
new HeadlessHost(new HeadlessHostOptions { TicksPerSecond = 0f }, new CountingScene())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CancellingScene(CancellationTokenSource cancellation, int afterTicks)
|
||||||
|
: Scene
|
||||||
|
{
|
||||||
|
public int UpdateCount;
|
||||||
|
|
||||||
|
protected override void OnLoad() { }
|
||||||
|
|
||||||
|
public override void Update(GameClock clock)
|
||||||
|
{
|
||||||
|
UpdateCount++;
|
||||||
|
if (UpdateCount >= afterTicks)
|
||||||
|
{
|
||||||
|
cancellation.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Update(clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,14 @@ public class SceneTransitionTests
|
|||||||
protected override void OnUnload() => UnloadCount++;
|
protected override void OnUnload() => UnloadCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Тайминг-машина живёт в ядре и не зависит от визуала перехода —
|
||||||
|
// тестируем на голой заглушке с длительностями, как у Fade.
|
||||||
|
private sealed class TimedTransition(float outDuration, float inDuration)
|
||||||
|
: Transition(outDuration, inDuration);
|
||||||
|
|
||||||
|
private static Transition Fade(float duration) =>
|
||||||
|
new TimedTransition(duration / 2f, duration / 2f);
|
||||||
|
|
||||||
private static void Tick(EngineContext context, float seconds)
|
private static void Tick(EngineContext context, float seconds)
|
||||||
{
|
{
|
||||||
context.Clock.Advance(seconds);
|
context.Clock.Advance(seconds);
|
||||||
@@ -31,7 +39,7 @@ public class SceneTransitionTests
|
|||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
// Fade(1.0) → фаза закрытия 0.5 c, фаза открытия 0.5 c.
|
// Fade(1.0) → фаза закрытия 0.5 c, фаза открытия 0.5 c.
|
||||||
context.Scenes.Switch(second, Transition.Fade(1f));
|
context.Scenes.Switch(second, Fade(1f));
|
||||||
Tick(context, 0.2f);
|
Tick(context, 0.2f);
|
||||||
|
|
||||||
Assert.True(context.Scenes.IsTransitioning);
|
Assert.True(context.Scenes.IsTransitioning);
|
||||||
@@ -61,7 +69,7 @@ public class SceneTransitionTests
|
|||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
context.Clock.TimeScale = 0f; // игра на паузе
|
context.Clock.TimeScale = 0f; // игра на паузе
|
||||||
|
|
||||||
context.Scenes.Switch(second, Transition.Fade(0.2f));
|
context.Scenes.Switch(second, Fade(0.2f));
|
||||||
Tick(context, 0.15f);
|
Tick(context, 0.15f);
|
||||||
Tick(context, 0.15f);
|
Tick(context, 0.15f);
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ public class SceneTransitionTests
|
|||||||
context.Scenes.Switch(first);
|
context.Scenes.Switch(first);
|
||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
context.Scenes.Switch(second, Transition.Fade(1f));
|
context.Scenes.Switch(second, Fade(1f));
|
||||||
Tick(context, 0.1f);
|
Tick(context, 0.1f);
|
||||||
context.Scenes.Switch(third); // передумали, пока экран закрывается
|
context.Scenes.Switch(third); // передумали, пока экран закрывается
|
||||||
|
|
||||||
@@ -99,12 +107,12 @@ public class SceneTransitionTests
|
|||||||
context.Scenes.Switch(first);
|
context.Scenes.Switch(first);
|
||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
context.Scenes.Switch(second, Transition.Fade(1f)); // 0.5 c закрытие + 0.5 c открытие
|
context.Scenes.Switch(second, Fade(1f)); // 0.5 c закрытие + 0.5 c открытие
|
||||||
Tick(context, 0.6f); // закрыто, своп на second, началось открытие
|
Tick(context, 0.6f); // закрыто, своп на second, началось открытие
|
||||||
Assert.Same(second, context.Scenes.Current);
|
Assert.Same(second, context.Scenes.Current);
|
||||||
|
|
||||||
Tick(context, 0.25f); // открытие наполовину (coverage ~0.5)
|
Tick(context, 0.25f); // открытие наполовину (coverage ~0.5)
|
||||||
context.Scenes.Switch(third, Transition.Fade(1f)); // передумали во время открытия
|
context.Scenes.Switch(third, Fade(1f)); // передумали во время открытия
|
||||||
|
|
||||||
Tick(context, 0.05f); // экран снова закрывается — свопа ещё нет
|
Tick(context, 0.05f); // экран снова закрывается — свопа ещё нет
|
||||||
Assert.Same(second, context.Scenes.Current);
|
Assert.Same(second, context.Scenes.Current);
|
||||||
@@ -124,7 +132,7 @@ public class SceneTransitionTests
|
|||||||
context.Scenes.Switch(first);
|
context.Scenes.Switch(first);
|
||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
context.Scenes.Switch(second, Transition.Fade(0f));
|
context.Scenes.Switch(second, Fade(0f));
|
||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
Tick(context, 0.016f);
|
Tick(context, 0.016f);
|
||||||
|
|
||||||
|
|||||||
@@ -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.Host\MrGameEng.Host.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user