reckfellENGINE

Area2D: вход, выход и действие один раз

Полный пример зоны: фильтры, сигналы, отложенное отключение и влияние на гравитацию.

На этой странице

Проверить обнаружение

Запустите тот же самостоятельный уровень: добавьте оба файла ниже, прикрепите CollisionLab2D к корню Entity2D и нажмите F6 после сборки. Настройки: окно 640×420, 60 шагов/с, гравитация 980 вниз. Ящик проходит через прямоугольную зону, а Entries/Exits увеличиваются. Форма зоны не останавливает тело.

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);
    }
}

Условия сигнала

У 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, тип цели и физический шаг; если тело не остановилось — это нормальное поведение зоны, для стены нужно физическое тело.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.