Proposal: 2D collision / physics-lite foundation — Body + Collider + esys_move (Phase 0 of #57) #65
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#65
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
This is the Phase 0 foundation prerequisite that #57 (builtin gameplay controllers) explicitly defers to a dedicated issue:
All four controller families (platformer / shooter / NPC AI / RPG) depend on it; none can be implemented until movement-with-collision exists. Sprites and animation are already handled by the engine (
esys_spriteanim#43,esys_motion,Tween.*), so this issue is only about the missing world-simulation layer.Current state
Collision.*today is a static-overlap stub only —selfhost/backend/game/emit_collide.ludicexposes just:Collision.rects(AABB vs AABB overlap → bool)Collision.circles,Collision.rect_circle,Collision.point_rectThese are boolean tests. There is no velocity integration, no swept/continuous collision, no resolution (push-out), no tile-grid broadphase, no triggers, and no per-entity
Body/Collidercomponents an engine system ticks. Value typesRect/Vector/IVec2andGrid.*(spatial queries + pathfinding) already exist to build on.Proposed scope
Follow the existing engine-owned-system (
esys_*) pattern (runtime/native/systems.ludic, resolved by name through the reflection ABI, no-op when a field is absent) so it composes with the six-lever contract in #57.Components (POD
propertybundles, defaulted, override-able):Body { vx, vy, gravity, max_fall, on_ground, ... }— Q16.16 fixed-point velocity/accel state.Collider { w, h, offx, offy, is_trigger, layer, mask }— AABB shape + layer/mask filtering.Engine system
esys_move(auto-ticked each frame, disable-able via lever 5):Grid.*) — the platformer/topdown broadphase.on_ground,hit_wall,hit_ceiling) consumed by controllers.Events (lever 4 — EV0–EV7 bus, observable + cancellable):
CollisionResolved(contact normal + other entity),TriggerEntered/TriggerExited, and a cancellable pre-resolve hook so a game can veto/alter a resolution.Policy (lever 6):
policy: intonBodyfor the common integration variants (e.g. platformer gravity vs top-down no-gravity vs floaty), with the cancellable event for the fully-custom case. Nofnvalues (Ludic has no closures) — enum-field-plus-event, deterministic + replayable.Constraints (inherited from #57 / Ludic invariants)
world_savesnapshots hold.Deliverable
A shared
Body/Collider+esys_movethat the #57 controller families build directly on top of — turning today's static overlap tests into a real (physics-lite) moving-body-with-collision layer. Blocks #PLATFORMER / #SHOOTER / #NPCAI / #RPG.Refs: #57.
Shipped in
0b149a6(pushed tomain).What landed
The static-overlap stub is now a real physics-lite moving-body-with-collision layer, delivered as an engine-owned system in the exact shape of
esys_spriteanim/esys_motion/esys_light2d— resolved by name through the reflection ABI, spliced only when the game declares the component, no-op when a field is absent, and byte-for-byte unchanged for any build that declares noBody(bootstrap fixpoint + every golden render still match).Components (POD
propertybundles, all defaulted, override-able):Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall, hit_ceiling }— Q16.16 fixed-point velocity/accel + engine-owned sub-pixel accumulators (rx/ry), an integrationpolicy(0 = platformer w/ gravity, 1 = top-down), and derived contact flags. Optionalhit_nx/hit_nyexpose the last contact normal.Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered, exited }— AABB shape offset fromPosition { x, y }, layer/mask filtering, one-way + trigger flags, and per-frame trigger outputs.Solids { tile, wall, oneway }— optional single config entity that turns on the tile-grid broadphase over theMap.*tilemap.esys_move(Update phase, after the game's handlers):max_fall) into a tentative move, carried in Q16.16 sub-pixels so slow speeds accumulate exactly.Colliderentities and the tile grid (Grid.*/rt_tile) — a fast body never tunnels.one_wayflag and one-way tile glyph) — block only a downward landing, pass through from below / horizontally.hit+entered/exitededges) but never resolves.on_ground/hit_wall/hit_ceilingfor controllers to consume.Constraints held (from #57 / Ludic invariants)
world_savesnapshots reproduce movement exactly.Events (lever 4)
Contact is surfaced as polled flags (the
SpriteAnim.event_firedpattern from #48), not engine-emitted events: the engine detects the boundary, the game raises its ownCollisionResolved/TriggerEntered/TriggerExitedfrom its handler. This keeps the engine from coupling to a user-declared event and stays inside the no-runtime-dispatch, deterministic/replayable model. The cancellable-pre-resolve case is covered by the lever-6policyenum + making a collider a trigger to veto resolution; a richer event object can layer on later without changing the foundation.Tests / docs
examples/library/physics.ludic— entity broadphase: gravity+land, wall stop, sub-pixel accumulation, entity platform, one-way (land-from-above and rise-through), trigger enter/exit edges. Asserts184 1 1 284 1 1 50 51 104 1 134 1 120 0 1 1 1 0 1 1 1.examples/library/physics_tiles.ludic— tile-grid broadphase: tile floor, tile wall, one-way tile land-from-above / rise-through. Asserts112 1 80 1 48 1 60.x test(now 89 passed, 0 failed); self-host suite 32/0 with goldens byte-identical.Collisionsection (docs/language/collision/_section.md).This is the shared
Body/Collider+esys_movefoundation the #57 controller families (#PLATFORMER / #SHOOTER / #NPCAI / #RPG) build directly on. Closing.