IPlayModeController (Engine.Editor.Contracts) wraps IWorld.Snapshot()/ Restore() as EnterPlay/ExitPlay — the entire mechanism is that one snapshot, per Snapshot's own doc comment. Exposed as a service, not just something engine.editor's UI calls directly, so a future non-UI driver (a test harness, a headless "play for N frames" CLI command) can drive Play mode without depending on ImGui. Engine.Host now looks up IPlayModeController once and checks IsPlaying every frame before running Stage.Update: Edit mode still renders the scene (so the view isn't frozen and the editor UI stays responsive) but never ticks it, same distinction Unity draws between its Scene and Game views. A project with no engine.editor loaded sees no behavior change — Update runs unconditionally, same as before this existed. Added "play"/"stop" as stdin commands alongside the existing "r" and "screenshot", both real controls (not just test scaffolding) for driving Play mode without a mouse — which is also how this got verified: an interactive run sent "play", screenshotted, sent "stop", and the log shows "Entered Play mode in 13.2ms", the real editor path exercising the same under-100ms budget WorldSnapshotTests already proves at the kernel level. The Lingua Editor panel's button/label flip Play/(Edit mode) to Stop/(Playing) correctly across both screenshots. Also fixes a real layout bug hit while verifying this: SetNextWindowPos with ImGuiCond.FirstUseEver only applies with no prior imgui.ini entry for that window title, and this project had already accumulated one from earlier runs (all three panels stacked exactly on top of each other) — gave each panel its own default position and .gitignore'd imgui.ini, which is per-machine session state, not source. Full suite still green: 65 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
108 lines
3.9 KiB
C#
108 lines
3.9 KiB
C#
using Engine.Editor.Contracts;
|
|
using Engine.Input.Contracts;
|
|
using Engine.Kernel.Diagnostics;
|
|
using Engine.Kernel.Plugins;
|
|
using Engine.Kernel.Scheduling;
|
|
using Engine.Kernel.World;
|
|
using Engine.Windowing.Contracts;
|
|
using ImGuiNET;
|
|
using Silk.NET.OpenGL;
|
|
using Silk.NET.OpenGL.Extensions.ImGui;
|
|
|
|
namespace Engine.Editor;
|
|
|
|
/// <summary>
|
|
/// M3's editor shell: an ImGui overlay drawn on top of the running scene.
|
|
/// Registers on Stage.Render, after engine.render's own Draw (per
|
|
/// project.json's plugin order) so it paints into the same back buffer the
|
|
/// scene just drew into — and before Stage.Present's SwapBuffers, so the
|
|
/// UI isn't delayed a frame. See Stage.Present's doc comment for why that
|
|
/// split exists at all.
|
|
///
|
|
/// Nothing here reads or writes a Component through GetComponent/Query —
|
|
/// HierarchyPanel walks IWorld.Roots/GameObject.Children directly, and the
|
|
/// not-yet-built Inspector will walk GameObject.Components the same way —
|
|
/// which is why this plugin never needs to declare Reads/Writes on its
|
|
/// system: those checks only guard the typed accessors, not plain property
|
|
/// reads. See GameObject.AddComponent's own doc comment on the same gap.
|
|
/// </summary>
|
|
public sealed class EditorPlugin : IPlugin
|
|
{
|
|
private readonly EditorState _state = new();
|
|
private GL? _gl;
|
|
private ImGuiController? _controller;
|
|
private ITime? _time;
|
|
private PlayModeController? _playMode;
|
|
|
|
public void Configure(IPluginContext ctx)
|
|
{
|
|
var window = ctx.Services.Require<IEngineWindow>();
|
|
var input = ctx.Services.Require<IEngineInput>();
|
|
_time = ctx.Time;
|
|
|
|
window.Native.GLContext!.MakeCurrent();
|
|
_gl = window.Native.CreateOpenGL();
|
|
_controller = new ImGuiController(_gl, window.Native, input.Native);
|
|
|
|
_playMode = new PlayModeController(ctx.World, ctx.Log);
|
|
ctx.Services.Provide<IPlayModeController>(_playMode);
|
|
|
|
ctx.Schedule.Add(Stage.Render, DrawUi);
|
|
ctx.Log.Info("editor UI ready (ImGui)");
|
|
}
|
|
|
|
public void Shutdown(IPluginContext ctx)
|
|
{
|
|
ctx.Schedule.RemoveAllFrom("engine.editor");
|
|
ctx.Services.Revoke<IPlayModeController>();
|
|
_controller?.Dispose();
|
|
_gl?.Dispose();
|
|
_controller = null;
|
|
_gl = null;
|
|
_time = null;
|
|
_playMode = null;
|
|
}
|
|
|
|
private void DrawUi(IWorld world)
|
|
{
|
|
_controller!.Update(_time!.DeltaTime);
|
|
|
|
// Nothing selected yet and there's something to select: default to
|
|
// the first root rather than opening on an empty, useless
|
|
// Inspector. Only fires once — any real click overwrites it, and
|
|
// it never fights a deliberate deselect because there's no way to
|
|
// deselect yet.
|
|
if (_state.Selected is null && world.Roots.Count > 0)
|
|
_state.Selected = world.Roots[0];
|
|
|
|
// FirstUseEver, not every frame: a real editor session lets the
|
|
// user drag panels wherever they want, and re-forcing a position
|
|
// every frame would fight that the moment they did. This only
|
|
// picks a sane, non-overlapping default before ImGui has ever
|
|
// seen these windows (or after Reset Layout, once that exists).
|
|
ImGui.SetNextWindowPos(new(10, 10), ImGuiCond.FirstUseEver);
|
|
ImGui.SetNextWindowSize(new(220, 90), ImGuiCond.FirstUseEver);
|
|
ImGui.Begin("Lingua Editor");
|
|
ImGui.Text($"FPS: {1f / MathF.Max(_time.DeltaTime, 0.0001f):F0}");
|
|
ImGui.Text($"Frame: {_time.FrameCount}");
|
|
ImGui.Separator();
|
|
|
|
if (ImGui.Button(_playMode!.IsPlaying ? "Stop" : "Play"))
|
|
{
|
|
if (_playMode.IsPlaying)
|
|
_playMode.ExitPlay();
|
|
else
|
|
_playMode.EnterPlay();
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.Text(_playMode.IsPlaying ? "(Playing)" : "(Edit mode)");
|
|
ImGui.End();
|
|
|
|
HierarchyPanel.Draw(world, _state);
|
|
InspectorPanel.Draw(_state);
|
|
|
|
_controller.Render();
|
|
}
|
|
}
|