Skip to content

Collider Debugging

When colliders do not line up with meshes, you get invisible walls and objects falling through floors. ColliderDebugManager wraps BabylonJS's PhysicsViewer so a collider can be drawn with one call, and NodeEntity carries two methods that walk its own node hierarchy and delegate to it.

Three ways in

ApproachFor
debugCollider in a level's spawn or entity definitionAlways-on during development, written into the entity's state
entity.showCollider() and hideCollider()Runtime toggling, inspector tools, the debug console
engine.debug.collidersOne TransformNode at a time, from your own tooling

Entity methods

showCollider(config?, color?) takes a ColliderDebugConfig and an optional color; hideCollider(name?) takes an optional node name. Both walk the entity's node hierarchy, the entity's own node included, for every node carrying a physicsBody, whether the node was freshly built, drawn from the pool, or adopted.

typescript
type ColliderDebugConfig = boolean | string | Record<string, string | false>;
CallShows or hides
showCollider() or showCollider(true)Every physics node on or under the entity's node, default color
showCollider('#ff0000')Every physics node on or under the entity's node, in that color
showCollider('head')The descendant named head alone, default color, when it carries a physics body
showCollider('head', '#ff0000')The descendant named head alone, in that color
showCollider({ head: '#ff0000', shield: false })head in that color; shield and every unnamed node left alone
showCollider(false)Nothing
hideCollider()Every physics node on or under the entity's node currently shown
hideCollider('head')The descendant named head alone

A string starting with # is a color; any other string is a node name. color applies only to the node-name form. A node name no descendant carries is a no-op, not a throw. Colors are #RRGGBB hex strings; a named CSS color throws inside Color3.FromHexString.

Calling either before the entity's mesh finishes loading walks whatever the hierarchy holds at that moment and finds nothing to show. onMeshLoaded is the first point at which the mesh, and any physics body a behavior attached to it, exists.

typescript
const hit = simulation.pickEntityForward(camera, 10);

if(hit !== undefined)
{
    hit.entity.showCollider('#00ff00');
}

Destroying an entity hides every collider it has shown, poolable or not. A pooled node is detached and hidden rather than disposed on despawn, so without this a collider shown for one entity would reappear on whichever entity claims that node next.

Config-driven: the debugCollider state field

NodeEntityState carries an optional debugCollider, typed the same as showCollider's config argument. When state carries it, showCollider(state.debugCollider) runs at the point construction reaches for the behaviors' onMeshLoaded, just before those hooks: on a fresh spawn, a pooled claim, an adopted node, and a restored entity alike. Left absent, nothing runs.

A level config's SpawnDefinition.debugCollider and EntityDefinition.debugCollider write into that field at spawn, the same way name writes displayName:

yaml
name: battlefield
scene: /assets/levels/battlefield.glb
physics: true

spawns:
    player_spawn:
        entity: PlayerEntity
        debugCollider: true

entities:
    soldier:
        entity: SoldierEntity
        debugCollider: '#00ff00'

A game can also supply it directly at spawn:

typescript
simulation.spawn('CrateEntity', undefined, { debugCollider: { body: '#ff0000' } });

Once written, debugCollider is in a snapshot like any other field. A restored entity runs the identical call from the identical state, so visualization follows the entity's own state, not the level that first spawned it.

ColliderDebugManager

The manager lives at engine.debug.colliders and operates on individual TransformNode references. The entity methods delegate to it.

typescript
const colliders = engine.debug.colliders;

colliders.showColliderForNode(node);
colliders.showColliderForNode(node, '#ff0000');
colliders.hideColliderForNode(node);
colliders.hideAll();
colliders.isShown(node);
colliders.dispose();

showColliderForNode is a no-op on a node with no physicsBody and on a node already shown. The underlying PhysicsViewer is not created until the first show, so there is no cost until something is drawn. Color materials are created once per hex string and reused.

sage constructs one manager for every game. There is no opting out.

typescript
import { ColliderDebugManager } from '@skewedaspect/sage';
import type { ColliderDebugConfig } from '@skewedaspect/sage';
  • Debug Console: sage.entities[id].showCollider() from the browser console
  • Physics: building physics bodies in a behavior
  • Levels: spawn and entity definitions

Released under the MIT License.