M4: PhysicsDemo — the "one small game, end to end"
samples/PhysicsDemo ties physics, audio, input, and rendering together through nothing but the kernel's own vocabulary — no plugin here references another plugin's implementation, only Contracts. Press Space to drop a box; engine.physics simulates it falling onto a static ground; CubeRenderer (new — engine.render's QuadRenderer drew flat cards, no good for a physics demo where a BoxCollider needs to actually look like a box) draws it; physics-demo-game watches each spawned box's own Y velocity via IPhysicsService and plays a bounce sound via IAudioService the moment a real fall settles. A looping ambient track plays throughout via AudioSource's own PlayOnAwake. project.json deliberately excludes engine.editor — this is the shippable configuration M4's "done when" actually asks for, not the dev one. "Landed" isn't a Box3D contact event — the native shim never exposed one (nothing but scalars crosses that boundary, see lingua_physics.c). Watching velocity every frame is the honest, right-sized alternative for a demo this size, not a shortcut around missing infrastructure. Verified two ways. First, real Space-key input isn't simulable here (no xdotool/ydotool under this Wayland session) — so PhysicsDemoGame.Tests drives the actual PhysicsDemoGamePlugin.Configure/Tick through the real Schedule with a controllable fake IEngineInput (and fake IPhysicsService/ IAudioService, since engine.physics/engine.audio's own correctness is already covered elsewhere): spawn-on-press with edge detection, the Rigidbody/BoxCollider/CubeRenderer combo the spawned box actually gets, and — the case most likely to be subtly wrong — a box that never actually falls doesn't false-positive as "landed," only one that fell past FallingThreshold and then settled does, exactly once. All 5 passed immediately. Second, a real windowed run: a box placed in scene.json above the ground visibly falls and settles onto it on screen after a real few-second wait — screenshotting by --screenshot-after-frames alone turned out not to prove this (VSync is off, so frames race by far faster than real physics time passes; enough elapsed frames isn't enough elapsed seconds), an interactive run with a real sleep before the screenshot command is what actually shows it. Full suite: 92 tests. Remaining for M4: the Linux + Windows build pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
@@ -12,11 +12,14 @@ namespace Engine.Render;
|
||||
|
||||
/// <summary>
|
||||
/// M3's render pipeline: a real perspective camera, drawing every
|
||||
/// GameObject with a QuadRenderer at its own WorldMatrix — upgraded from
|
||||
/// M2's single hardcoded NDC-space quad specifically because gizmos need
|
||||
/// real 3D geometry and a real camera to mean anything (a handle dragged
|
||||
/// in screen space has to map onto an actual 3D axis). See M3 in
|
||||
/// docs/kernel-contract.md §8.
|
||||
/// GameObject with a QuadRenderer or CubeRenderer at its own WorldMatrix —
|
||||
/// upgraded from M2's single hardcoded NDC-space quad specifically because
|
||||
/// gizmos need real 3D geometry and a real camera to mean anything (a
|
||||
/// handle dragged in screen space has to map onto an actual 3D axis). See
|
||||
/// M3 in docs/kernel-contract.md §8. CubeRenderer itself is M4's addition,
|
||||
/// for the same reason M3's gizmo needed a real camera: a BoxCollider
|
||||
/// falling and settling only reads as a physics object if it looks like
|
||||
/// one, not a flat card.
|
||||
///
|
||||
/// GameObject.WorldMatrix and this plugin's own matrices are both
|
||||
/// System.Numerics.Matrix4x4, which is row-vector (v' = v * M, and
|
||||
@@ -95,11 +98,64 @@ public sealed class RenderPlugin : IPlugin
|
||||
0.5f, 0.5f, 0f, 1f, 1f,
|
||||
];
|
||||
|
||||
// Unit cube (1x1x1 before LocalScale), centered at its own origin — see
|
||||
// CubeRenderer's own doc comment for why this exists (M4's physics
|
||||
// demo needs falling BoxColliders to actually look like boxes). No
|
||||
// face culling is enabled anywhere in this plugin, so winding order
|
||||
// doesn't matter here the way it would with CullFace on.
|
||||
private static readonly float[] CubeVertices =
|
||||
[
|
||||
// position uv
|
||||
-0.5f, -0.5f, 0.5f, 0f, 0f, // front (+Z)
|
||||
0.5f, -0.5f, 0.5f, 1f, 0f,
|
||||
0.5f, 0.5f, 0.5f, 1f, 1f,
|
||||
0.5f, 0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, 0.5f, 0f, 1f,
|
||||
-0.5f, -0.5f, 0.5f, 0f, 0f,
|
||||
|
||||
0.5f, -0.5f, -0.5f, 0f, 0f, // back (-Z)
|
||||
-0.5f, -0.5f, -0.5f, 1f, 0f,
|
||||
-0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
0.5f, 0.5f, -0.5f, 0f, 1f,
|
||||
0.5f, -0.5f, -0.5f, 0f, 0f,
|
||||
|
||||
-0.5f, -0.5f, -0.5f, 0f, 0f, // left (-X)
|
||||
-0.5f, -0.5f, 0.5f, 1f, 0f,
|
||||
-0.5f, 0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, -0.5f, 0f, 1f,
|
||||
-0.5f, -0.5f, -0.5f, 0f, 0f,
|
||||
|
||||
0.5f, -0.5f, 0.5f, 0f, 0f, // right (+X)
|
||||
0.5f, -0.5f, -0.5f, 1f, 0f,
|
||||
0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
0.5f, 0.5f, 0.5f, 0f, 1f,
|
||||
0.5f, -0.5f, 0.5f, 0f, 0f,
|
||||
|
||||
-0.5f, 0.5f, 0.5f, 0f, 0f, // top (+Y)
|
||||
0.5f, 0.5f, 0.5f, 1f, 0f,
|
||||
0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
0.5f, 0.5f, -0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, -0.5f, 0f, 1f,
|
||||
-0.5f, 0.5f, 0.5f, 0f, 0f,
|
||||
|
||||
-0.5f, -0.5f, -0.5f, 0f, 0f, // bottom (-Y)
|
||||
0.5f, -0.5f, -0.5f, 1f, 0f,
|
||||
0.5f, -0.5f, 0.5f, 1f, 1f,
|
||||
0.5f, -0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, -0.5f, 0.5f, 0f, 1f,
|
||||
-0.5f, -0.5f, -0.5f, 0f, 0f,
|
||||
];
|
||||
|
||||
private GL? _gl;
|
||||
private IEngineWindow? _window;
|
||||
private CameraService? _camera;
|
||||
private uint _vao;
|
||||
private uint _vbo;
|
||||
private uint _cubeVao;
|
||||
private uint _cubeVbo;
|
||||
private uint _program;
|
||||
private uint _texture;
|
||||
private int _modelLocation;
|
||||
@@ -143,6 +199,20 @@ public sealed class RenderPlugin : IPlugin
|
||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
_cubeVao = _gl.GenVertexArray();
|
||||
_gl.BindVertexArray(_cubeVao);
|
||||
|
||||
_cubeVbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _cubeVbo);
|
||||
_gl.BufferData<float>(BufferTargetARB.ArrayBuffer, CubeVertices, BufferUsageARB.StaticDraw);
|
||||
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
@@ -159,7 +229,7 @@ public sealed class RenderPlugin : IPlugin
|
||||
ctx.Events.Subscribe(_onTextureReloaded);
|
||||
|
||||
ctx.Services.Provide<IScreenCapture>(new GlScreenCapture(_gl, _window));
|
||||
ctx.Schedule.Add(Stage.Render, Draw).Reads<QuadRenderer>();
|
||||
ctx.Schedule.Add(Stage.Render, Draw).Reads<QuadRenderer>().Reads<CubeRenderer>();
|
||||
ctx.Schedule.Add(Stage.Present, Present);
|
||||
ctx.Log.Info("GL context created, 3D quad pipeline ready");
|
||||
}
|
||||
@@ -176,6 +246,8 @@ public sealed class RenderPlugin : IPlugin
|
||||
_gl.DeleteTexture(_texture);
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
_gl.DeleteVertexArray(_cubeVao);
|
||||
_gl.DeleteBuffer(_cubeVbo);
|
||||
_gl.DeleteProgram(_program);
|
||||
_gl.Dispose();
|
||||
}
|
||||
@@ -237,6 +309,13 @@ public sealed class RenderPlugin : IPlugin
|
||||
SetMatrix(_modelLocation, go.WorldMatrix);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, 6);
|
||||
}
|
||||
|
||||
_gl.BindVertexArray(_cubeVao);
|
||||
foreach (var go in world.Query<CubeRenderer>())
|
||||
{
|
||||
SetMatrix(_modelLocation, go.WorldMatrix);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, 36);
|
||||
}
|
||||
}
|
||||
|
||||
// Split from Draw() into its own Stage.Present system so engine.editor
|
||||
|
||||
Reference in New Issue
Block a user