reckfellENGINE

Коллизии 3D: простые формы и составные тела

Полный уровень с падающим ящиком, полом, стеной и выбором мышью; границы применения convex и trimesh.

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

Запустить пример 3D

Добавьте LabBody3D.cs, ScreenPicker3D.cs и CollisionLab3D.cs в C#-проект. Создайте отдельный уровень с корнем Entity3D, прикрепите CollisionLab3D, соберите и запустите F6. Оставьте корень без масштаба/поворота, физику 60 шагов/с и гравитацию 3D 9.8 с направлением (0, -1, 0). Камера, свет, видимые меши и коллизии создаются кодом.

Ящик падает на пол. Space один раз прикладывает импульс вверх; R запрашивает сброс через _IntegrateForces. Левый щелчок по ящику записывает результат в Picker.LastHit; это данные запроса без автоматического выделения материала. Луч Ray проверяет стену и доступен для просмотра через IsColliding/GetCollisionPoint.

Ящик на полу рядом со стеной в работающем примере 3D

LabBody3D.cs
C#
using System;
using Reckfell;

public partial class LabBody3D : RigidBody3D
{
    private Vector3? _resetPosition;
    public int ResetCount { get; private set; }
    public void RequestReset(Vector3 worldPosition)
    {
        if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
        _resetPosition = worldPosition;
        Sleeping = false;
    }
    public override void _IntegrateForces(PhysicsDirectBodyState3D state)
    {
        if (_resetPosition is not Vector3 position) return;
        state.Transform = new Transform3D(Basis.Identity, position);
        state.LinearVelocity = Vector3.Zero;
        state.AngularVelocity = Vector3.Zero;
        _resetPosition = null;
        ResetCount++;
    }
}
ScreenPicker3D.cs
C#
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;

public partial class ScreenPicker3D : Entity
{
    [Export] public Camera3D? Camera { get; set; }
    [Export] public uint Mask { get; set; } = 4;
    [Export] public float Distance { get; set; } = 100;
    private Vector2? _pending;
    public Hit LastHit { get; private set; } = new();
    public int QueryCount { get; private set; }
    public void RequestPick(Vector2 viewportPosition) { if (viewportPosition.IsFinite()) _pending = viewportPosition; }
    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } click)
            RequestPick(click.Position);
    }
    public override void _PhysicsProcess(double delta)
    {
        if (_pending is not Vector2 screen) return;
        _pending = null; LastHit = new();
        if (Camera is null || !ReckfellObject.IsInstanceValid(Camera) || !Camera.IsInsideTree() || !float.IsFinite(Distance) || Distance <= 0) return;
        Vector3 origin = Camera.ProjectRayOrigin(screen);
        Vector3 end = origin + Camera.ProjectRayNormal(screen) * Distance;
        var query = PhysicsRayQueryParameters3D.Create(origin, end, Mask);
        query.CollideWithAreas = false;
        LastHit = Camera.GetWorld3D().DirectSpaceState.IntersectRay(query);
        QueryCount++;
    }
}
CollisionLab3D.cs
C#
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;

public partial class CollisionLab3D : Entity3D
{
    public LabBody3D Crate { get; private set; } = null!;
    public StaticBody3D Floor { get; private set; } = null!;
    public StaticBody3D Wall { get; private set; } = null!;
    public RayCast3D Ray { get; private set; } = null!;
    public Camera3D Camera { get; private set; } = null!;
    public ScreenPicker3D Picker { get; private set; } = null!;
    public int Contacts { get; private set; }
    public override void _Ready()
    {
        Floor = StaticBox("Floor", new Vector3(0, -.25f, 0), new Vector3(12, .5f, 8));
        Wall = StaticBox("Wall", new Vector3(3, 1.5f, 0), new Vector3(.5f, 3, 4));
        Crate = new LabBody3D { Name = "Crate", Position = new Vector3(0, 3, 0), Mass = 2,
            LockRotation = true, CollisionLayer = 4, CollisionMask = 1, ContactMonitor = true, MaxContactsReported = 8,
            PhysicsMaterialOverride = new PhysicsMaterial { Bounce = 0, Friction = 1 } };
        Crate.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One } });
        AddVisual(Crate, Vector3.One, new Color(.25f, .45f, 1));
        Crate.BodyEntered += body => { if (body == Floor) Contacts++; };
        AddChild(Crate);
        Ray = new RayCast3D { Position = new Vector3(-4, 1.5f, 0), TargetPosition = new Vector3(8, 0, 0),
            CollisionMask = 1, CollideWithAreas = false, Enabled = true }; AddChild(Ray);
        Camera = new Camera3D { Position = new Vector3(8, 7, 10), Current = true }; AddChild(Camera);
        Camera.LookAt(ToGlobal(new Vector3(0, 1, 0)));
        Picker = new ScreenPicker3D { Camera = Camera }; AddChild(Picker);
        var light = new DirectionalLight3D { RotationDegrees = new Vector3(-55, -30, 0), LightEnergy = 1.3f }; AddChild(light);
        var environment = new Reckfell.Environment { BackgroundMode = Reckfell.Environment.BGMode.Color,
            BackgroundColor = new Color(.035f, .035f, .045f), AmbientLightSource = Reckfell.Environment.AmbientSource.Color,
            AmbientLightColor = Colors.White, AmbientLightEnergy = .55f };
        AddChild(new WorldEnvironment { Environment = environment });
    }
    private StaticBody3D StaticBox(string name, Vector3 position, Vector3 size)
    {
        var body = new StaticBody3D { Name = name, Position = position, CollisionLayer = 1, CollisionMask = 0 };
        body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
        AddVisual(body, size, new Color(.3f, .3f, .34f)); AddChild(body); return body;
    }
    private static void AddVisual(Entity3D parent, Vector3 size, Color color)
    {
        parent.AddChild(new MeshInstance3D { Mesh = new BoxMesh { Size = size }, MaterialOverride =
            new StandardMaterial3D { AlbedoColor = color, Roughness = .85f } });
    }
    public Hit Cast(Vector3 worldFrom, Vector3 worldTo, uint mask = 1, Rid? exclude = null, bool inside = false)
    {
        var query = PhysicsRayQueryParameters3D.Create(worldFrom, worldTo, mask);
        query.HitFromInside = inside;
        if (exclude is Rid rid) query.Exclude = new Reckfell.Collections.Array<Rid> { rid };
        return GetWorld3D().DirectSpaceState.IntersectRay(query);
    }
    public override void _UnhandledKeyInput(InputEvent @event)
    {
        if (@event is not InputEventKey { Pressed: true, Echo: false } key) return;
        if (key.PhysicalKeycode == Key.R) Crate.RequestReset(ToGlobal(new Vector3(0, 3, 0)));
        if (key.PhysicalKeycode == Key.Space) Crate.ApplyCentralImpulse(Vector3.Up * 10);
    }
}

Иерархия и размеры

CollisionShape3D должен быть прямым ребёнком тела или Area3D. MeshInstance3D рядом с ним отвечает только за изображение. У пола BoxShape3D.Size = (12, 0.5, 8), у ящика (1, 1, 1). Size — полный размер, не половинные extents. Положение дочерней формы задаёт её смещение относительно тела.

Для составного объекта добавьте несколько CollisionShape3D под одно тело. Это позволяет представить вогнутую вещь набором простых/выпуклых частей без динамического trimesh. Layer и Mask задаются у тела целиком. Для выбора отдельной части используйте индекс shape из результата запроса и сопоставление владельцев форм, а не предполагайте совпадение индекса с GetChild.

Какую форму выбрать

ГеометрияПодходит
BoxShape3D, SphereShape3DПростые ящики, стены, круглые предметы
CapsuleShape3DПерсонаж; радиус и полная высота, без острых нижних углов
CylinderShape3DЦилиндрические предметы; отдельно проверяйте устойчивость контактов выбранного backend
ConvexPolygonShape3DВыпуклая оболочка; вырезы и отверстия оболочка заполнит
Несколько convex/примитивовПодвижный предмет сложной формы
ConcavePolygonShape3D / trimeshНеподвижная треугольная геометрия окружения; не объём динамического тела
HeightMapShape3DСтатический рельеф с ограничениями представления карты высот
WorldBoundaryShape3DБесконечная статическая граница

Concave/trimesh — поверхность треугольников; объект целиком внутри замкнутого меша не обязательно определяется как пересечение заполненного объёма. Не назначайте его обычному динамическому RigidBody3D или CharacterBody3D. Создание convex из модели меняет геометрию; проверьте отверстия, ступени и число полученных частей. Автоматическое создание не гарантирует подходящую игровую форму.

Масштаб, ресурсы и цена

Оставляйте масштаб физической ветви единичным, задавайте размеры в ресурсе. В частности, движок предупреждает о неравномерном масштабе CollisionShape3D. Масштаб родительской сущности тоже наследуется. Отделяйте визуальные дочерние объекты от физической геометрии.

Shape3D — разделяемый ресурс. Если несколько экземпляров используют один BoxShape3D, изменение Size затронет их все; Duplicate изолирует форму. Отключайте CollisionShape3D через SetDeferred из физического сигнала. Disabled убирает форму из запросов/контактов после применения изменения; скрытие MeshInstance этого не делает.

Примитивы обычно дают простые контакты, но результат зависит от числа тел, форм, пересечений и backend. Слишком много convex-частей тоже дорого. Не переделывайте все статические меши в отдельные динамические тела. Профилируйте репрезентативный уровень; пример с одним ящиком не подтверждает бюджет большого мира или устойчивость высокой стопки.

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

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

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

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