glemy/physics/collision_sweep
The pairwise collision sweep for a whole tick’s worth of entities –
genuinely genre-agnostic core: the traversal algorithm and the
default bounce response are baked in (every 2D circle-physics game
wants both, unconditionally), but what happens between two
overlapping entities beyond a plain bounce – merge, destroy,
deal damage, do nothing extra – is entirely decided by the
interact callback each caller supplies, not by this module. See
docs/technical-architecture.md’s Core API section for why this
shape (a generic traversal parameterized over a caller-supplied
per-pair decision, not a guess at what every future game’s
interaction rules look like) is the actual core/game boundary here
– extracted from what decision 0047 found already hardcoded a
specific game’s merge rule into what was meant to be core physics.
Pure, target-agnostic, no FFI: matches the physics/vector2,
physics/entity, physics/bounds, physics/collision pattern of
small, independently-tested primitive modules physics.gleam
composes together.
Types
What should happen between two entities the sweep is currently
considering, decided entirely by the caller’s interact function –
this module never inspects Entity fields itself to make this
decision (that would be baking a specific game’s rules back in).
pub type PairInteraction(event) {
Bounce
Consume(replacement: entity.Entity, event: event)
}
Constructors
-
BounceThe default, always-generic response: resolve as an ordinary elastic collision (
glemy/physics/collision.resolve). -
Consume(replacement: entity.Entity, event: event)The two entities are consumed and replaced by one
replacemententity instead of bouncing –eventis an opaque value of the caller’s own choosing (e.g. a score delta), collected and returned alongside the resolved entities but never interpreted by this module.event’s type is a generic parameter specifically so this module never needs to know what a “meaningful outcome” is for any particular game.
Values
pub fn resolve_all_collisions(
entities: List(entity.Entity),
interact: fn(entity.Entity, entity.Entity) -> PairInteraction(
event,
),
) -> #(List(entity.Entity), List(event))
Resolves every pair of entities exactly once, in a single sequential sweep: each entity is checked against every entity after it in the list, threading its updated state forward as it goes (so if it just bounced off one neighbour, the next check sees that new velocity). This is a standard simplification for simple engines — a sequential pass rather than solving all pairs simultaneously — and is O(n²), the reason broad-phase culling becomes worth it once entity counts grow.
Deliberately not chained within the same pass: once target is
consumed per interact’s decision about the first pair it matches,
the resulting entity isn’t itself re-checked against anything else
until the next frame — everything later in the list still gets its
own full sweep, just never against the vanished pair or the entity
they became. Imperceptible at 60fps for a same-frame mutual-overlap
cluster; avoids both unbounded same-pass recursion on a dense
cluster and “has this entity already been consumed once this pass”
bookkeeping.