Physics · class

ColliderSet

Explained in Your first game, Rigid bodies.

class ColliderSet
import { ColliderSet } from '@driftengine/physics';

Constructor

new

constructor(boxes: readonly Collider[], cellSize?: number)
ParameterTypeDescription
boxesreadonly Collider[]
cellSize?number

Properties

NameTypeDescription
BASE_GROUPstaticreadonlyColliderGroupThe group the constructor's own colliders belong to, which add never mints and remove refuses.
More

Reserved because of the slots nothing inserts. The constructor skips an undefined entry without putting it in any cell, and still counts it — so those slots are live and are in no bucket, and a removal that reached one would walk buckets it was never written to. Keeping the constructor's slots in a group nothing can drop puts that case out of reach instead of making it survivable.

A set meant to stream is constructed empty and takes everything through add, which is what the refusal points at when it fires.

dataFloat32ArrayThe packed bounds, six floats a slot.
More

A reference to this is valid until the next add, which may reallocate. bounds() is the reader that survives both that and a change of packing.

Its length is capacity, not count. Those are equal on a set that has never had remove called on it, which is every set built the old way; after a removal the live slots are sparse and a walk of 0..count reads the wrong ones. Walk 0..capacity and skip !liveAt(i).

Accessors

NameTypeDescription
countgetnumberHow many colliders the set holds.
capacitygetnumberHow many slots are allocated, live or not. The true bound of a walk over data.

Methods

liveAt

liveAt(index: number): boolean

Whether slot index holds a collider. Every slot of a set that has never had remove called on it does.

ParameterTypeDescription
indexnumber

add

add(boxes: readonly Collider[]): ColliderGroup

Take a batch of colliders and hand back the handle that drops them again.

ParameterTypeDescription
boxesreadonly Collider[]
More

The grid is updated for these colliders and for no others, which is the whole point: a consumer whose world streams was rebuilding a set at every region crossing, at about 2.1 µs a box, and paying a whole frame for it.

Slots come off the free list before the arrays grow, so a set that loads a ring of regions around a player settles at the ring's high-water mark instead of climbing with distance travelled. Live indices are untouched by this: a slot already in use stays where it is.

absorb

absorb(other: ColliderSet): ColliderGroup

Take everything in another set as one group, without hashing any of it again.

ParameterTypeDescription
otherColliderSet
More

Copy and offset instead of recompute. One Float32Array.set moves the packed bounds, the shapes are copied by reference, and each of the source's buckets is appended to this set's with a constant added to every index. Nothing is floored, no shape bounds are recomputed, and the result queries identically to a one-shot build of the union.

It appends past everything and skips the free list, because a constant offset is what makes the bucket remap a copy: source slot i has to land at base + i and nothing may disturb that.

Which is exactly why the source's holes come across as holes. A source that has had remove called on it — every streamed set — cannot be compacted on the way in, because skipping its holes would make the offset a function of position. Copying them while calling them live would be worse: those slots carry the bytes of colliders that were deleted, and a set reporting them would have resurrected geometry a consumer believed was gone. So liveness is copied with the bytes, the holes land on this set's free list, and the next add fills them. Requiring a dense source instead was the other option and was rejected: it would refuse the only input this was asked for.

What a churned source does cost is its dead slots as bytes copied and space held until the free list drains them. Bounded, visible in bytes(), and not a leak.

A copy and not a move. The source is unchanged and stays usable, which is what lets a caller keep a prototype region and stamp it more than once.

join(a, b, c) is this in a loop, which is why there is no second method for it.

remove

remove(group: ColliderGroup): void

Drop a group and everything in it.

ParameterTypeDescription
groupColliderGroup
More

Refuses the base group and a group it does not hold; a streamer double-freeing a region is a bug, and a silent no-op hides it until the memory it was meant to release turns up in a budget.

shapeAt

shapeAt(index: number): ConvexShape | undefined

The volume collider index really is, or undefined where it is its box.

ParameterTypeDescription
indexnumber

bounds

bounds(index: number, out: Aabb): void

The packed bounds of collider index, written into out. Allocation-free.

ParameterTypeDescription
indexnumber
outAabb
More

data is public and the stride is not. A reader outside this file that decodes data[i * 6 + 1] by hand keeps working right up until the packing changes, and then reports confident nonsense rather than failing.

query

query(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, out: Int32Array): number
ParameterTypeDescription
minXnumber
minYnumber
minZnumber
maxXnumber
maxYnumber
maxZnumber
outInt32Array

bytes

bytes(): ColliderBytes

What this set costs in memory.

More

Because a consumer can count its own bytes and cannot see the engine's, which makes a streaming budget half-measurable: the question "does the ring of regions around the player fit on a phone" has no answer without this.

O(n) over slots and over the index, and it allocates a Set to count each distinct shape once. Not a per-frame call.

auditCells

auditCells(): number

How many bucket entries name a slot nothing holds. Always zero.

More

The one defect in this structure that has no symptom of its own, which is why it gets a check of its own. A remove that misses a bucket entry leaves the slot marked dead — so the fingerprint skips it and is right, and nothing walks it by index — while the stale entry goes on naming it: query returns it, and the sweep reads its stale bounds and collides with geometry that is gone. No measurement of frame time, query cost or hash can see that.

O(total entries) and not something to call in a frame. For tests and for the streaming check.

Signatures

MAX_HITS = 512staticreadonly

static readonly MAX_HITS = 512