reckfellENGINE

Коллизии 2D: формы, слои и рабочий пример

Падающее тело, пол, зона и луч в одном уровне. Как выбрать форму и не изменить все экземпляры сразу.

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

Запустить уровень

Добавьте LabBody2D.cs и CollisionLab2D.cs в C#-проект. Создайте отдельный уровень с корнем Entity2D, прикрепите CollisionLab2D, соберите проект и запустите F6. Размер окна для примера — 640×420 или больше; частота физики 60, гравитация 2D 980 с направлением (0, 1). Корень примера оставьте без масштаба и поворота.

Синий ящик падает сквозь контур зоны и останавливается на полу. Счётчики показывают вход/выход; белый луч упирается в стену. R возвращает ящик наверх через физический интегратор. Текст и прямоугольники нарисованы кодом; это пояснение физики, а не автоматически видимые коллайдеры игры.

Падающий ящик внутри зоны, пол, стена и луч в работающем примере

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

Как устроена форма

В иерархии CollisionShape2D — прямой ребёнок CollisionObject2D: физического тела или Area2D. Её Shape — ресурс геометрии. В примере StaticBody2D содержит RectangleShape2D пола, RigidBody2D — RectangleShape2D ящика. Объединяйте несколько CollisionShape2D под одним телом для составной формы; вложение под обычный Entity2D не превращает её в форму родительского тела.

Layer/Mask находятся у тела или зоны, не у ресурса RectangleShape2D. В примере ящик принадлежит слою 3 (бит 4), обнаруживает мир (бит 1), а зона проверяет бит 4. Пол не обязан обнаруживать зону. Area не блокирует ящик. Зона и физическое тело — разные роли.

Выбрать геометрию

ФормаПрименение / ограничение
CircleShape2DКруглые объекты; радиус, а не диаметр
RectangleShape2DПол, стены и ящики; Size — полная ширина и высота
CapsuleShape2DПерсонаж с округлёнными краями; настройте радиус и полную высоту
SegmentShape2DОтрезок, не заполненная площадка
SeparationRayShape2DСпециальная форма разделения для движения персонажей; не заменяет произвольную физическую геометрию
WorldBoundaryShape2DБесконечная граница; не подвижный ящик
ConvexPolygonShape2DОдин выпуклый заполненный контур без вогнутых вырезов
ConcavePolygonShape2DНабор отрезков для статического окружения; не заполненный объём

CollisionPolygon2D с режимом заполненного многоугольника может разложить вогнутый контур на выпуклые части. Режим сегментов имеет другую семантику. Проверьте полученное разложение: очень подробная обводка спрайта добавляет лишние контакты и вычисления. Упрощённая форма часто лучше совпадает с ожидаемым игровым поведением, чем каждый пиксель изображения.

Размеры и общие ресурсы

Меняйте размеры ресурса Shape, сохраняя масштаб физической ветви единичным. Это особенно важно при наследовании масштаба от родителя. Видимый Sprite можно масштабировать отдельно, но затем проверьте совпадение рисунка и коллизии. Неравномерный или отрицательный масштаб не является универсальным способом настроить любую физическую форму.

Два CollisionShape2D могут ссылаться на один Shape. Изменение RectangleShape2D.Size тогда изменит оба коллайдера. Для независимого экземпляра назначьте копию через Duplicate, затем меняйте её. Для включения/отключения из физического сигнала используйте SetDeferred с CollisionShape2D.PropertyName.Disabled; локальный флаг обработки предотвращает повторное действие до выполнения отложенной операции.

Проверить результат

Во время запуска включите отображение видимых форм коллизий в отладочном меню редактора и сопоставьте форму с рисунком. Если тело проваливается, проверьте родителя формы, Disabled, размеры, фильтры и скорость. Если изменение затронуло все префабы — общий ресурс. Если проседает производительность — число активных тел/контактов и частей формы, а не только число сущностей. Не объединяйте весь большой уровень в один сложный динамический коллайдер; измеряйте подходящее разбиение на статические участки.

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

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

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

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