Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
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>
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user