Камера с препятствиями: SpringArm3D
Настроить вращение и дистанцию камеры, исключить тело персонажа и разобраться с Shape, Margin и слоями.
Что получится
Камера вращается вокруг неподвижного объекта и приближается перед стеной. Стрелки ←/→ вращают пружину, ↑/↓ меняют требуемую дистанцию от 2 до 40. Это отдельный пример камеры: движения персонажа, мыши и плавного следования здесь нет.
SpringArm3D проверяет препятствия вдоль локальной +Z и перемещает прямых Entity3D-потомков на найденную дистанцию. Camera3D смотрит вдоль своей -Z, поэтому камера на положительной Z направлена обратно к объекту. Не помещайте тело персонажа прямым потомком пружины: оно тоже будет перемещаться.
Запустить полный пример
Нужны C#-проект Reckfell.NET.Sdk 1.0.0, net10.0 и уровень .rscene с корнем Entity3D. Сохраните все три файла ниже, выполните dotnet build и прикрепите ViewLab3D.cs к корню. Включите экспортированное UseWall до запуска для проверки стены. По умолчанию оно выключено, чтобы видеть объект и переключение его моделей без препятствия.
В проекте должны существовать действия ui_left, ui_right, ui_up и ui_down с назначенными стрелками. Если их нет в Input Map, добавьте их. Ресурсы, камера, подпись, пол и коллизии создаются кодом; внешние модели не нужны. Точные действия интерфейса редактора этим примером не проверяются.
LodVisual3D.cs
using Reckfell;
public partial class LodVisual3D : MeshInstance3D
{
public int ProcessTicks { get; private set; }
public override void _Process(double delta) => ProcessTicks++;
}ViewLabMeshes.cs
using Reckfell;
public static class ViewLabMeshes
{
public static StandardMaterial3D Material(Color color) => new()
{
AlbedoColor = color,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
};
public static MeshInstance3D Box(Vector3 size, Color color) => new()
{
Mesh = new BoxMesh { Size = size },
MaterialOverride = Material(color)
};
public static TextMesh Text(string text) => new()
{
Text = text,
FontSize = 64,
PixelSize = 0.01f,
Depth = 0.15f
};
}ViewLab3D.cs
using Reckfell;
public partial class ViewLab3D : Entity3D
{
[Export] public float Distance { get; set; } = 6;
[Export] public bool UseWall { get; set; }
public SpringArm3D Boom { get; private set; } = null!;
public Camera3D Camera { get; private set; } = null!;
public LodVisual3D NearModel { get; private set; } = null!;
public LodVisual3D FarModel { get; private set; } = null!;
public Label3D Caption { get; private set; } = null!;
public MeshInstance3D Floor { get; private set; } = null!;
public StaticBody3D ActorBody { get; private set; } = null!;
public StaticBody3D? WallBody { get; private set; }
public override void _Ready()
{
ActorBody = new StaticBody3D { Position = new Vector3(0, 1, 0) };
AddChild(ActorBody);
ActorBody.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(1.4f, 2, 1.4f) } });
NearModel = new LodVisual3D
{
Position = ActorBody.Position,
Mesh = new SphereMesh { Radius = 0.7f, Height = 1.4f, RadialSegments = 64, Rings = 32 },
MaterialOverride = ViewLabMeshes.Material(Colors.White),
VisibilityRangeEnd = 12
};
FarModel = new LodVisual3D
{
Position = ActorBody.Position,
Mesh = new BoxMesh { Size = new Vector3(1.2f, 1.2f, 1.2f) },
MaterialOverride = ViewLabMeshes.Material(new Color(0.08f, 0.25f, 1)),
VisibilityRangeBegin = 12
};
AddChild(NearModel);
AddChild(FarModel);
Floor = new MeshInstance3D
{
Mesh = new PlaneMesh { Size = new Vector2(20, 20) },
MaterialOverride = ViewLabMeshes.Material(new Color(0.1f, 0.12f, 0.15f))
};
AddChild(Floor);
Caption = new Label3D
{
Text = "Reckfell",
Position = new Vector3(0, 2.1f, 0),
FontSize = 48,
PixelSize = 0.008f,
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled,
Modulate = new Color(0.6f, 0.75f, 1)
};
AddChild(Caption);
if (UseWall)
{
WallBody = new StaticBody3D { Position = new Vector3(0, 1, 3) };
AddChild(WallBody);
WallBody.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(6, 4, 0.4f) } });
WallBody.AddChild(ViewLabMeshes.Box(new Vector3(6, 4, 0.4f), new Color(0.1f, 0.12f, 0.15f)));
}
Boom = new SpringArm3D
{
Position = new Vector3(0, 1, 0),
Shape = new SphereShape3D { Radius = 0.2f },
CollisionMask = 1,
Margin = 0.15f
};
AddChild(Boom);
Boom.AddExcludedObject(ActorBody.GetRid());
Camera = new Camera3D { Current = true, Near = 0.05f, Fov = 60 };
Boom.AddChild(Camera);
SetDistance(Distance);
}
public void SetDistance(float distance)
{
if (!float.IsFinite(distance)) return;
Distance = Mathf.Clamp(distance, 2, 40);
Boom.SpringLength = Distance;
}
public override void _PhysicsProcess(double delta)
{
SetDistance(Distance + Input.GetAxis("ui_up", "ui_down") * 8 * (float)delta);
Boom.RotateY(Input.GetAxis("ui_left", "ui_right") * 1.5f * (float)delta);
}
}Метод SetDistance вызывайте после создания Boom в _Ready. До запуска задавайте экспортированное Distance. Не меняйте UseWall во время игры в ожидании появления стены: пример читает его только при создании уровня.
Понять три способа проверки
| Условие | Запрос | Что учитывать |
|---|---|---|
| Shape задана | Проверка формы | Здесь сфера радиусом 0.2; её размер определяет зазор |
| Shape=null, есть прямой Camera3D-потомок | Автоматическая форма камеры | Камера внутри дополнительного Entity3D уже не прямой потомок |
| Shape=null, прямой камеры нет | Луч | Только в этой ветке проверенной версии вычитается Margin |
Margin не применяется к явно заданной форме и автоматической форме камеры в проверенной версии. Не увеличивайте его бесконечно, если сфера всё ещё подходит близко к стене: подберите Shape и Near камеры. Слишком большой ближний срез или слишком маленькая форма могут оставить видимые проблемы у углов. Пример не доказывает защиту от любого быстрого движения или сложной геометрии.
GetHitLength возвращает последний физический результат. Сразу после присваивания SpringLength ещё нельзя ожидать нового значения; дождитесь физического обновления. Сохраняйте единичный масштаб пружины и её родителей, чтобы длины интерпретировались предсказуемо.
Исключить себя и настроить слои
CollisionMask=1 проверяет первый физический слой. Стена должна иметь CollisionShape3D и соответствующий CollisionLayer; один MeshInstance3D не останавливает камеру. CollisionMask=0 исключает все слои и полезен для диагностики, но отключает защиту от препятствий.
Boom.AddExcludedObject(ActorBody.GetRid()) исключает тело цели. Нужен RID физического тела, не GetInstanceId() и не RID меша. Исключение касается запросов пружины, а не столкновений персонажа с миром.
Все прямые Entity3D-потомки получают позицию на оси пружины; их боковое локальное смещение не сохраняется. Для плечевой камеры смещайте подходящий родительский pivot, а не рассчитывайте на Camera.Position.X, которую пружина перепишет.
Проверить результат и найти ошибку
При UseWall=true на исходной дистанции 6 камера остаётся перед стеной, а GetHitLength меньше SpringLength. Поверните пружину в свободную сторону: камера снова отдаляется. В тесте форму, автоматическую камеру и луч проверяли отдельно; уменьшение длины на величину Margin подтверждено только для луча.
| Симптом | Проверка |
|---|---|
| Камера за стеной | Физическая форма стены, CollisionLayer, CollisionMask и включённая обработка пружины |
| Камера упирается в цель | Исключён ли RID тела цели |
| Изменение Margin ничего не даёт | Какая ветка запроса используется; с Shape это ожидаемо |
| Камера сместила лишний объект | Нет ли других прямых Entity3D-потомков пружины |
| Стрелки не работают | Действия Input Map и фокус окна игры |
| Нельзя увидеть дальнюю модель | Отключите UseWall до запуска: стена сокращает фактическую дистанцию |
Следующие задачи: текст в мире, дистанционная видимость, SpringArm3D API.