using System.Numerics; using Engine.Audio.Contracts; using Engine.Input.Contracts; using Engine.Kernel.Plugins; using Engine.Kernel.Scheduling; using Engine.Kernel.World; using Engine.Physics.Contracts; using Engine.Render.Contracts; using Silk.NET.Input; namespace PhysicsDemoGame; /// /// M4's "one small game, end to end": pressing Space drops a new box from /// above the ground; engine.physics simulates it falling and settling; /// engine.render draws it as a real cube (CubeRenderer, added specifically /// for this); this plugin plays a bounce sound (engine.audio) the moment /// each box's fall actually stops. Physics, audio, input, and rendering /// all cooperate here through nothing but the kernel's own vocabulary — /// this plugin never references engine.physics/audio/render's /// implementation assemblies, only their Contracts. /// /// "Landed" isn't a Box3D contact event — the native shim doesn't expose /// one (see native/physics-native/lingua_physics.c's own doc comment on /// why nothing but scalars crosses that boundary; a contact callback would /// need to carry structured per-contact data across it, exactly what's /// kept out). Honest scope for a demo this size: watch each spawned box's /// own Y velocity via IPhysicsService every frame, and call it landed the /// first time a real fall (velocity past FallingThreshold at some point) /// settles back near zero. /// public sealed class PhysicsDemoGamePlugin : IPlugin { private const float SpawnHeight = 6f; private const float FallingThreshold = -1f; private const float RestThreshold = 0.3f; private readonly Random _random = new(); private readonly List _spawned = []; private readonly HashSet _hasFallen = []; private readonly HashSet _landed = []; private IEngineInput? _input; private IPhysicsService? _physics; private IAudioService? _audio; private GameObject? _bounceSound; private bool _spacePressedLastFrame; public void Configure(IPluginContext ctx) { _input = ctx.Services.Require(); _physics = ctx.Services.Require(); _audio = ctx.Services.Require(); ctx.Schedule.Add(Stage.Update, Tick) .Writes() .Writes() .Writes(); ctx.Log.Info("physics demo ready — press Space to drop a box"); } public void Shutdown(IPluginContext ctx) { ctx.Schedule.RemoveAllFrom("physics-demo-game"); _spawned.Clear(); _hasFallen.Clear(); _landed.Clear(); _bounceSound = null; _input = null; _physics = null; _audio = null; } private void Tick(IWorld world) { _bounceSound ??= world.Roots.FirstOrDefault(go => go.Name == "Bounce"); var spacePressed = _input!.IsKeyDown(Key.Space); if (spacePressed && !_spacePressedLastFrame) Spawn(world); _spacePressedLastFrame = spacePressed; foreach (var box in _spawned) { if (_landed.Contains(box)) continue; var velocityY = _physics!.GetLinearVelocity(box).Y; if (velocityY < FallingThreshold) { _hasFallen.Add(box); continue; } if (_hasFallen.Contains(box) && MathF.Abs(velocityY) < RestThreshold) { _landed.Add(box); if (_bounceSound is not null) _audio!.Play(_bounceSound); } } } private void Spawn(IWorld world) { var go = world.CreateGameObject($"Box{_spawned.Count}"); go.Transform = Transform.Identity; go.Transform.LocalPosition = new Vector3( (float)(_random.NextDouble() * 4 - 2), SpawnHeight, (float)(_random.NextDouble() * 4 - 2)); go.AddComponent().Type = BodyType.Dynamic; go.AddComponent(); go.AddComponent(); _spawned.Add(go); } }