reckfellENGINE

XR: движение стиком и шаговый поворот

Рабочая основа локомоции: коллайдер, origin, физический шаг, ввод и диагностика.

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

Этот пример добавляет движение левым стиком, шаговый поворот правым и коллизии игрока. Нужны рабочая XR-сессия и карта действий. Физическое перемещение головы обрабатывается отдельно от игрового: камера остаётся под управлением tracking.

Подготовьте проект

  1. Выполните настройку XR и добавьте XrStartup из обработки запуска.
  2. Скопируйте RoomScaleBody.cs из room scale. Его код здесь используется без изменений.
  3. Используйте вертикальный риг без масштаба и наклона тела: мировой +Y направлен вверх, XRServer.WorldScale=1. В этих условиях одна единица соответствует метру.
  4. Создайте структуру ниже. У Player должна быть CapsuleShape3D: Radius=0.2, Height=1.8, позиция CollisionShape3D=(0, 0.9, 0). Начальная позиция Player — на уровне пола.
Текст
Level
├── XrStartup (XrStartup.cs)
├── Player (CharacterBody3D, XrPlayer.cs)
│   ├── CollisionShape3D (CapsuleShape3D)
│   └── XROrigin3D
│       ├── XRCamera3D (head)
│       ├── XRController3D (left_hand, grip)
│       └── XRController3D (right_hand, grip)
└── Floor (StaticBody3D)
    └── CollisionShape3D (BoxShape3D)

У Floor задайте BoxShape3D.Size=(20, 1, 20), позицию тела (0, -0.5, 0). Верх коллайдера окажется на Y=0. Видимый пол создаётся отдельно: MeshInstance3D не заменяет физический коллайдер. У игрока и препятствий должны пересекаться collision layer/mask. Не добавляйте второй скрипт, двигающий Player или origin.

Настройте действия

В рабочей карте действий сохраните позы контроллеров и добавьте два действия Vector2. Одного select из минимального примера недостаточно. Конкретный пример профиля: /interaction_profiles/oculus/touch_controller.

ДействиеТипToplevelPathsBindingPath
gripPose/user/hand/left, /user/hand/rightОтдельные привязки /user/hand/left/input/grip/pose и /user/hand/right/input/grip/pose
move_playerVector2/user/hand/left/user/hand/left/input/thumbstick
turn_playerVector2/user/hand/right/user/hand/right/input/thumbstick

Это пример для профиля со стиками, а не обещание поддержки любого устройства. Для другого профиля используйте допустимые ему пути. Minimal Simple Controller не предоставляет стики. Сохраните карту до инициализации сеанса; изменение ресурса во время работы не перестраивает runtime-действия. В этом примере положительный Y движения означает вперёд, положительный X поворота — вправо.

У камеры оставьте Tracker=head. У контроллеров сначала задайте Tracker=left_hand/right_hand, затем Pose=grip. Не меняйте Pose динамически: в текущем API есть ограничение кэширования tracking.

Движение и поворот

Сохраните как XrWalk.cs. Это объект состояния: создаётся один экземпляр на игрока, а не каждый кадр. Step вызывается ровно один раз за физический тик.

C#
using System;
using Reckfell;

public sealed class XrWalk
{
    private bool turnLatched;

    // Call once per physics tick. Upright, unscaled rig; one instance per player.
    // true means physical head/body alignment is blocked, not successful tracking.
    public bool Step(CharacterBody3D body, XROrigin3D origin, Entity3D head,
        Vector2 move, float turn, bool allowInput, double delta)
    {
        if (!move.IsFinite() || !float.IsFinite(turn)
            || !double.IsFinite(delta) || delta <= 0 || delta > 1)
            throw new ArgumentOutOfRangeException(nameof(delta), "Invalid input or physics delta.");
        if (!allowInput)
        {
            body.Velocity = Vector3.Zero;
            turnLatched = true; // Require neutral before the next snap after focus loss.
            return false;
        }
        if (RoomScaleBody.FollowHead(body, origin, head))
        {
            body.Velocity = Vector3.Zero;
            turnLatched = true;
            return true;
        }
        if (Mathf.Abs(turn) <= 0.25f) turnLatched = false;
        if (!turnLatched && Mathf.Abs(turn) >= 0.75f)
        {
            RoomScaleBody.TurnAroundHead(body, origin, head,
                -Mathf.Sign(turn) * Mathf.Pi / 6);
            turnLatched = true;
        }
        if (move.Length() <= 0.2f) move = Vector2.Zero;
        if (move.LengthSquared() > 1) move = move.Normalized();
        Vector3 forward = -head.GlobalBasis.Z;
        forward.Y = 0;
        if (forward.LengthSquared() < 0.000001f) forward = -body.GlobalBasis.Z;
        forward = forward.Normalized();
        Vector3 horizontal = (forward.Cross(Vector3.Up) * move.X + forward * move.Y) * 2;
        Vector3 velocity = body.Velocity;
        velocity.X = horizontal.X;
        velocity.Z = horizontal.Z;
        if (body.IsOnFloor() && velocity.Y < 0) velocity.Y = 0;
        else velocity.Y -= 9.8f * (float)delta;
        body.Velocity = velocity; // Units/second: MoveAndSlide applies its own frame delta.
        body.MoveAndSlide(); // Carries the origin with the body; do not compensate it here.
        return false;
    }
}

Скорость — 2 единицы/с; гравитация — 9.8 единицы/с². Velocity не умножается на delta: MoveAndSlide применяет физический шаг сам. Мёртвая зона движения — 0.2 без дополнительного растягивания диапазона. Диагональ ограничена общей скоростью. Направление берётся из горизонтальной проекции взгляда; при взгляде строго вверх/вниз используется направление тела.

Поворот — 30° при отклонении стика от 0.75. Для повторного шага верните стик до 0.25; удержание не повторяет поворот. После потери допуска к вводу также требуется нейтраль. Это параметры примера, не универсальная настройка комфорта.

Подключите к игроку

Сохраните как XrPlayer.cs и прикрепите к Player. Назначьте все пять ссылок: Origin, Head, MoveController=левый контроллер, TurnController=правый, Startup. При неполной структуре скрипт выводит понятную ошибку и отключает обработку.

C#
using Reckfell;

public partial class XrPlayer : CharacterBody3D
{
    [Export] public XROrigin3D Origin { get; set; } = null!;
    [Export] public XRCamera3D Head { get; set; } = null!;
    [Export] public XRController3D MoveController { get; set; } = null!;
    [Export] public XRController3D TurnController { get; set; } = null!;
    [Export] public XrStartup Startup { get; set; } = null!;
    public bool PhysicalGap { get; private set; }
    private readonly XrWalk walk = new();

    public override void _Ready()
    {
        if (Origin is null || Head is null || MoveController is null
            || TurnController is null || Startup is null || !IsAncestorOf(Origin)
            || Head.GetParent() != Origin || MoveController.GetParent() != Origin
            || TurnController.GetParent() != Origin)
        {
            ReckfellApi.PushError("XrPlayer: assign the startup and complete body/origin rig.");
            SetPhysicsProcess(false);
        }
    }

    public override void _PhysicsProcess(double delta)
    {
        var tracker = XRServer.GetTracker(Head.Tracker) as XRPositionalTracker;
        var pose = tracker?.GetPose("default"); // Borrowed state: do not Dispose.
        var movePose = MoveController.GetPose();
        var turnPose = TurnController.GetPose();
        bool active = Startup.HasFocus && pose is not null && pose.HasTrackingData
            && movePose is not null && movePose.HasTrackingData
            && turnPose is not null && turnPose.HasTrackingData;
        PhysicalGap = walk.Step(this, Origin, Head,
            active ? MoveController.GetVector2("move_player") : Vector2.Zero,
            active ? TurnController.GetVector2("turn_player").X : 0, active, delta);
    }
}

Допуск требует фокуса и актуальных pose головы и обоих контроллеров. Скрипт читает HasTrackingData самой pose; общие объекты не освобождает. Без допуска пример обнуляет скорость и останавливает всю свою локомоцию, включая гравитацию. Он не останавливает tracking камеры и не меняет паузу всего проекта. Для движущихся платформ и других правил паузы нужна отдельная политика.

RoomScaleBody сначала подводит коллайдер под физическую голову, сохраняя мировой origin. При игровом MoveAndSlide origin, наоборот, движется вместе с телом. Не компенсируйте его второй раз. PhysicalGap=true означает, что голова и тело остались разнесены из-за препятствия; виртуальное движение блокируется. Физическую голову это не останавливает: затемнение и запрет взаимодействий при проникновении — отдельная игровая логика, которой в примере нет.

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

СитуацияОжидаемый результат / диагностика
Левый стик вперёдДвижение по полу в направлении взгляда; по диагонали скорость не растёт
Правый стик вправо и удержаниеОдин поворот на 30°; следующий только после нейтрали
Виртуальное движение в стенуCapsuleShape3D останавливает тело; камера движется вместе с телом
Физический шаг за стенуВозможен PhysicalGap; tracking головы продолжается, это не физическая защита
Нет движенияПроверьте Startup.FailureReason/HasFocus, pose обеих рук, имена действий и профиль; нулевой GetVector2 сам по себе не подтверждает подключение
ПроваливаниеПроверьте Shape, позицию пола, layer/mask и вертикальность рига
Двойное смещение / поворотУберите второго владельца движения; не умножайте Velocity на delta и не двигайте Head вручную

На установленном движке выполнены 16 проверок XrWalk с настоящими коллайдерами пола/стены, искусственной головой и вводом. XrPlayer и XrStartup скомпилированы вместе; полный callback игрока с реальной XR-сессией не выполнялся. Привязки устройства, восстановление сеанса, комфорт, склоны и движущиеся платформы не подтверждены. Телепортации, прыжка и вращательного sweep произвольной формы здесь нет; риг рассчитан на вертикальную симметричную капсулу.

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

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

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

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