2D collisions: shapes, layers and a working example
A falling body, floor, area and ray in one level. Choose a shape and avoid changing every instance at once.
Run the level
Add LabBody2D.cs and CollisionLab2D.cs to a C# project. Create a separate level with an Entity2D root, attach CollisionLab2D, build and run with F6. Use a window of 640×420 or larger, 60 physics ticks/s and 2D gravity 980 in direction (0, 1). Leave the sample root unscaled and unrotated.
The blue crate falls through the outlined area and rests on the floor. Counters show entry/exit; the white ray stops at the wall. R returns the crate above through the physics integrator. Text and rectangles are drawn by code to explain physics; game colliders are not automatically visible.
LabBody2D.cs
using System;
using Reckfell;
public partial class LabBody2D : RigidBody2D
{
private Vector2? _resetPosition;
public int ResetCount { get; private set; }
public void RequestReset(Vector2 worldPosition)
{
if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
_resetPosition = worldPosition;
Sleeping = false;
}
public override void _IntegrateForces(PhysicsDirectBodyState2D state)
{
if (_resetPosition is not Vector2 position) return;
state.Transform = new Transform2D(0, position);
state.LinearVelocity = Vector2.Zero;
state.AngularVelocity = 0;
_resetPosition = null;
ResetCount++;
}
}CollisionLab2D.cs
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;
public partial class CollisionLab2D : Entity2D
{
public LabBody2D Crate { get; private set; } = null!;
public StaticBody2D Floor { get; private set; } = null!;
public StaticBody2D Wall { get; private set; } = null!;
public Area2D Trigger { get; private set; } = null!;
public RayCast2D Ray { get; private set; } = null!;
public int Entries { get; private set; }
public int Exits { get; private set; }
public bool ConsumeOnEnter { get; set; }
public bool Consumed { get; private set; }
public Hit LastRayHit { get; private set; } = new();
public override void _Ready()
{
Floor = StaticBox("Floor", new Vector2(320, 320), new Vector2(600, 40));
Wall = StaticBox("Wall", new Vector2(480, 180), new Vector2(32, 240));
Crate = new LabBody2D { Name = "Crate", Position = new Vector2(160, 40), Mass = 2,
LockRotation = true, CollisionLayer = 4, CollisionMask = 1, ContactMonitor = true, MaxContactsReported = 8,
PhysicsMaterialOverride = new PhysicsMaterial { Bounce = 0, Friction = 1 } };
Crate.AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = new Vector2(40, 40) } });
AddChild(Crate);
Trigger = new Area2D { Name = "Trigger", Position = new Vector2(160, 170), CollisionLayer = 0,
CollisionMask = 4, Monitoring = true };
Trigger.AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = new Vector2(120, 120) } });
Trigger.BodyEntered += OnBodyEntered;
Trigger.BodyExited += body => { if (body == Crate) Exits++; };
AddChild(Trigger);
Ray = new RayCast2D { Position = new Vector2(20, 170), TargetPosition = new Vector2(540, 0),
CollisionMask = 1, CollideWithAreas = false, Enabled = true };
AddChild(Ray);
}
private StaticBody2D StaticBox(string name, Vector2 position, Vector2 size)
{
var body = new StaticBody2D { Name = name, Position = position, CollisionLayer = 1, CollisionMask = 0 };
body.AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = size } });
AddChild(body); return body;
}
private void OnBodyEntered(Entity2D body)
{
if (body != Crate || Consumed) return;
Entries++;
if (!ConsumeOnEnter) return;
Consumed = true;
Trigger.SetDeferred(Area2D.PropertyName.Monitoring, false);
}
public Hit Cast(Vector2 worldFrom, Vector2 worldTo, uint mask = 1, bool areas = false, Rid? exclude = null, bool inside = false)
{
var query = PhysicsRayQueryParameters2D.Create(worldFrom, worldTo, mask);
query.CollideWithAreas = areas; query.HitFromInside = inside;
if (exclude is Rid rid) query.Exclude = new Reckfell.Collections.Array<Rid> { rid };
return GetWorld2D().DirectSpaceState.IntersectRay(query);
}
public override void _PhysicsProcess(double delta)
{
LastRayHit = Cast(Ray.GlobalPosition, Ray.ToGlobal(Ray.TargetPosition));
QueueRedraw();
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event is InputEventKey { Pressed: true, Echo: false, PhysicalKeycode: Key.R })
Crate.RequestReset(ToGlobal(new Vector2(160, 40)));
}
public override void _Draw()
{
if (Crate is null) return;
DrawRect(new Rect2(Floor.Position - new Vector2(300, 20), new Vector2(600, 40)), new Color(.22f, .22f, .25f));
DrawRect(new Rect2(Wall.Position - new Vector2(16, 120), new Vector2(32, 240)), new Color(.4f, .4f, .44f));
DrawRect(new Rect2(Trigger.Position - new Vector2(60, 60), new Vector2(120, 120)), new Color(.4f, .6f, 1), false, 2);
DrawRect(new Rect2(ToLocal(Crate.GlobalPosition) - new Vector2(20, 20), new Vector2(40, 40)), new Color(.25f, .45f, 1));
Vector2 end = LastRayHit.Count > 0 ? ToLocal(LastRayHit["position"].AsVector2()) : Ray.Position + Ray.TargetPosition;
DrawLine(Ray.Position, end, Colors.White, 2); DrawCircle(end, 4, Colors.White);
bool ru = TranslationServer.GetLocale().StartsWith("ru");
DrawString(ThemeDB.FallbackFont, new Vector2(24, 28), ru ? "R — сбросить тело" : "R — reset body", fontSize: 18);
DrawString(ThemeDB.FallbackFont, new Vector2(24, 380), (ru ? "Вход / выход: " : "Enter / exit: ") + Entries + " / " + Exits, fontSize: 18);
}
}Shape structure
CollisionShape2D is a direct child of a CollisionObject2D: a physics body or Area2D. Its Shape is a geometry resource. Here the StaticBody2D floor and RigidBody2D crate each contain a RectangleShape2D. Combine several CollisionShape2D children under one body for a compound shape; nesting a shape under an ordinary Entity2D does not attach it to a higher body.
Layer/Mask belong to the body or area, not the RectangleShape2D resource. The crate belongs to layer 3 (bit 4), detects the world (bit 1), and the area checks bit 4. The floor does not need to detect the area. An Area does not block the crate. Areas and physics bodies have different roles.
Choose geometry
| Shape | Use / limitation |
|---|---|
| CircleShape2D | Round objects; specify a radius, not a diameter |
| RectangleShape2D | Floors, walls and crates; Size is the full width and height |
| CapsuleShape2D | Characters with rounded ends; set radius and total height |
| SegmentShape2D | A segment, not a filled platform |
| SeparationRayShape2D | A specialized separation shape for character movement, not arbitrary physics geometry |
| WorldBoundaryShape2D | An infinite boundary, not a moving crate |
| ConvexPolygonShape2D | One filled convex outline without concave cutouts |
| ConcavePolygonShape2D | Segments for static surroundings, not a filled volume |
CollisionPolygon2D in solid-polygon mode can decompose a concave outline into convex pieces. Segment mode has different semantics. Inspect the decomposition: an overly detailed sprite outline adds contacts and computation. A simplified shape often matches intended gameplay better than every image pixel.
Size and shared resources
Resize the Shape resource while retaining unit scale in the physics branch, including inherited scale. Scale a visual Sprite separately if needed, then check alignment. Nonuniform or negative scaling is not a universal way to configure every physics shape.
Two CollisionShape2D entities can share one Shape. Changing RectangleShape2D.Size then changes both colliders. Assign a Duplicate before editing an independent instance. To enable/disable from a physics signal, use SetDeferred with CollisionShape2D.PropertyName.Disabled; a local handled flag prevents repeated actions before the deferred operation runs.
Verify the result
Enable visible collision shapes in the editor debug menu during a run and compare them with the artwork. A falling-through body needs checks for shape parent, Disabled, size, filters and speed. A change affecting every prefab suggests a shared resource. Performance depends on active bodies/contacts and shape pieces, not only entity count. Avoid turning a whole large level into one complex dynamic collider; measure suitable static-region partitioning.