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; /// Управление игроком (WASD/стрелки) и прыжок-писк на пробел. public sealed class PlayerControlSystem( Entity player, ActionMap actions, AudioManager audio, SoundEffect beep, MrGameEng.DevConsole.DevConsole console) : BaseSystem { protected override void OnUpdateGroup() { if (console.IsOpen) { return; } ref var transform = ref player.GetComponent(); 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); } } } /// Камера следует за игроком; колесо — зум, Q/E — поворот. public sealed class CameraControlSystem(Entity cameraEntity, Entity player, InputManager input) : BaseSystem { protected override void OnUpdateGroup() { ref var camera = ref cameraEntity.GetComponent(); var target = player.GetComponent().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; } } /// /// Отскок сущностей со скоростью от границ мира. На больших количествах работа режется /// на сегменты по всем ядрам (чанк Friflo держит весь архетип — сам по себе он слишком крупный). /// public sealed class BounceSystem(RectF bounds) : QuerySystem { private const int ParallelThreshold = 8192; private const int SegmentSize = 8192; private readonly List<(Chunk Transforms, Chunk 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 transforms, Chunk 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); } } } } /// Пауза (P), музыка (M), переключение сцены с переходом (Tab). public sealed class SceneHotkeysSystem( EngineContext context, ActionMap actions, Func nextScene, Transition transition) : BaseSystem { protected override void OnUpdateGroup() { if (context.Services.Get().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().Music; if (music.IsPlaying) { music.Pause(); } else { music.Resume(); } } if (actions.IsPressed(SampleAction.SwitchScene) && !context.Scenes.IsTransitioning) { context.Scenes.Switch(nextScene(), transition); } } } /// FPS и статистика рендера: в HUD-лейбл и в заголовок окна (4 раза в секунду). 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().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)"; } } } /// Общая настройка ввода для сцен сэмпла. public static class SampleInput { public static ActionMap CreateActions(InputManager input) => new ActionMap(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); }