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:
Emil
2026-09-02 17:47:15 +03:00
co-authored by Claude Sonnet 5
parent 88e0afa46b
commit 2d0168104d
15 changed files with 566 additions and 6 deletions
+41
View File
@@ -73,6 +73,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Audio", "plugins\eng
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Audio.Tests", "tests\Engine.Audio.Tests\Engine.Audio.Tests.csproj", "{E610BD6A-A732-4863-8D19-555B75134E13}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PhysicsDemo", "PhysicsDemo", "{51874E45-60A1-E43B-395E-A3BE4050752F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GamePlugins", "GamePlugins", "{9760813E-A577-3C56-DE20-0218A108BABD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "physics-demo-game", "physics-demo-game", "{83FF5113-4091-0AE3-A9AD-50B7BB917258}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysicsDemoGame", "samples\PhysicsDemo\GamePlugins\physics-demo-game\PhysicsDemoGame.csproj", "{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysicsDemoGame.Tests", "tests\PhysicsDemoGame.Tests\PhysicsDemoGame.Tests.csproj", "{0201686F-0E2A-400F-8166-CBD11B65D724}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -371,6 +383,30 @@ Global
{E610BD6A-A732-4863-8D19-555B75134E13}.Release|x64.Build.0 = Release|Any CPU
{E610BD6A-A732-4863-8D19-555B75134E13}.Release|x86.ActiveCfg = Release|Any CPU
{E610BD6A-A732-4863-8D19-555B75134E13}.Release|x86.Build.0 = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|x64.Build.0 = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Debug|x86.Build.0 = Debug|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|Any CPU.Build.0 = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|x64.ActiveCfg = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|x64.Build.0 = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|x86.ActiveCfg = Release|Any CPU
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF}.Release|x86.Build.0 = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|x64.ActiveCfg = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|x64.Build.0 = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|x86.ActiveCfg = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Debug|x86.Build.0 = Debug|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|Any CPU.Build.0 = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|x64.ActiveCfg = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|x64.Build.0 = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|x86.ActiveCfg = Release|Any CPU
{0201686F-0E2A-400F-8166-CBD11B65D724}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -408,5 +444,10 @@ Global
{2C2E545D-6937-476D-AC62-F96CEA6403F9} = {2D4F0C3C-7532-0E50-D9FD-7209344A42BE}
{C9AA2FF3-EB6C-4849-AD9F-9E99EFF30AEB} = {2D4F0C3C-7532-0E50-D9FD-7209344A42BE}
{E610BD6A-A732-4863-8D19-555B75134E13} = {2D4F0C3C-7532-0E50-D9FD-7209344A42BE}
{51874E45-60A1-E43B-395E-A3BE4050752F} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{9760813E-A577-3C56-DE20-0218A108BABD} = {51874E45-60A1-E43B-395E-A3BE4050752F}
{83FF5113-4091-0AE3-A9AD-50B7BB917258} = {9760813E-A577-3C56-DE20-0218A108BABD}
{1AFDA567-D86A-43C8-91B9-34CC13F48FEF} = {83FF5113-4091-0AE3-A9AD-50B7BB917258}
{0201686F-0E2A-400F-8166-CBD11B65D724} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal