Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s

This commit is contained in:
Leonid Pershin
2026-06-11 04:03:07 +03:00
parent 31aba3aeee
commit ff2231a8ab
72 changed files with 4113 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

@@ -0,0 +1,23 @@
<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.Assets.Generator\MrGameEng.Assets.Generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\**\*.*" />
<None Include="Assets\**\*.*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
+15
View File
@@ -0,0 +1,15 @@
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),
},
new MainScene());
host.Run();
+46
View File
@@ -0,0 +1,46 @@
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;
}
}
+156
View File
@@ -0,0 +1,156 @@
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) : BaseSystem
{
protected override void OnUpdateGroup()
{
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>Отскок сущностей со скоростью от границ мира.</summary>
public sealed class BounceSystem(RectF bounds) : QuerySystem<Transform2D, Velocity>
{
protected override void OnUpdate()
{
var delta = Tick.deltaTime;
foreach (var (transforms, velocities, _) in Query.Chunks)
{
var t = transforms.Span;
var v = velocities.Span;
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) : BaseSystem
{
protected override void OnUpdateGroup()
{
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.Switch(nextScene());
}
}
}
/// <summary>FPS и статистика рендера в заголовке окна (обновляется 4 раза в секунду).</summary>
public sealed class TitleStatsSystem(EngineContext context, Renderer2D renderer, string sceneName) : 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;
context.Services.Get<GameWindow>().Title =
$"MrGameEng Sample — {sceneName} | {fps:F0} FPS | sprites: {renderer.SubmittedSprites} | culled: {renderer.CulledSprites} | draw calls: {renderer.DrawCalls}";
}
}
/// <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);
}
@@ -0,0 +1,106 @@
using Friflo.Engine.ECS;
using Microsoft.Xna.Framework;
using MrGameEng.Assets;
using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Input;
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);
var playerTexture = assets.Load(GameAssets.Textures.Player);
var shapesTexture = assets.Load(GameAssets.Textures.Shapes);
var beep = assets.Load(GameAssets.Sounds.Beep);
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)),
};
// Декорации по всему миру: уезжаешь камерой — попадают под 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));
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);
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep));
UpdateSystems.Add(new BounceSystem(WorldBounds));
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene()));
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, "Main"));
var music = audio.Music;
if (!music.IsPlaying)
{
music.Volume = 0.4f;
music.Play(assets.Load(GameAssets.Music.Theme));
}
}
}
@@ -0,0 +1,71 @@
using Friflo.Engine.ECS;
using Microsoft.Xna.Framework;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Input;
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));
UpdateSystems.Add(new BounceSystem(WorldBounds));
UpdateSystems.Add(new StressCameraSystem(camera, input));
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene()));
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, $"Stress {SpriteCount:N0}"));
}
/// <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);
}
}
}