Core gains a static Log (Debug/Info/Warning/Error + event); the engine logs key events like scene switches. The console captures Log output into a 2048-line ring buffer and executes registered commands with input history (up/down), Tab prefix completion and scrolling (PageUp/PageDown/End/wheel). Built-ins: help, clear, echo, timescale, close, quit; games register their own (sample: stress/main/beep). Console core is pure logic covered by headless tests; the Myra overlay renders a single label rebuilt only when the Revision counter moves - an idle or closed console costs nothing per frame. Toggled with the backquote key; sample gameplay hotkeys are suppressed while open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
220 lines
7.7 KiB
C#
220 lines
7.7 KiB
C#
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);
|
|
}
|