scene.UseUI() creates a per-scene Myra Desktop and registers UiRenderSystem last in the draw phase (UI on top, window pixels). GameHost now exposes itself as a Game service (Myra needs the instance; useful for games too). Myra was picked over Gum/ImGui for the FontStashSharp ecosystem fit, pipeline-free assets and maturity; its internal SpriteBatch use is a documented exception to the engine rule. Sample: on-screen HUD replaces window-title-only stats — live FPS label, music volume slider and scene-switch buttons on both scenes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
5.8 KiB
C#
163 lines
5.8 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) : 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, Transition transition) : 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.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}";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|