Area2D: entry, exit and one-time actions
A complete area example: filters, signals, deferred disabling and gravity effects.
Verify detection
Run the same standalone level: add both files below, attach CollisionLab2D to an Entity2D root, build and press F6. Use a 640×420 window, 60 ticks/s and downward gravity 980. The crate passes through the rectangular area and Entries/Exits increase. The area shape does not stop the body.
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);
}
}Signal requirements
Area2D needs Monitoring, an active child shape and a matching CollisionMask. Here Mask = 4 tests crate CollisionLayer = 4. The crate Mask = 1 is for the floor; it does not determine whether this area detects it. BodyEntered/BodyExited supply a physics body; AreaEntered/AreaExited supply another Area2D. Detecting another area also depends on its Monitorable, which permits detection rather than enabling its own monitoring.
GetOverlappingBodies/Areas expose the last physics update. Changing Position and immediately reading the list does not prove there is no overlap. Use signals or read after the required tick. These queries are not a way to keep observing with Monitoring disabled.
BodyEntered concerns a body; BodyShapeEntered concerns a shape pair and can occur multiple times for a compound object. Do not grant one-item rewards per shape event. Validate the intended object, group or component and its current state; overlap alone does not authorize a gameplay action.
Consume a pickup once
Enable ConsumeOnEnter before the crate enters. The handler first sets Consumed, then disables Monitoring through a deferred operation. A repeated callback before application cannot perform the action again. A game pickup should similarly commit its result before deferred shape/monitoring changes or QueueFree.
Directly changing Monitoring/Monitorable or collision structure inside a signal can reach a locked physics callback. SetDeferred addresses processing phase, not an arbitrary seconds-long delay. Do not substitute a timer for correct operation order. Disabling monitoring can clear tracked overlaps and emit exit events; such an exit does not necessarily mean the body physically moved out.
Change gravity and damping
GravitySpaceOverride controls combining with the world and other areas; editing Gravity while Disabled does not enable a field. For zero gravity, choose Replace and Gravity = 0. This removes gravitational acceleration but does not erase existing velocity. Stopping needs separate motion/damping logic. The test resets a body inside an already active area separately from entering it: fields and overlap lists apply on physics ticks.
| Mode | Meaning |
|---|---|
| Disabled | Do not override this environment property |
| Combine | Add the contribution and continue processing |
| CombineReplace | Add the contribution and stop lower-priority processing |
| Replace | Replace the contribution and stop further processing |
| ReplaceCombine | Replace the contribution and allow subsequent combining |
Account for Priority, separate gravity/linear-damping/angular-damping override modes and body parameters. Do not rely on a meaningful order for equal-priority areas. With point gravity, GravityDirection defines the local centre position, GravityPoint enables the mode and GravityPointUnitDistance controls distance normalization; test both centre and edge. This is a field for bodies that support it, not automatic movement force for your CharacterBody.
Area2D can also override an audio bus; detection, physics fields and audio are separate settings. For a missing signal, check shape, Monitoring, Mask/Layer, target type and physics tick first. A body not stopping is normal area behavior; use a physics body for a wall.