Recursive AI

Filter 2D physics collision layers in C#

onegame is a free, open-source game engine for .NET from Recursive AI. MIT or Apache-2.0, no editor required.

Choose which bodies collide and which layers a ray can see. Layers are numbered slots 0–31 that you assign.

Terminal
dotnet new console
dotnet add package RecursiveAI.OneGame.Physics

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Fixed and FVec2 are onegame’s deterministic fixed-point number and 2D vector types.

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D();
space.SetLayerCollision(1, 2, false);
space.AddBox(new FVec2(1, 1), FVec2.Zero,
    new BodyOptions { Layer = 2 });
Console.WriteLine(space.Raycast(
    new FVec2(-3, 0), new FVec2(1, 0),
    10, out _, layers: 1u << 2));

Disables collisions between layers 1 and 2; the layer-2 ray still prints True.

Text
True

Unzip the example, then dotnet run.

The full program

The full program casts past a nearer layer-1 body to a layer-2 target. It then saves and restores the space and checks that two dynamic bodies on the disabled layer pair pass through one another. The restore matters because the collision matrix is saved with the space.

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D(new PhysicsOptions
    { Gravity = FVec2.Zero });
space.AddBox(new FVec2(1, 1), new FVec2(2, 0),
    new BodyOptions
    { Kind = BodyKind.Static, Layer = 1 });
var target = space.AddBox(
    new FVec2(1, 1), new FVec2(4, 0),
    new BodyOptions
    { Kind = BodyKind.Static, Layer = 2 });
space.SetLayerCollision(1, 2, false);
var found = space.Raycast(
    FVec2.Zero, new FVec2(1, 0),
    10, out var hit, layers: 1u << 2);
var selected = found && hit.Body == target;
Console.WriteLine($"Selected layer 2: {selected}");
var writer = new SyncWriter();
space.Write(writer);
var restored = new PhysicsSpace2D(new PhysicsOptions
    { Gravity = FVec2.Zero });
var reader = new SyncReader(writer.Written);
restored.Read(ref reader);
if (!reader.Ok)
    throw new InvalidOperationException(
        reader.Error.ToString());
// Check the disabled pair after loading.
var a = restored.AddBox(
    new FVec2(1, 1), new FVec2(0, 5),
    new BodyOptions
    { Layer = 1, Velocity = new FVec2(2, 0) });
var b = restored.AddBox(
    new FVec2(1, 1), new FVec2(2, 5),
    new BodyOptions
    { Layer = 2, Velocity = new FVec2(-2, 0) });
for (var tick = 0; tick < 60; tick++)
    restored.Step();
var passed = restored.Position(a).X >
    restored.Position(b).X;
Console.WriteLine(
    $"Restored pair passes through: {passed}");

dotnet run prints:

Text
Selected layer 2: True
Restored pair passes through: True

Layer indices outside the supported range are rejected; removed body references throw StaleBodyException.

Variations

Include two layers in one ray mask

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D();
space.AddBox(new FVec2(1, 1), new FVec2(3, 0),
    new BodyOptions { Layer = 5 });
uint mask = (1u << 2) | (1u << 5);
Console.WriteLine(space.Raycast(FVec2.Zero,
    new FVec2(1, 0), 10, out _, layers: mask));
Text
True

The short answer

Assign each body a BodyOptions.Layer, disable unwanted pairs with SetLayerCollision, and pass a separate bit mask to Raycast. The collision matrix controls body interactions; it is not a substitute for a query mask. Disabling a pair does not make either layer invisible to every ray.

OneGame’s physics guide specifies 32 layers and says that the collision matrix is saved with the space. The numbers are slots you assign in your game. There are no automatic “player,” “enemy” or “terrain” categories. Choose named constants in your level code so the same number means the same thing everywhere that constructs the world.

What is the difference between a collision layer and a collision mask?

A body’s layer is one index. A ray’s mask is a set of allowed indices, represented by bits in a uint. Layer 2 and the number 2 used as a mask are not equivalent: 1u << 2 selects layer 2, while the mask value 2 selects bit 1. This is a common source of rays that miss the intended body.

Use bitwise OR to include several layers, as in the variation above. Use uint.MaxValue to include all layers; that is also the ray method’s default. Validate any configurable layer index before shifting it into a mask. A bad shift expression can create the wrong mask before the physics API ever sees it.

The matrix answers a different question: should bodies on one layer collide with bodies on another? Keep that rule in simulation setup, alongside the body creation options. Apply the same setup on every node. Changing the matrix only in response to a rendering choice would make the physics state depend on presentation rather than the game’s rules.

How do you make a raycast ignore a layer?

Pass a mask that leaves that layer’s bit unset. The full program makes the distinction concrete: a nearer static box is on layer 1, the farther target is on layer 2, and the ray includes only layer 2. Its hit body is therefore the target rather than the nearer box. Checking the returned body reference is more useful than checking only that something was hit.

For an “everything except this layer” query, invert the single-layer mask with bitwise NOT on the unsigned value. This changes only that query; it does not change how the excluded body collides with the rest of the world. A trigger is a separate property again: the 2D ray method skips triggers, regardless of the mask.

The download also exercises a restore, not just a fresh setup. It writes the space through SyncWriter, restores into another space with the same tick rate, checks the reader result, and steps two bodies through one another. That shows the disabled pair remains disabled after loading. In a game, attach the space in RegisterSystems so a world built to load a save has the required table and stepping system before the restore starts.

All onegame recipes