The named collision matrix and how bodies filter each other.
Physics layers
Layers decide which bodies collide with which. Without them, everything hits everything. With them, players pass through pickups, bullets ignore their own team, and triggers only notice the objects you care about.
Declare the collision matrix
physics.layers is an object: each key is a layer name, and its value is the list of layer names it collides with.
"physics": {
"layers": {
"goal": ["player"],
"player": ["goal", "wall"],
"wall": ["player"]
}
}
Read it like a sentence: the goal layer collides with player; player collides with goal and wall; wall collides with player. Rules:
- Each key is a layer name. Every collider picks exactly one of these names via its
layerfield. - The value lists the layers that key collides with. Both sides must accept each other: a contact only happens when each layer lists the other. In the example above,
playerlistsgoalandgoallistsplayer, so they meet. If either side omits the other, the pair is ignored — no collision, no trigger, no contact event. - A layer name only needs to appear as a key once. Listing a name in a value without declaring it as a key is not enough to create the layer.
- Maximum 16 properties.
- Authored order does not assign bit indices. The parser sorts layers by name, so the same matrix always resolves the same way regardless of how the keys are written.
If you omit physics.layers, everything sits on a single implicit default layer and all bodies collide with each other.
How filtering works
Every collider carries a layer name. When two bodies overlap, the engine looks up both names in the matrix and asks one question: does each side list the other?
layers[colliderA.layer] contains colliderB.layer
layers[colliderB.layer] contains colliderA.layer
If both answers are yes, the contact is generated. If either says no, the pair is ignored. That is the whole authoring story — no bit math, no masks to maintain by hand.
Colliders also expose low-level category_bits / mask_bits fields for special cases, but they are derived from the matrix and are not how you author collision rules. Prefer the matrix.
Worked example
"physics": {
"layers": {
"player": ["world", "enemy", "pickup"],
"enemy": ["world", "player"],
"world": ["player", "enemy"],
"pickup": ["player"]
}
}
| Body | layer | Collides with |
|---|---|---|
| Player | player | World, enemies, pickups |
| Enemy | enemy | World and player, not other enemies |
| Bullet | enemy | Same as enemy — enemy bullets hit the player |
| Wall | world | Player and enemies |
| Coin | pickup | Player only |
Enemies walk through each other because enemy does not list enemy. Coins never bounce off walls because pickup lists only player, and world does not list pickup.
Triggers and layers
{
"CircleCollider2D": {
"radius": 1.5,
"is_trigger": true,
"layer": "pickup"
}
}
A trigger uses the same matrix. It generates physics_trigger_enter/stay/exit instead of solid collision. A pickup area that should only notice the player sits on a layer that lists only player — and player must list that layer back.
2D and 3D
This matrix is the 2D collision matrix (it drives the Box2D world). 3D colliders also take a layer string field, but there is no separate 3D matrix in the project file. Keep the layer names coherent so 2D HUD hit areas and 3D world bodies can share vocabulary.
Debugging filters
demi run \
--debug-overlays colliders,contacts
Colliders draw in their layer. If two shapes overlap and no contact appears, check the matrix on both sides — a one-way listing is the usual cause.
Choosing layers
- Start with
player,enemy,world,pickup. - Add
trigger,debris,ragdollas needed. - Prefer a few broad layers over one layer per entity type.
- Remember the 16-layer ceiling when designing large games.
- Keep the matrix symmetric where you want contact: if A should hit B, list each in the other's value.
See Colliders and Physics events for what to do once contacts happen.