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

  • Bounce

    The 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 replacement entity instead of bouncing – event is 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.

Search Document