Collision profiles and mesh fitting
Pick a profile on a prefab body, or a primitive actor component:
ts
import { prefab, box, collisionProfile, responseBetween } from 'gameable';
const Hero = prefab({
body: { shape: 'capsule', dims: [0.3, 0.6], kind: 'character', collision: 'Pawn' },
});
const wall = box([2, 1, 0.5], { collision: 'InvisibleWall' });
const response = responseBetween(collisionProfile('Pawn'), collisionProfile('Trigger'));
// response is 'overlap': the weaker response wins, regardless of argument order.collisionProfile(name) returns a fresh frozen setup and frozen responses. Unknown names throw and list the available profiles. The 18 built-in profiles are NoCollision, BlockAll, OverlapAll, BlockAllDynamic, OverlapAllDynamic, IgnoreOnlyPawn, OverlapOnlyPawn, Pawn, Spectator, CharacterMesh, PhysicsActor, Destructible, InvisibleWall, InvisibleWallDynamic, Trigger, Ragdoll, Vehicle and UI. Deprecated aliases are excluded. Unspecified responses default to Block, including game channels. Thus OverlapAll and Trigger still block game channels unless explicitly overridden.
For custom policies, provide { objectType, enabled, responses }, where responses has a required default and optional channel overrides. Object type is independent of body mobility. enabled is none, query, physics or queryAndPhysics. none resolves every pair to Ignore. Otherwise the pair uses the weaker of each body's response to the other's object type: Ignore wins over Overlap, which wins over Block.
Channel mapping
| Channel | Existing body layers |
|---|---|
| worldStatic | staticGeometry |
| worldDynamic | defaultLayer |
| pawn | character, player, enemy |
| physicsBody | debris |
| vehicle | user4 |
| destructible | user5 |
| game0 … game3 | user0 … user3 |
| visibility, camera | Trace channels; no body bits |
Profiles and custom setups cannot be combined with raw layer or mask on the same body/component. Existing declarations without a policy keep their behavior, including actor collision: 'block' | 'overlap' | 'none'. mesh() takes a collision only with a fit: mesh('thing.house', { fit: 'box', collision: 'BlockAll' }) gives the mesh a body the page builds from the model itself (see fit a body to its model). Without one, give the actor a box(), sphere() or capsule() body component with this collision.
Collision enforcement
lowerCollision(policy) returns body fields and an overlapMask. Query-only bodies are sensors: queries find them, but they push nothing. Physics-only bodies simulate and are excluded from ray, sweep and overlap queries. Mixed Block/Overlap profiles automatically create a companion sensor sharing the main body's shape, transform and guest identity; its contact edges dispatch actor overlap callbacks.
Character controllers do not support query-only or overlap profiles; spawn warns GAMEABLE_COLLISION_UNSUPPORTED: <profile>: <why>. Layer bits still cannot distinguish profiles sharing an object type. Use responseBetween for precise policy checks and trace post-filtering.
traceMask(setups, 'visibility' | 'camera') builds a query mask from candidate bodies' policies. Supply the setups used in the scene; retain them for precise post-filtering when policies share layers. A response container by itself cannot identify the object types responding to a trace.
Mesh collision generation
For a model the game draws, prefer body.fit (or mesh(asset, { fit })): the page reads the model's own geometry, so the game needs no vertices of its own. Call fitBox, fitSphere, fitCapsule or fitKDop during initialization or asset preparation when the game has the vertices itself. Input is a nonempty Float32Array of local xyz vertices in metres, with scale already baked in. Fits return a local center, rotation, shape and dims: boxes use half-extents, spheres radius, and capsules radius plus cylindrical half-height. Apply the returned local transform when placing the shape. Capsule local Y rotates onto the longest bounds axis; ties prefer X, then Y, then Z. Box fitting uses an AABB; the reference specifies no oriented-box fit. Sphere fitting chooses the smaller of an incremental sphere and an AABB-center sphere.
When the source capsule heuristic under-encloses radial points, including box corners, the fit expands its radius to the maximum vertex distance from the chosen axis, then applies the same length rule to contain every vertex. Zero-radius sphere/capsule inputs also throw. Capsule zero segment length has a minimum of 1e-8 metres. The heuristic selects a bounds axis rather than searching arbitrary axes or minimizing volume.
fitKDop(vertices, 10 | 18 | 26) returns packed convex hull points by intersecting support half-spaces. Ten uses 10X; 18 adds edge directions and 26 adds corner directions. Flat inputs remain flat, with no padding. Degenerate inputs do not receive artificial thickness. Hull points need host-side geometry preparation before simulation.
shapeForQuery(setup) and shapeForSimulation(setup) route caller-supplied simple and complex geometries. Set collisionTraceFlag to simpleAndComplex, simpleAsComplex, complexAsSimple or useDefault. The latter requires a resolved defaultFlag; query complexity uses traceComplex, default false. ComplexAsSimple selects triangle surfaces intended for fixed/kinematic bodies; it does not enable dynamic concave simulation.
Tile bodies
mergeTileColliders(cells, cellSize = 1) greedily merges same-height cells into rectangles, scanning z then x and extending x first. Coordinates are integer indices: cell (0,0) covers [0,1] on x and z with default spacing. Height is in metres from ground y=0. Mixed heights never merge. Greedy meshing reduces seams and preserves area, but does not guarantee a globally minimum rectangle cover.
spawnTileColliders(ctx, cells, { collision: 'BlockAll' }) spawns one fixed box per merged rectangle. BlockAll is the default profile. This approximates a tile-map aggregate collider with multiple bodies because the SDK has no compound spawn command. Use it once during initialization; these helpers allocate.
See Physics for the engine's body and query APIs.