Area2D: вход, выход и действие один раз
Полный пример зоны: фильтры, сигналы, отложенное отключение и влияние на гравитацию.
Проверить обнаружение
Запустите тот же самостоятельный уровень: добавьте оба файла ниже, прикрепите CollisionLab2D к корню Entity2D и нажмите F6 после сборки. Настройки: окно 640×420, 60 шагов/с, гравитация 980 вниз. Ящик проходит через прямоугольную зону, а Entries/Exits увеличиваются. Форма зоны не останавливает тело.
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);
}
}Условия сигнала
У Area2D включён Monitoring, есть активная дочерняя форма и подходящая CollisionMask. Здесь Mask = 4 проверяет CollisionLayer ящика = 4. CollisionMask ящика = 1 нужна для пола; она не определяет, видит ли его эта зона. BodyEntered/BodyExited передают физическое тело; AreaEntered/AreaExited — другую Area2D. Для обнаружения другой зоны важен её Monitorable: это разрешение быть обнаруженной, а не включение собственного наблюдения.
GetOverlappingBodies/Areas возвращают состояние последнего физического обновления. Изменение Position и немедленное чтение списка в следующей строке не подтверждает отсутствие пересечения. Используйте события либо читайте после нужного шага. При выключенном Monitoring эти запросы не являются способом продолжать наблюдение.
Сигнал BodyEntered относится к телу, BodyShapeEntered — к конкретной паре форм и может возникать несколько раз для составного объекта. Не начисляйте награду за каждый shape-сигнал, если нужна одна награда за предмет. Проверяйте нужный объект, группу или компонент и актуальность его состояния; само пересечение не означает допустимость действия.
Подбор предмета один раз
Включите ConsumeOnEnter до входа ящика в зону. Обработчик сначала выставляет Consumed, затем отключает Monitoring отложенно. Повторный callback до применения изменения уже не выполнит действие. В игровом предмете аналогично сначала зафиксируйте результат, затем отложенно отключите форму/наблюдение или вызовите QueueFree.
Прямое изменение Monitoring/Monitorable или структуры столкновений внутри сигнала может попасть в заблокированный физический callback. SetDeferred нужен из-за фазы обработки, а не как задержка на произвольное число секунд. Не используйте таймер вместо исправления порядка операций. Отключение наблюдения может очистить отслеживаемые пересечения и вызвать события выхода; такой выход не обязательно означает физическое перемещение тела.
Изменить гравитацию и сопротивление
GravitySpaceOverride управляет способом совмещения поля с миром и другими зонами; само изменение Gravity при Disabled не включает поле. Для невесомости задайте Replace и Gravity = 0. Это убирает ускорение от гравитации, но не уже набранную скорость. Для остановки нужен отдельный расчёт движения/сопротивления. В тесте сброс тела внутри уже активной зоны проверяется отдельно от входа: изменения поля и списка пересечений применяются на физических шагах.
| Режим | Смысл |
|---|---|
| Disabled | Не изменять это свойство среды |
| Combine | Прибавить вклад и продолжить обработку |
| CombineReplace | Прибавить вклад, остановить обработку нижних приоритетов |
| Replace | Заменить вклад, остановить дальнейшую обработку |
| ReplaceCombine | Заменить вклад, разрешить дальнейшее совмещение |
Учитывайте Priority, отдельные override-режимы гравитации/linear damp/angular damp и параметры тела. Не рассчитывайте на значимый порядок зон с равным приоритетом. У точечной гравитации GravityDirection задаёт локальное положение центра, GravityPoint включает режим, а GravityPointUnitDistance определяет нормализацию силы по расстоянию; проверьте центр и край зоны. Это физическое поле для поддерживающих его тел, а не автоматическая сила движения вашего CharacterBody.
Area2D также может задавать аудиошину; обнаружение, физическое поле и аудио — отдельные настройки. Если не пришёл сигнал, сначала проверьте форму, Monitoring, Mask/Layer, тип цели и физический шаг; если тело не остановилось — это нормальное поведение зоны, для стены нужно физическое тело.