Remove the sample project: LittleSim is the engine showcase now
CI / build-test (push) Failing after 1m4s
CI / build-test (push) Failing after 1m4s
The MrGameEng.Sample demo game is deleted along with its assets; the LittleSim god-sim (which vendors this engine as a submodule) takes over as the living showcase where every engine feature is demonstrated. CLAUDE.md, README and docs updated; the demonstration rule now points to LittleSim, and the stress-scene perf figure is kept as a historical reference measurement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a395e58458
commit
19ab3cbbc0
@@ -11,11 +11,15 @@ Keep them up to date when architecture or conventions change.
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/ MrGameEng.* engine libraries (one per functional area)
|
src/ MrGameEng.* engine libraries (one per functional area)
|
||||||
samples/ MrGameEng.Sample — demo game showcasing every engine feature
|
|
||||||
tests/ xUnit test projects, one per engine library
|
tests/ xUnit test projects, one per engine library
|
||||||
|
tools/ CLI tools (atlas packer)
|
||||||
docs/ architecture, conventions, roadmap (Russian)
|
docs/ architecture, conventions, roadmap (Russian)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The engine has no sample project: the **LittleSim** game
|
||||||
|
(https://gitea.hsrv.site/mrleo1nid/LittleSim, this repo as the `engine/` submodule)
|
||||||
|
is the living showcase — new engine features are demonstrated there.
|
||||||
|
|
||||||
Engine modules: `Core` (game loop, ECS world, scenes, time), `Graphics` (custom batched
|
Engine modules: `Core` (game loop, ECS world, scenes, time), `Graphics` (custom batched
|
||||||
renderer, camera, sprites), `Input`, `Audio`, `Assets` (runtime loading, no content
|
renderer, camera, sprites), `Input`, `Audio`, `Assets` (runtime loading, no content
|
||||||
pipeline), `Assets.Generator` (Roslyn source generator for typed asset handles),
|
pipeline), `Assets.Generator` (Roslyn source generator for typed asset handles),
|
||||||
@@ -38,7 +42,6 @@ depends on `Graphics` (Texture2DRegion) and `Assets` (loader registration);
|
|||||||
```
|
```
|
||||||
dotnet build MrGameEng.sln
|
dotnet build MrGameEng.sln
|
||||||
dotnet test MrGameEng.sln
|
dotnet test MrGameEng.sln
|
||||||
dotnet run --project samples/MrGameEng.Sample
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture rules
|
## Architecture rules
|
||||||
@@ -61,8 +64,8 @@ dotnet run --project samples/MrGameEng.Sample
|
|||||||
only through generated typed handles (`AssetRef<T>`), never string paths.
|
only through generated typed handles (`AssetRef<T>`), never string paths.
|
||||||
- New engine functionality goes into the matching module, or a new
|
- New engine functionality goes into the matching module, or a new
|
||||||
`MrGameEng.<Area>` library if it is a distinct area — never into `Core` by default.
|
`MrGameEng.<Area>` library if it is a distinct area — never into `Core` by default.
|
||||||
- Every public engine feature must be demonstrated in `MrGameEng.Sample`
|
- Every public engine feature must be covered by tests where logic is testable
|
||||||
and covered by tests where logic is testable without a GPU.
|
without a GPU, and demonstrated in the LittleSim game (the engine's showcase).
|
||||||
|
|
||||||
## Code conventions
|
## Code conventions
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core", "src\MrGameEng.Core\MrGameEng.Core.csproj", "{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core", "src\MrGameEng.Core\MrGameEng.Core.csproj", "{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Sample", "samples\MrGameEng.Sample\MrGameEng.Sample.csproj", "{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}"
|
||||||
@@ -79,18 +75,6 @@ Global
|
|||||||
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x64.Build.0 = Release|Any CPU
|
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x86.ActiveCfg = Release|Any CPU
|
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x86.Build.0 = Release|Any CPU
|
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -349,7 +333,6 @@ Global
|
|||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
{09FF1FD5-7CCD-4A0B-AC0C-4910A2B5DF6B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
{E6C4D3FA-4FEF-4FA0-9AB3-42928E576139} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
|
|
||||||
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
{2E9A054C-77A1-484B-BBFF-51FC40CD7D87} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
{1936FFA7-B642-4B1C-A01A-A3B25BBFCAD1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
{1936FFA7-B642-4B1C-A01A-A3B25BBFCAD1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
{F3C7CE85-EF6B-4CEE-9042-50B8710EF656} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
{F3C7CE85-EF6B-4CEE-9042-50B8710EF656} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ and [Friflo.Engine.ECS](https://github.com/friflo/Friflo.Engine.ECS) — ECS-fir
|
|||||||
|
|
||||||
Developer documentation (Russian): [docs/architecture.md](docs/architecture.md), [docs/roadmap.md](docs/roadmap.md).
|
Developer documentation (Russian): [docs/architecture.md](docs/architecture.md), [docs/roadmap.md](docs/roadmap.md).
|
||||||
|
|
||||||
|
Showcase: the [LittleSim](https://gitea.hsrv.site/mrleo1nid/LittleSim) game — a god-sim
|
||||||
|
built on this engine (vendored there as the `engine/` submodule) that demonstrates
|
||||||
|
every engine feature.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE).
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|||||||
+14
-7
@@ -20,8 +20,10 @@
|
|||||||
аллоцировать память. Проверяется бенчмарками и code review.
|
аллоцировать память. Проверяется бенчмарками и code review.
|
||||||
3. **Модульность.** Движок разбит на библиотеки по функциональным областям.
|
3. **Модульность.** Движок разбит на библиотеки по функциональным областям.
|
||||||
Игра подключает только то, что использует.
|
Игра подключает только то, что использует.
|
||||||
4. **Всё демонстрируется.** Каждая публичная возможность движка показана
|
4. **Всё демонстрируется.** Каждая публичная возможность движка покрыта тестами
|
||||||
в `MrGameEng.Sample` и покрыта тестами (где логика тестируема без GPU).
|
(где логика тестируема без GPU) и показана в игре
|
||||||
|
[LittleSim](https://gitea.hsrv.site/mrleo1nid/LittleSim) — живой витрине движка
|
||||||
|
(движок подключён туда сабмодулем `engine/`).
|
||||||
|
|
||||||
## Модули
|
## Модули
|
||||||
|
|
||||||
@@ -276,13 +278,14 @@ CLI-обёртка: `dotnet run --project tools/MrGameEng.AtlasTool -- <исто
|
|||||||
вдвое больше кадра): загрузка не ждёт, пока GPU дорисует предыдущий кадр;
|
вдвое больше кадра): загрузка не ждёт, пока GPU дорисует предыдущий кадр;
|
||||||
`Discard` — только на перемотке кольца.
|
`Discard` — только на перемотке кольца.
|
||||||
- **Тайминги фаз** кадра (submit/sort/build/upload/draw, мс) доступны как свойства
|
- **Тайминги фаз** кадра (submit/sort/build/upload/draw, мс) доступны как свойства
|
||||||
`Renderer2D` — выводятся в HUD стресс-сцены; оптимизации делаются только по ним.
|
`Renderer2D` — выводи их в HUD при оптимизации; решения только по замерам.
|
||||||
- Текстурные атласы — первоклассный гражданин: `Sprite` хранит регион атласа,
|
- Текстурные атласы — первоклассный гражданин: `Sprite` хранит регион атласа,
|
||||||
спрайты одного атласа батчатся автоматически.
|
спрайты одного атласа батчатся автоматически.
|
||||||
- Цель по производительности: ≥100k спрайтов при 60 FPS на среднем десктопе,
|
- Цель по производительности: ≥100k спрайтов при 60 FPS на среднем десктопе,
|
||||||
0 аллокаций на кадр ниже порога параллелизма. Стресс-сцена Sample: 100k сущностей
|
0 аллокаций на кадр ниже порога параллелизма. Эталонный замер (стресс-сцена,
|
||||||
(~61k в кадре) ≈ 297 FPS в Release. **Производительность измеряется только
|
до переноса демо в LittleSim): 100k сущностей, ~61k в кадре ≈ 297 FPS в Release.
|
||||||
в Release** — Debug-сборка медленнее в 5–6 раз (нет инлайнинга JIT).
|
**Производительность измеряется только в Release** — Debug-сборка медленнее
|
||||||
|
в 5–6 раз (нет инлайнинга JIT).
|
||||||
|
|
||||||
## Сцены и переходы
|
## Сцены и переходы
|
||||||
|
|
||||||
@@ -323,11 +326,15 @@ Game.Draw ──► Scene.Draw ──► SystemRoot (Draw-фаза): кам
|
|||||||
```
|
```
|
||||||
MrGameEng.sln
|
MrGameEng.sln
|
||||||
src/ библиотеки движка (MrGameEng.*)
|
src/ библиотеки движка (MrGameEng.*)
|
||||||
samples/ MrGameEng.Sample — демо всех возможностей
|
|
||||||
tests/ xUnit-тесты, по проекту на библиотеку
|
tests/ xUnit-тесты, по проекту на библиотеку
|
||||||
|
tools/ CLI-инструменты (упаковщик атласов)
|
||||||
docs/ документация (русский)
|
docs/ документация (русский)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Демо-проекта в движке нет: витрина движка — игра
|
||||||
|
[LittleSim](https://gitea.hsrv.site/mrleo1nid/LittleSim) (движок подключён туда
|
||||||
|
сабмодулем `engine/`).
|
||||||
|
|
||||||
## Зафиксированные версии
|
## Зафиксированные версии
|
||||||
|
|
||||||
| Зависимость | Версия | Назначение |
|
| Зависимость | Версия | Назначение |
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,8 @@
|
|||||||
Базовый движок (ядро, рендер, ассеты с кодогенерацией, ввод, звук, демо) реализован —
|
Базовый движок (ядро, рендер, ассеты с кодогенерацией, ввод, звук, демо) реализован —
|
||||||
текущие возможности описаны в [architecture.md](architecture.md). Здесь — только планы.
|
текущие возможности описаны в [architecture.md](architecture.md). Здесь — только планы.
|
||||||
|
|
||||||
Пункт считается завершённым, когда покрыт тестами и показан в `MrGameEng.Sample`;
|
Пункт считается завершённым, когда покрыт тестами и показан в игре
|
||||||
|
[LittleSim](https://gitea.hsrv.site/mrleo1nid/LittleSim) — витрине движка;
|
||||||
после завершения он убирается отсюда и фиксируется в architecture.md.
|
после завершения он убирается отсюда и фиксируется в architecture.md.
|
||||||
|
|
||||||
## Ближайшее
|
## Ближайшее
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"name": "Textures",
|
|
||||||
"pageSize": 2048,
|
|
||||||
"padding": 2,
|
|
||||||
"pages": [
|
|
||||||
{
|
|
||||||
"file": "Textures.atlas.0.png",
|
|
||||||
"width": 256,
|
|
||||||
"height": 128
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"regions": [
|
|
||||||
{
|
|
||||||
"key": "player",
|
|
||||||
"page": 0,
|
|
||||||
"x": 68,
|
|
||||||
"y": 2,
|
|
||||||
"w": 64,
|
|
||||||
"h": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "shapes",
|
|
||||||
"page": 0,
|
|
||||||
"x": 2,
|
|
||||||
"y": 2,
|
|
||||||
"w": 64,
|
|
||||||
"h": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 760 B |
Binary file not shown.
|
Before Width: | Height: | Size: 933 B |
@@ -1,29 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Input\MrGameEng.Input.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Tilemaps\MrGameEng.Tilemaps.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.UI\MrGameEng.UI.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
|
|
||||||
<ProjectReference Include="..\..\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
|
|
||||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="Assets\**\*.*" />
|
|
||||||
<None Include="Assets\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.Sample.Scenes;
|
|
||||||
|
|
||||||
using var host = new GameHost(
|
|
||||||
new GameHostOptions
|
|
||||||
{
|
|
||||||
Title = "MrGameEng Sample",
|
|
||||||
Width = 1280,
|
|
||||||
Height = 720,
|
|
||||||
ClearColor = new Color(24, 26, 32),
|
|
||||||
VSync = false, // техдемо: показываем реальный FPS, не ограниченный частотой монитора
|
|
||||||
},
|
|
||||||
new MainScene());
|
|
||||||
|
|
||||||
host.Run();
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
|
|
||||||
namespace MrGameEng.Sample;
|
|
||||||
|
|
||||||
/// <summary>Действия игрока, привязанные к клавишам через ActionMap.</summary>
|
|
||||||
public enum SampleAction
|
|
||||||
{
|
|
||||||
MoveLeft,
|
|
||||||
MoveRight,
|
|
||||||
MoveUp,
|
|
||||||
MoveDown,
|
|
||||||
Jump,
|
|
||||||
Pause,
|
|
||||||
ToggleMusic,
|
|
||||||
SwitchScene,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Скорость для движущихся сущностей сэмпла.</summary>
|
|
||||||
public struct Velocity : IComponent
|
|
||||||
{
|
|
||||||
public Vector2 Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Слои рендера сэмпла; регистрируются один раз на общий Renderer2D.</summary>
|
|
||||||
public static class SampleLayers
|
|
||||||
{
|
|
||||||
public static LayerId Actors { get; private set; }
|
|
||||||
|
|
||||||
public static LayerId Ui { get; private set; }
|
|
||||||
|
|
||||||
private static bool _registered;
|
|
||||||
|
|
||||||
public static void EnsureRegistered(Renderer2D renderer)
|
|
||||||
{
|
|
||||||
if (_registered)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Actors = renderer.Layers.Register("Actors", LayerSpace.World, LayerSortMode.YSort);
|
|
||||||
Ui = renderer.Layers.Register("UI", LayerSpace.Screen);
|
|
||||||
_registered = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using Friflo.Engine.ECS.Systems;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Audio;
|
|
||||||
using Microsoft.Xna.Framework.Input;
|
|
||||||
using MrGameEng.Audio;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
using MrGameEng.Input;
|
|
||||||
|
|
||||||
namespace MrGameEng.Sample;
|
|
||||||
|
|
||||||
/// <summary>Управление игроком (WASD/стрелки) и прыжок-писк на пробел.</summary>
|
|
||||||
public sealed class PlayerControlSystem(
|
|
||||||
Entity player, ActionMap<SampleAction> actions, AudioManager audio, SoundEffect beep,
|
|
||||||
MrGameEng.DevConsole.DevConsole console) : BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
if (console.IsOpen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var transform = ref player.GetComponent<Transform2D>();
|
|
||||||
var move = new Vector2(
|
|
||||||
actions.GetAxis(SampleAction.MoveLeft, SampleAction.MoveRight),
|
|
||||||
actions.GetAxis(SampleAction.MoveUp, SampleAction.MoveDown));
|
|
||||||
|
|
||||||
if (move != Vector2.Zero)
|
|
||||||
{
|
|
||||||
move.Normalize();
|
|
||||||
transform.Position += move * 260f * Tick.deltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.IsPressed(SampleAction.Jump))
|
|
||||||
{
|
|
||||||
audio.Play(beep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Камера следует за игроком; колесо — зум, Q/E — поворот.</summary>
|
|
||||||
public sealed class CameraControlSystem(Entity cameraEntity, Entity player, InputManager input) : BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
ref var camera = ref cameraEntity.GetComponent<Camera>();
|
|
||||||
var target = player.GetComponent<Transform2D>().Position;
|
|
||||||
|
|
||||||
camera.Position = Vector2.Lerp(camera.Position, target, Math.Min(1f, 6f * Tick.deltaTime));
|
|
||||||
camera.Zoom = Math.Clamp(camera.Zoom * (1f + input.WheelDelta * 0.001f), 0.2f, 5f);
|
|
||||||
|
|
||||||
var rotate = (input.IsKeyDown(Keys.E) ? 1f : 0f) - (input.IsKeyDown(Keys.Q) ? 1f : 0f);
|
|
||||||
camera.Rotation += rotate * 1.2f * Tick.deltaTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отскок сущностей со скоростью от границ мира. На больших количествах работа режется
|
|
||||||
/// на сегменты по всем ядрам (чанк Friflo держит весь архетип — сам по себе он слишком крупный).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BounceSystem(RectF bounds) : QuerySystem<Transform2D, Velocity>
|
|
||||||
{
|
|
||||||
private const int ParallelThreshold = 8192;
|
|
||||||
private const int SegmentSize = 8192;
|
|
||||||
|
|
||||||
private readonly List<(Chunk<Transform2D> Transforms, Chunk<Velocity> Velocities)> _chunks = [];
|
|
||||||
private readonly List<(int Chunk, int Start, int Length)> _segments = [];
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
var delta = Tick.deltaTime;
|
|
||||||
_chunks.Clear();
|
|
||||||
var total = 0;
|
|
||||||
foreach (var (transforms, velocities, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
_chunks.Add((transforms, velocities));
|
|
||||||
total += transforms.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (total < ParallelThreshold)
|
|
||||||
{
|
|
||||||
foreach (var (transforms, velocities) in _chunks)
|
|
||||||
{
|
|
||||||
Move(transforms, velocities, 0, transforms.Length, delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_segments.Clear();
|
|
||||||
for (var c = 0; c < _chunks.Count; c++)
|
|
||||||
{
|
|
||||||
var length = _chunks[c].Transforms.Length;
|
|
||||||
for (var start = 0; start < length; start += SegmentSize)
|
|
||||||
{
|
|
||||||
_segments.Add((c, start, Math.Min(SegmentSize, length - start)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Parallel.For(0, _segments.Count, i =>
|
|
||||||
{
|
|
||||||
var (chunk, start, length) = _segments[i];
|
|
||||||
Move(_chunks[chunk].Transforms, _chunks[chunk].Velocities, start, length, delta);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Move(Chunk<Transform2D> transforms, Chunk<Velocity> velocities, int start, int length, float delta)
|
|
||||||
{
|
|
||||||
var t = transforms.Span.Slice(start, length);
|
|
||||||
var v = velocities.Span.Slice(start, length);
|
|
||||||
for (var i = 0; i < t.Length; i++)
|
|
||||||
{
|
|
||||||
ref var position = ref t[i].Position;
|
|
||||||
ref var velocity = ref v[i].Value;
|
|
||||||
position += velocity * delta;
|
|
||||||
|
|
||||||
if (position.X < bounds.Left || position.X > bounds.Right)
|
|
||||||
{
|
|
||||||
velocity.X = -velocity.X;
|
|
||||||
position.X = Math.Clamp(position.X, bounds.Left, bounds.Right);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position.Y < bounds.Top || position.Y > bounds.Bottom)
|
|
||||||
{
|
|
||||||
velocity.Y = -velocity.Y;
|
|
||||||
position.Y = Math.Clamp(position.Y, bounds.Top, bounds.Bottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Пауза (P), музыка (M), переключение сцены с переходом (Tab).</summary>
|
|
||||||
public sealed class SceneHotkeysSystem(
|
|
||||||
EngineContext context, ActionMap<SampleAction> actions, Func<Scene> nextScene, Transition transition) : BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
if (context.Services.Get<MrGameEng.DevConsole.DevConsole>().IsOpen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.IsPressed(SampleAction.Pause))
|
|
||||||
{
|
|
||||||
context.Clock.TimeScale = context.Clock.TimeScale > 0f ? 0f : 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.IsPressed(SampleAction.ToggleMusic))
|
|
||||||
{
|
|
||||||
var music = context.Services.Get<AudioManager>().Music;
|
|
||||||
if (music.IsPlaying)
|
|
||||||
{
|
|
||||||
music.Pause();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
music.Resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions.IsPressed(SampleAction.SwitchScene) && !context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
context.Scenes.Switch(nextScene(), transition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>FPS и статистика рендера: в HUD-лейбл и в заголовок окна (4 раза в секунду).</summary>
|
|
||||||
public sealed class StatsSystem(
|
|
||||||
EngineContext context, Renderer2D renderer, string sceneName, Myra.Graphics2D.UI.Label? hudLabel = null) : BaseSystem
|
|
||||||
{
|
|
||||||
private float _accumulated;
|
|
||||||
private int _frames;
|
|
||||||
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
_accumulated += context.Clock.UnscaledDeltaTime;
|
|
||||||
_frames++;
|
|
||||||
if (_accumulated < 0.25f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fps = _frames / _accumulated;
|
|
||||||
_accumulated = 0f;
|
|
||||||
_frames = 0;
|
|
||||||
var stats =
|
|
||||||
$"{fps:F0} FPS | sprites: {renderer.SubmittedSprites} | culled: {renderer.CulledSprites} | draw calls: {renderer.DrawCalls}";
|
|
||||||
context.Services.Get<GameWindow>().Title = $"MrGameEng Sample — {sceneName} | {stats}";
|
|
||||||
if (hudLabel is not null)
|
|
||||||
{
|
|
||||||
hudLabel.Text =
|
|
||||||
$"{sceneName}\n{stats}\n" +
|
|
||||||
$"submit {renderer.SubmitMs:F2} | sort {renderer.SortMs:F2} | build {renderer.BuildMs:F2} | " +
|
|
||||||
$"upload {renderer.UploadMs:F2} | draw {renderer.DrawMs:F2} (ms)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Общая настройка ввода для сцен сэмпла.</summary>
|
|
||||||
public static class SampleInput
|
|
||||||
{
|
|
||||||
public static ActionMap<SampleAction> CreateActions(InputManager input) =>
|
|
||||||
new ActionMap<SampleAction>(input)
|
|
||||||
.Bind(SampleAction.MoveLeft, Keys.A)
|
|
||||||
.Bind(SampleAction.MoveLeft, Keys.Left)
|
|
||||||
.Bind(SampleAction.MoveRight, Keys.D)
|
|
||||||
.Bind(SampleAction.MoveRight, Keys.Right)
|
|
||||||
.Bind(SampleAction.MoveUp, Keys.W)
|
|
||||||
.Bind(SampleAction.MoveUp, Keys.Up)
|
|
||||||
.Bind(SampleAction.MoveDown, Keys.S)
|
|
||||||
.Bind(SampleAction.MoveDown, Keys.Down)
|
|
||||||
.Bind(SampleAction.Jump, Keys.Space)
|
|
||||||
.Bind(SampleAction.Pause, Keys.P)
|
|
||||||
.Bind(SampleAction.ToggleMusic, Keys.M)
|
|
||||||
.Bind(SampleAction.SwitchScene, Keys.Tab);
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Assets;
|
|
||||||
using MrGameEng.Atlases;
|
|
||||||
using MrGameEng.Audio;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.DevConsole;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
using MrGameEng.Input;
|
|
||||||
using MrGameEng.Tilemaps;
|
|
||||||
using MrGameEng.UI;
|
|
||||||
using Myra.Graphics2D.UI;
|
|
||||||
|
|
||||||
namespace MrGameEng.Sample.Scenes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Интерактивная демо-сцена: ассеты через сгенерированные хендлы, анимация, слои
|
|
||||||
/// (мир + Y-sort + screen-space HUD), камера с зумом/поворотом, ввод, звук и музыка.
|
|
||||||
/// WASD — игрок, колесо — зум, Q/E — поворот, Space — звук, P — пауза, M — музыка, Tab — стресс-сцена.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MainScene : Scene
|
|
||||||
{
|
|
||||||
private const int DecorCount = 1500;
|
|
||||||
private static readonly RectF WorldBounds = new(-2000f, -2000f, 4000f, 4000f);
|
|
||||||
|
|
||||||
protected override void OnLoad()
|
|
||||||
{
|
|
||||||
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
|
||||||
var audio = Context.Services.GetOrDefault<AudioManager>() ?? Context.UseAudio();
|
|
||||||
var input = this.UseInput();
|
|
||||||
var actions = SampleInput.CreateActions(input);
|
|
||||||
this.UseSpriteAnimation();
|
|
||||||
|
|
||||||
var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) });
|
|
||||||
SampleLayers.EnsureRegistered(renderer);
|
|
||||||
this.UseTilemaps();
|
|
||||||
|
|
||||||
var playerTexture = assets.Load(GameAssets.Textures.Player);
|
|
||||||
var shapesTexture = assets.Load(GameAssets.Textures.Shapes);
|
|
||||||
var beep = assets.Load(GameAssets.Sounds.Beep);
|
|
||||||
|
|
||||||
// Текстурный атлас: Assets/Atlases собран утилитой MrGameEng.AtlasTool из Assets/Textures
|
|
||||||
// (см. README). Регионы адресуются исходным путём без расширения и батчатся в один draw call.
|
|
||||||
Context.UseTextureAtlases();
|
|
||||||
var atlas = assets.Load(GameAssets.Atlases.Textures);
|
|
||||||
|
|
||||||
var shapeRegions = new[]
|
|
||||||
{
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(0, 0, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(32, 0, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(0, 32, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(32, 32, 32, 32)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Тайловая площадка под стартовой зоной: шахматный пол, строится кодом из TileSet/TileGrid.
|
|
||||||
// Видимые клетки сабмитятся каждый кадр; Depth -10 кладёт пол под декорации.
|
|
||||||
var floorTiles = new TileSet();
|
|
||||||
var floorA = floorTiles.Add(shapeRegions[0], new Color(70, 75, 95));
|
|
||||||
var floorB = floorTiles.Add(shapeRegions[2], new Color(55, 60, 78));
|
|
||||||
var floorGrid = new TileGrid(24, 14);
|
|
||||||
for (var y = 0; y < floorGrid.Height; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < floorGrid.Width; x++)
|
|
||||||
{
|
|
||||||
floorGrid[x, y] = (x + y) % 2 == 0 ? floorA : floorB;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Store.CreateEntity(new Tilemap(floorGrid, floorTiles, tileSize: 48f)
|
|
||||||
{
|
|
||||||
Origin = new Vector2(-24 * 24f, -14 * 24f),
|
|
||||||
Depth = -10f,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Декорации по всему миру: уезжаешь камерой — попадают под culling (см. заголовок окна).
|
|
||||||
var random = new Random(42);
|
|
||||||
for (var i = 0; i < DecorCount; i++)
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(shapeRegions[random.Next(shapeRegions.Length)]);
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
sprite.Color = Color.White * 0.35f;
|
|
||||||
Store.CreateEntity(
|
|
||||||
new Transform2D(
|
|
||||||
new Vector2(
|
|
||||||
random.NextSingle() * WorldBounds.Width + WorldBounds.Left,
|
|
||||||
random.NextSingle() * WorldBounds.Height + WorldBounds.Top),
|
|
||||||
rotation: random.NextSingle() * MathF.Tau,
|
|
||||||
scale: new Vector2(0.5f + random.NextSingle())),
|
|
||||||
sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Бродячие "существа" на Y-sort слое: кто ниже на экране — тот ближе.
|
|
||||||
for (var i = 0; i < 200; i++)
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(shapeRegions[random.Next(shapeRegions.Length)], SampleLayers.Actors);
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
var angle = random.NextSingle() * MathF.Tau;
|
|
||||||
Store.CreateEntity(
|
|
||||||
new Transform2D(new Vector2(random.Next(-600, 600), random.Next(-400, 400))),
|
|
||||||
sprite,
|
|
||||||
new Velocity { Value = new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * (40f + random.Next(80)) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Игрок: анимированный спрайт (2 кадра из атласа player.png), Y-sort слой.
|
|
||||||
var playerSprite = new Sprite(new Texture2DRegion(playerTexture, new Rectangle(0, 0, 32, 32)), SampleLayers.Actors);
|
|
||||||
playerSprite.CenterOrigin();
|
|
||||||
var blink = new SpriteAnimationClip(
|
|
||||||
[
|
|
||||||
new Texture2DRegion(playerTexture, new Rectangle(0, 0, 32, 32)),
|
|
||||||
new Texture2DRegion(playerTexture, new Rectangle(32, 0, 32, 32)),
|
|
||||||
], framesPerSecond: 3f);
|
|
||||||
|
|
||||||
var player = Store.CreateEntity(
|
|
||||||
new Transform2D(Vector2.Zero, scale: new Vector2(2f)),
|
|
||||||
playerSprite,
|
|
||||||
new SpriteAnimator(blink));
|
|
||||||
|
|
||||||
// Пара спрайтов из атласа рядом со стартом игрока — вся пара рисуется одним draw call.
|
|
||||||
var atlasShowcase = new[] { ("player", -80f), ("shapes", 80f) };
|
|
||||||
foreach (var (key, offsetX) in atlasShowcase)
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(atlas.GetRegion(key), SampleLayers.Actors);
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
Store.CreateEntity(new Transform2D(new Vector2(offsetX, -120f)), sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
var camera = Store.CreateEntity(new Camera(Vector2.Zero, zoom: 1f, bounds: WorldBounds));
|
|
||||||
|
|
||||||
// HUD: золотой квадрат в углу на screen-space слое — не двигается с камерой.
|
|
||||||
var hud = new Sprite(shapeRegions[3], SampleLayers.Ui);
|
|
||||||
Store.CreateEntity(new Transform2D(new Vector2(16f, 16f)), hud);
|
|
||||||
|
|
||||||
// UI (Myra): HUD со статистикой, слайдер громкости, кнопка перехода. После UseRenderer2D!
|
|
||||||
var desktop = this.UseUI();
|
|
||||||
var statsLabel = new Label();
|
|
||||||
var volumeSlider = new HorizontalSlider { Minimum = 0f, Maximum = 1f, Value = audio.Music.Volume, Width = 180 };
|
|
||||||
volumeSlider.ValueChanged += (_, _) => audio.Music.Volume = volumeSlider.Value;
|
|
||||||
var switchButton = new Button { Content = new Label { Text = "Stress scene (Tab)" } };
|
|
||||||
switchButton.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
Context.Scenes.Switch(new StressScene(), Transition.Fade(0.8f));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var panel = new VerticalStackPanel { Left = 12, Top = 12, Spacing = 6 };
|
|
||||||
panel.Widgets.Add(statsLabel);
|
|
||||||
panel.Widgets.Add(new Label { Text = "Music volume (M — pause)" });
|
|
||||||
panel.Widgets.Add(volumeSlider);
|
|
||||||
panel.Widgets.Add(switchButton);
|
|
||||||
desktop.Root = panel;
|
|
||||||
|
|
||||||
// Консоль разработчика (клавиша `) — последней, чтобы рисовалась поверх UI.
|
|
||||||
var console = this.UseDevConsole();
|
|
||||||
console.Register("stress", "switch to the stress scene", (_, _) =>
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
Context.Scenes.Switch(new StressScene(), Transition.Fade(0.8f));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.Register("main", "switch to the main scene", (_, _) =>
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
Context.Scenes.Switch(new MainScene(), Transition.Fade(0.8f));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.Register("beep", "play the beep sound", (_, _) => audio.Play(beep));
|
|
||||||
console.Register("path", "switch to the pathfinding & collisions demo", (_, _) =>
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
Context.Scenes.Switch(new PathfindingScene(), Transition.Fade(0.6f));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.Register("atlas", "list texture atlas regions", (c, _) =>
|
|
||||||
{
|
|
||||||
c.WriteLine($"atlas '{atlas.Name}': {atlas.Pages.Count} page(s), {atlas.Regions.Count} region(s)");
|
|
||||||
foreach (var (key, region) in atlas.Regions.OrderBy(r => r.Key, StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
c.WriteLine($" {key}: {region.Bounds.Width}x{region.Bounds.Height} at ({region.Bounds.X},{region.Bounds.Y})");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep, console));
|
|
||||||
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
|
||||||
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
|
|
||||||
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene(), Transition.Fade(0.8f)));
|
|
||||||
UpdateSystems.Add(new StatsSystem(Context, renderer, "Main", statsLabel));
|
|
||||||
|
|
||||||
var music = audio.Music;
|
|
||||||
if (!music.IsPlaying)
|
|
||||||
{
|
|
||||||
music.Volume = 0.4f;
|
|
||||||
music.Play(assets.Load(GameAssets.Music.Theme));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using Friflo.Engine.ECS.Systems;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Assets;
|
|
||||||
using MrGameEng.Collisions;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.DevConsole;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
using MrGameEng.Input;
|
|
||||||
using MrGameEng.Pathfinding;
|
|
||||||
using MrGameEng.UI;
|
|
||||||
|
|
||||||
namespace MrGameEng.Sample.Scenes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Демо поиска пути и коллизий: ЛКМ ставит цель — толпа агентов стекается к ней по
|
|
||||||
/// flow field, жёлтым подсвечен A*-путь из левого верхнего угла, столкнувшиеся агенты
|
|
||||||
/// вспыхивают красным. Tab/'main' — назад.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PathfindingScene : Scene
|
|
||||||
{
|
|
||||||
private const int CellsX = 64;
|
|
||||||
private const int CellsY = 36;
|
|
||||||
private const int CellSize = 20;
|
|
||||||
private const int AgentCount = 250;
|
|
||||||
private static readonly Point AStarStart = new(1, 1);
|
|
||||||
|
|
||||||
private readonly WallGrid _grid = new(CellsX, CellsY, seed: 99);
|
|
||||||
private readonly FlowField _field = new();
|
|
||||||
private readonly List<Point> _path = [];
|
|
||||||
private readonly List<Entity> _pathMarkers = [];
|
|
||||||
private FlowFieldBuilder _builder = null!;
|
|
||||||
private GridPathfinder _pathfinder = null!;
|
|
||||||
private Texture2DRegion _white = null!;
|
|
||||||
private Entity _goalMarker;
|
|
||||||
|
|
||||||
/// <summary>Состояние агента: время красной вспышки после столкновения.</summary>
|
|
||||||
private struct AgentState : IComponent
|
|
||||||
{
|
|
||||||
public float Flash;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnLoad()
|
|
||||||
{
|
|
||||||
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
|
||||||
var input = this.UseInput();
|
|
||||||
var actions = SampleInput.CreateActions(input);
|
|
||||||
var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) });
|
|
||||||
SampleLayers.EnsureRegistered(renderer);
|
|
||||||
|
|
||||||
var shapesTexture = assets.Load(GameAssets.Textures.Shapes);
|
|
||||||
_white = new Texture2DRegion(shapesTexture, new Rectangle(40, 41, 14, 14)); // жёлтый квадрат как "белый" регион
|
|
||||||
_builder = new FlowFieldBuilder(_grid);
|
|
||||||
_pathfinder = new GridPathfinder(_grid);
|
|
||||||
|
|
||||||
// Стены.
|
|
||||||
for (var y = 0; y < CellsY; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < CellsX; x++)
|
|
||||||
{
|
|
||||||
if (!_grid.IsPassable(x, y))
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(_white) { Color = new Color(70, 74, 84), Depth = 0f };
|
|
||||||
Store.CreateEntity(CellTransform(x, y), sprite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Агенты на проходимых клетках.
|
|
||||||
var random = new Random(7);
|
|
||||||
for (var i = 0; i < AgentCount; i++)
|
|
||||||
{
|
|
||||||
var cell = RandomPassableCell(random);
|
|
||||||
var sprite = new Sprite(_white) { Color = new Color(120, 180, 255), Depth = 1f };
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
Store.CreateEntity(
|
|
||||||
new Transform2D(CellCenter(cell), scale: new Vector2(0.45f)),
|
|
||||||
sprite,
|
|
||||||
new AgentState(),
|
|
||||||
Collider.Circle(4f));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Маркер цели.
|
|
||||||
var goalSprite = new Sprite(_white) { Color = new Color(80, 220, 120), Depth = 0.6f };
|
|
||||||
_goalMarker = Store.CreateEntity(CellTransform(0, 0), goalSprite);
|
|
||||||
|
|
||||||
var world = this.UseCollisions(cellSize: 16f);
|
|
||||||
|
|
||||||
UpdateSystems.Add(new ClickTargetSystem(this, input, renderer));
|
|
||||||
UpdateSystems.Add(new AgentSteerSystem(this));
|
|
||||||
UpdateSystems.Add(new CollisionFlashSystem(world));
|
|
||||||
UpdateSystems.Add(new FlashDecaySystem());
|
|
||||||
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene(), Transition.Fade(0.6f)));
|
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
|
||||||
var label = new Myra.Graphics2D.UI.Label
|
|
||||||
{
|
|
||||||
Text = "ЛКМ — цель: толпа идёт по flow field, жёлтое — путь A*.\nСтолкновения агентов подсвечиваются красным. Tab — назад.",
|
|
||||||
};
|
|
||||||
desktop.Root = new Myra.Graphics2D.UI.VerticalStackPanel { Left = 12, Top = 12 };
|
|
||||||
((Myra.Graphics2D.UI.VerticalStackPanel)desktop.Root).Widgets.Add(label);
|
|
||||||
|
|
||||||
this.UseDevConsole();
|
|
||||||
|
|
||||||
SetGoal(new Point(CellsX / 2, CellsY / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetGoal(Point goal)
|
|
||||||
{
|
|
||||||
if (!_grid.IsPassable(goal.X, goal.Y))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_builder.Build([goal], _field);
|
|
||||||
_goalMarker.GetComponent<Transform2D>().Position = CellTopLeft(goal);
|
|
||||||
|
|
||||||
foreach (var marker in _pathMarkers)
|
|
||||||
{
|
|
||||||
marker.DeleteEntity();
|
|
||||||
}
|
|
||||||
|
|
||||||
_pathMarkers.Clear();
|
|
||||||
if (_pathfinder.FindPath(AStarStart, goal, _path))
|
|
||||||
{
|
|
||||||
foreach (var cell in _path)
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(_white) { Color = new Color(240, 210, 60) * 0.55f, Depth = 0.5f };
|
|
||||||
_pathMarkers.Add(Store.CreateEntity(CellTransform(cell.X, cell.Y), sprite));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Transform2D CellTransform(int x, int y) =>
|
|
||||||
new(CellTopLeft(new Point(x, y)), scale: new Vector2(CellSize / 14f));
|
|
||||||
|
|
||||||
private static Vector2 CellTopLeft(Point cell) => new(cell.X * CellSize, cell.Y * CellSize);
|
|
||||||
|
|
||||||
private static Vector2 CellCenter(Point cell) =>
|
|
||||||
new(cell.X * CellSize + CellSize / 2f, cell.Y * CellSize + CellSize / 2f);
|
|
||||||
|
|
||||||
private Point RandomPassableCell(Random random)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var cell = new Point(random.Next(CellsX), random.Next(CellsY));
|
|
||||||
if (_grid.IsPassable(cell.X, cell.Y))
|
|
||||||
{
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Случайные прямоугольные стены; старт A* и центр всегда свободны.</summary>
|
|
||||||
private sealed class WallGrid : IPathGrid
|
|
||||||
{
|
|
||||||
private readonly bool[,] _walls;
|
|
||||||
|
|
||||||
public WallGrid(int width, int height, int seed)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
_walls = new bool[width, height];
|
|
||||||
var random = new Random(seed);
|
|
||||||
for (var i = 0; i < 70; i++)
|
|
||||||
{
|
|
||||||
var w = random.Next(1, 7);
|
|
||||||
var h = random.Next(1, 7);
|
|
||||||
var x0 = random.Next(width - w);
|
|
||||||
var y0 = random.Next(height - h);
|
|
||||||
for (var y = y0; y < y0 + h; y++)
|
|
||||||
{
|
|
||||||
for (var x = x0; x < x0 + w; x++)
|
|
||||||
{
|
|
||||||
_walls[x, y] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var y = 0; y < 3; y++)
|
|
||||||
{
|
|
||||||
for (var x = 0; x < 3; x++)
|
|
||||||
{
|
|
||||||
_walls[AStarStart.X + x, AStarStart.Y + y] = false;
|
|
||||||
_walls[width / 2 + x - 1, height / 2 + y - 1] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Width { get; }
|
|
||||||
|
|
||||||
public int Height { get; }
|
|
||||||
|
|
||||||
public bool IsPassable(int x, int y) => !_walls[x, y];
|
|
||||||
|
|
||||||
public float Cost(int x, int y) => 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ClickTargetSystem(PathfindingScene scene, InputManager input, Renderer2D renderer) : BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
if (!input.IsMousePressed(MouseButton.Left))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var world = renderer.ScreenToWorld(input.MousePosition.ToVector2());
|
|
||||||
var cell = new Point((int)(world.X / CellSize), (int)(world.Y / CellSize));
|
|
||||||
if (cell.X >= 0 && cell.X < CellsX && cell.Y >= 0 && cell.Y < CellsY)
|
|
||||||
{
|
|
||||||
scene.SetGoal(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class AgentSteerSystem(PathfindingScene scene) : QuerySystem<Transform2D, AgentState>
|
|
||||||
{
|
|
||||||
private const float Speed = 70f;
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
var delta = Tick.deltaTime;
|
|
||||||
var field = scene._field;
|
|
||||||
foreach (var (transforms, _, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
var t = transforms.Span;
|
|
||||||
for (var i = 0; i < t.Length; i++)
|
|
||||||
{
|
|
||||||
ref var position = ref t[i].Position;
|
|
||||||
var cx = Math.Clamp((int)(position.X / CellSize), 0, CellsX - 1);
|
|
||||||
var cy = Math.Clamp((int)(position.Y / CellSize), 0, CellsY - 1);
|
|
||||||
if (!field.IsReachable(cx, cy) || field.DistanceAt(cx, cy) <= 0.6f)
|
|
||||||
{
|
|
||||||
continue; // недостижимо или уже у цели
|
|
||||||
}
|
|
||||||
|
|
||||||
position += field.DirectionAt(cx, cy) * (Speed * delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CollisionFlashSystem(CollisionWorld world) : BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
foreach (var pair in world.Pairs)
|
|
||||||
{
|
|
||||||
Flash(pair.A);
|
|
||||||
Flash(pair.B);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Flash(Entity entity)
|
|
||||||
{
|
|
||||||
if (entity.HasComponent<AgentState>())
|
|
||||||
{
|
|
||||||
entity.GetComponent<AgentState>().Flash = 0.25f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FlashDecaySystem : QuerySystem<AgentState, Sprite>
|
|
||||||
{
|
|
||||||
private static readonly Color Calm = new(120, 180, 255);
|
|
||||||
private static readonly Color Hit = new(235, 70, 60);
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
var delta = Tick.deltaTime;
|
|
||||||
foreach (var (states, sprites, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
var a = states.Span;
|
|
||||||
var s = sprites.Span;
|
|
||||||
for (var i = 0; i < a.Length; i++)
|
|
||||||
{
|
|
||||||
a[i].Flash = Math.Max(0f, a[i].Flash - delta);
|
|
||||||
s[i].Color = a[i].Flash > 0f ? Hit : Calm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Assets;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.DevConsole;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
using MrGameEng.Input;
|
|
||||||
using MrGameEng.UI;
|
|
||||||
|
|
||||||
namespace MrGameEng.Sample.Scenes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Стресс-сцена: 100 000 спрайтов скачут в мире 4000×4000. Колесо — зум (чем дальше,
|
|
||||||
/// тем больше спрайтов в кадре), Tab — обратно в основную сцену. Цель: 60 FPS.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class StressScene : Scene
|
|
||||||
{
|
|
||||||
private const int SpriteCount = 100_000;
|
|
||||||
private static readonly RectF WorldBounds = new(-2000f, -2000f, 4000f, 4000f);
|
|
||||||
|
|
||||||
protected override void OnLoad()
|
|
||||||
{
|
|
||||||
var assets = Context.Services.Get<AssetManager>();
|
|
||||||
var input = this.UseInput();
|
|
||||||
var actions = SampleInput.CreateActions(input);
|
|
||||||
|
|
||||||
var renderer = this.UseRenderer2D();
|
|
||||||
SampleLayers.EnsureRegistered(renderer);
|
|
||||||
|
|
||||||
var shapesTexture = assets.Load(GameAssets.Textures.Shapes);
|
|
||||||
var regions = new[]
|
|
||||||
{
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(0, 0, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(32, 0, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(0, 32, 32, 32)),
|
|
||||||
new Texture2DRegion(shapesTexture, new Rectangle(32, 32, 32, 32)),
|
|
||||||
};
|
|
||||||
|
|
||||||
var random = new Random(7);
|
|
||||||
for (var i = 0; i < SpriteCount; i++)
|
|
||||||
{
|
|
||||||
var sprite = new Sprite(regions[random.Next(regions.Length)]);
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
var angle = random.NextSingle() * MathF.Tau;
|
|
||||||
Store.CreateEntity(
|
|
||||||
new Transform2D(
|
|
||||||
new Vector2(
|
|
||||||
random.NextSingle() * WorldBounds.Width + WorldBounds.Left,
|
|
||||||
random.NextSingle() * WorldBounds.Height + WorldBounds.Top),
|
|
||||||
scale: new Vector2(0.4f + random.NextSingle() * 0.6f)),
|
|
||||||
sprite,
|
|
||||||
new Velocity { Value = new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * (30f + random.Next(150)) });
|
|
||||||
}
|
|
||||||
|
|
||||||
var camera = Store.CreateEntity(new Camera(Vector2.Zero, zoom: 0.3f));
|
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
|
||||||
var statsLabel = new Myra.Graphics2D.UI.Label();
|
|
||||||
var backButton = new Myra.Graphics2D.UI.Button
|
|
||||||
{
|
|
||||||
Content = new Myra.Graphics2D.UI.Label { Text = "Back to main (Tab)" },
|
|
||||||
};
|
|
||||||
backButton.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
Context.Scenes.Switch(new MainScene(), Transition.Wipe(0.8f, Color.DarkSlateBlue));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var panel = new Myra.Graphics2D.UI.VerticalStackPanel { Left = 12, Top = 12, Spacing = 6 };
|
|
||||||
panel.Widgets.Add(statsLabel);
|
|
||||||
panel.Widgets.Add(backButton);
|
|
||||||
desktop.Root = panel;
|
|
||||||
|
|
||||||
this.UseDevConsole(); // поверх UI
|
|
||||||
|
|
||||||
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
|
||||||
UpdateSystems.Add(new StressCameraSystem(camera, input));
|
|
||||||
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene(), Transition.Wipe(0.8f, Color.DarkSlateBlue)));
|
|
||||||
UpdateSystems.Add(new StatsSystem(Context, renderer, $"Stress {SpriteCount:N0}", statsLabel));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Только зум колесом — чтобы регулировать число видимых спрайтов.</summary>
|
|
||||||
private sealed class StressCameraSystem(Entity cameraEntity, InputManager input)
|
|
||||||
: Friflo.Engine.ECS.Systems.BaseSystem
|
|
||||||
{
|
|
||||||
protected override void OnUpdateGroup()
|
|
||||||
{
|
|
||||||
ref var camera = ref cameraEntity.GetComponent<Camera>();
|
|
||||||
camera.Zoom = Math.Clamp(camera.Zoom * (1f + input.WheelDelta * 0.001f), 0.05f, 5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user