reckfellENGINE

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.

On this page

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.

Falling crate inside the area, with floor, wall and ray in the running example

LabBody2D.cs
C#
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
C#
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

ShapeUse / limitation
CircleShape2DRound objects; specify a radius, not a diameter
RectangleShape2DFloors, walls and crates; Size is the full width and height
CapsuleShape2DCharacters with rounded ends; set radius and total height
SegmentShape2DA segment, not a filled platform
SeparationRayShape2DA specialized separation shape for character movement, not arbitrary physics geometry
WorldBoundaryShape2DAn infinite boundary, not a moving crate
ConvexPolygonShape2DOne filled convex outline without concave cutouts
ConcavePolygonShape2DSegments 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.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.