概要

このドキュメントでは、Unity XR Interaction Toolkit (3.3.0) における移動(Locomotion)システム、特にDynamicMoveProvider がどのようにVR空間での移動を実現しているかをみていく。

全体アーキテクチャ

XR Interaction Toolkit 3.3.0の移動システムは、以下の階層構造を持つ:

LocomotionMediator (仲介者)
  └─ XRBodyTransformer (変換適用者)
      └─ XRMovableBody (移動可能な身体の抽象化)
          ├─ XROrigin (実際のVR空間)
          ├─ IXRBodyPositionEvaluator (身体位置の評価)
          └─ IConstrainedXRBodyManipulator (CharacterController等)

LocomotionProvider (抽象基底クラス)
  └─ ContinuousMoveProvider (連続移動)
      └─ DynamicMoveProvider (動的方向切替)

各クラスの役割と実装詳細

1. DynamicMoveProvider

ファイル: Assets/Samples/XR Interaction Toolkit/3.3.0/Starter Assets/Scripts/DynamicMoveProvider.cs

役割: 左右のコントローラーごとに、頭基準(HeadRelative) or コントローラー基準(HandRelative)の移動方向を動的に切り替える

主要な処理:

protected override Vector3 ComputeDesiredMove(Vector2 input)
{
    // 左手の入力に対する基準Transformを決定
    switch (m_LeftHandMovementDirection)
    {
        case MovementDirection.HeadRelative:
            m_LeftMovementPose = m_HeadTransform.GetWorldPose();
            break;
        case MovementDirection.HandRelative:
            m_LeftMovementPose = m_LeftControllerTransform.GetWorldPose();
            break;
    }
    
    // 右手も同様に処理
    switch (m_RightHandMovementDirection)
    {
        case MovementDirection.HeadRelative:
            m_RightMovementPose = m_HeadTransform.GetWorldPose();
            break;
        case MovementDirection.HandRelative:
            m_RightMovementPose = m_RightControllerTransform.GetWorldPose();
            break;
    }
    
    // 両手の入力の大きさに応じて基準Poseをブレンド
    var leftHandValue = leftHandMoveInput.ReadValue();
    var rightHandValue = rightHandMoveInput.ReadValue();
    
    var totalSqrMagnitude = leftHandValue.sqrMagnitude + rightHandValue.sqrMagnitude;
    var leftHandBlend = leftHandValue.sqrMagnitude / totalSqrMagnitude;
    
    var combinedPosition = Vector3.Lerp(m_RightMovementPose.position, m_LeftMovementPose.position, leftHandBlend);
    var combinedRotation = Quaternion.Slerp(m_RightMovementPose.rotation, m_LeftMovementPose.rotation, leftHandBlend);
    
    // 結合したTransformを基準として親クラスに渡す
    m_CombinedTransform.SetPositionAndRotation(combinedPosition, combinedRotation);
    return base.ComputeDesiredMove(input);
}

重要ポイント:

  • 左手と右手で異なる移動方向基準を設定可能(例: 左手は頭基準、右手はコントローラー基準)
  • 両手の入力量に応じて基準をブレンドするため、両方のスティックを同時に倒した時にスムーズに合成される
  • m_CombinedTransformという中間TransformをforwardSourceとして親クラスに提供

2. ContinuousMoveProvider

ファイル: Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/Movement/ContinuousMoveProvider.cs

役割: スティック入力から実際の移動ベクトルを計算し、移動を実行

主要な処理フロー:

2.1 Update() - メインループ

protected void Update()
{
    m_IsMovingXROrigin = false;
    
    var xrOrigin = mediator.xrOrigin?.Origin;
    if (xrOrigin == null)
        return;
    
    var input = ReadInput(); // 左右のスティック入力を読み取り
    var translationInWorldSpace = ComputeDesiredMove(input);
    
    if (input != Vector2.zero || m_GravityDrivenVelocity != Vector3.zero || m_InAirVelocity != Vector3.zero)
        MoveRig(translationInWorldSpace);
    
    if (!m_IsMovingXROrigin)
        TryEndLocomotion();
}

2.2 ComputeDesiredMove() - 移動ベクトル計算

protected virtual Vector3 ComputeDesiredMove(Vector2 input)
{
    // スティック入力を3D空間の移動ベクトルに変換
    // X: ストレイフ(横移動)、Z: 前後移動
    var inputMove = Vector3.ClampMagnitude(
        new Vector3(m_EnableStrafe ? input.x : 0f, 0f, input.y), 
        1f
    );
    
    var deltaTime = Time.deltaTime;
    
    // 空中制御: 地上にいない場合はスムージングを適用
    if (m_EnableFly || m_InAirControlModifier >= 1f || isGrounded)
    {
        m_InAirVelocity = inputMove;
    }
    else
    {
        // 空中では入力に対する反応を減衰させる
        m_InAirVelocity += deltaTime * m_InAirControlModifier * 10 * (inputMove - m_InAirVelocity);
        
        // 小さくなったらゼロにスナップ(快適性vignette対策)
        if (m_InAirVelocity.sqrMagnitude <= 1e-4f)
            m_InAirVelocity = Vector3.zero;
    }
    
    // forwardSourceの方向を基準に移動ベクトルを計算
    var forwardSourceTransform = m_ForwardSource ?? xrOrigin.Camera.transform;
    var inputForwardInWorldSpace = forwardSourceTransform.forward;
    
    var originTransform = xrOrigin.Origin.transform;
    var speedFactor = m_MoveSpeed * deltaTime * originTransform.localScale.x; // ユーザースケールで調整
    
    // フライモードの場合は直接3D移動
    if (m_EnableFly)
    {
        var inputRightInWorldSpace = forwardSourceTransform.right;
        var combinedMove = inputMove.x * inputRightInWorldSpace + inputMove.z * inputForwardInWorldSpace;
        return combinedMove * speedFactor;
    }
    
    var originUp = originTransform.up;
    
    // forwardSourceがXR Originの上方向と平行な場合の特殊処理
    if (Mathf.Approximately(Mathf.Abs(Vector3.Dot(inputForwardInWorldSpace, originUp)), 1f))
    {
        inputForwardInWorldSpace = -forwardSourceTransform.up;
    }
    
    // 水平面に投影して、XR Originの上方向に対して垂直な移動を実現
    var inputForwardProjectedInWorldSpace = Vector3.ProjectOnPlane(inputForwardInWorldSpace, originUp);
    var forwardRotation = Quaternion.FromToRotation(originTransform.forward, inputForwardProjectedInWorldSpace);
    
    var translationInRigSpace = forwardRotation * m_InAirVelocity * speedFactor;
    var translationInWorldSpace = originTransform.TransformDirection(translationInRigSpace);
    
    return translationInWorldSpace;
}

2.3 MoveRig() - 移動実行

protected virtual void MoveRig(Vector3 translationInWorldSpace)
{
    var xrOrigin = mediator.xrOrigin?.Origin;
    if (xrOrigin == null)
        return;
    
    FindCharacterController();
    
    var motion = translationInWorldSpace;
    
    // 重力速度の追加(GravityProviderがない場合のレガシー処理)
    if (m_GravityProvider == null && m_CharacterController != null && m_CharacterController.enabled)
    {
        if (m_CharacterController.isGrounded || !m_UseGravity || m_EnableFly)
            m_GravityDrivenVelocity = Vector3.zero;
        else
            m_GravityDrivenVelocity += Physics.gravity * Time.deltaTime;
        
        motion += m_GravityDrivenVelocity * Time.deltaTime;
    }
    
    // Locomotion状態を開始
    TryStartLocomotionImmediately();
    if (locomotionState != LocomotionState.Moving)
        return;
    
    // XROriginMovement transformationを設定
    m_IsMovingXROrigin = true;
    transformation.motion = motion;
    
    // XRBodyTransformerに変換をキューイング
    TryQueueTransformation(transformation);
}

重要ポイント:

  • m_InAirVelocity: 空中での入力スムージングにより、ジャンプ中の急な方向転換を防止
  • Vector3.ProjectOnPlane: 頭やコントローラーの上下の向きに関わらず、水平面での移動を保証
  • m_MoveSpeed * localScale.x: ユーザーのスケールに応じて移動速度を調整

3. LocomotionProvider

ファイル: Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionProvider.cs

役割: 全ての移動Providerの基底クラス。LocomotionMediatorとの通信を担当

状態遷移:

Idle (待機)
  ↓ TryPrepareLocomotion() or TryStartLocomotionImmediately()
Preparing (準備中)
  ↓ canStartMoving == true (Mediator.Update()でチェック)
Moving (移動中)  ← この状態のみXRBodyTransformerにアクセス可能
  ↓ TryEndLocomotion()
Ended (終了)
  ↓ 次フレームのMediator.Update()
Idle

重要メソッド:

TryQueueTransformation()

protected bool TryQueueTransformation(IXRBodyTransformation bodyTransformation)
{
    if (!CanQueueTransformation())
        return false;
    
    m_ActiveBodyTransformer.QueueTransformation(bodyTransformation, m_TransformationPriority);
    m_AnyTransformationsQueued = true;
    return true;
}

OnLocomotionStateChanging()

internal void OnLocomotionStateChanging(LocomotionState oldState, LocomotionState state, XRBodyTransformer transformer)
{
    if (state == LocomotionState.Moving)
    {
        m_ActiveBodyTransformer = transformer;
        Subscribe(transformer);
        
        OnLocomotionStateChanging(state);
        locomotionStateChanged?.Invoke(this, state);
        
        OnLocomotionStarting();
        locomotionStarted?.Invoke(this);
    }
    else if (state == LocomotionState.Ended)
    {
        m_ActiveBodyTransformer = null;
        
        if (!m_AnyTransformationsQueued)
        {
            Unsubscribe();
            OnLocomotionEnding();
            locomotionEnded?.Invoke(this);
        }
    }
}

重要ポイント:

  • locomotionState == Moving の時のみ TryQueueTransformation() が成功
  • イベント: locomotionStarted, locomotionEnded, beforeStepLocomotion, afterStepLocomotion
  • m_TransformationPriority: 複数のProviderが同時に動作する場合の優先度

4. LocomotionMediator

ファイル: Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionMediator.cs

役割: 複数のLocomotionProviderの状態を管理し、XRBodyTransformerへのアクセスを仲介

主要処理:

protected void Update()
{
    s_ProvidersToRemove.Clear();
    foreach (var kvp in m_ProviderDataMap)
    {
        var provider = kvp.Key;
        if (provider == null)
        {
            s_ProvidersToRemove.Add(provider);
            continue;
        }
        
        var providerData = kvp.Value;
        
        // Preparing状態でcanStartMovingがtrueなら→Moving状態へ
        if (providerData.state == LocomotionState.Preparing && provider.canStartMoving)
        {
            ChangeState(provider, providerData, LocomotionState.Moving);
        }
        // Ended状態で1フレーム経過したら→Idle状態へ
        else if (providerData.state == LocomotionState.Ended && Time.frameCount > providerData.locomotionEndFrame)
        {
            ChangeState(provider, providerData, LocomotionState.Idle);
        }
    }
    
    // 破棄されたProviderをクリーンアップ
    if (s_ProvidersToRemove.Count > 0)
    {
        foreach (var provider in s_ProvidersToRemove)
            m_ProviderDataMap.Remove(provider);
    }
}

重要ポイント:

  • Dictionary<LocomotionProvider, LocomotionProviderData> で各Providerの状態を管理
  • Ended状態は1フレーム遅延してからIdleに戻る(キューに残った変換の適用を保証)
  • 複数のProviderが同時にアクティブになることを許可(優先度でソート)

5. XRBodyTransformer

ファイル: Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRBodyTransformer.cs

役割: キューに積まれた変換(IXRBodyTransformation)を優先度順に適用

主要処理:

protected virtual void Update()
{
    if (m_TransformationsQueue.Count == 0)
        return;
    
    beforeApplyTransformations?.Invoke(this);
    
    // 優先度順にソートされたキューから変換を取り出して適用
    while (m_TransformationsQueue.Count > 0)
    {
        m_TransformationsQueue.First.Value.transformation.Apply(m_MovableBody);
        m_TransformationsQueue.RemoveFirst();
    }
    
    m_ApplyTransformationsEventArgs.bodyTransformer = this;
    afterApplyTransformations?.Invoke(m_ApplyTransformationsEventArgs);
}

QueueTransformation():

public void QueueTransformation(IXRBodyTransformation transformation, int priority = 0)
{
    var orderedTransformation = new OrderedTransformation
    {
        transformation = transformation,
        priority = priority,
    };
    
    // priorityの昇順でLinkedListに挿入
    var node = m_TransformationsQueue.First;
    if (node == null || node.Value.priority > priority)
    {
        m_TransformationsQueue.AddFirst(orderedTransformation);
        return;
    }
    
    while (node.Next != null && node.Next.Value.priority <= priority)
        node = node.Next;
    
    m_TransformationsQueue.AddAfter(node, orderedTransformation);
}

重要ポイント:

  • LinkedList<OrderedTransformation> による優先度付きキュー
  • 毎フレーム、全ての変換を適用してキューをクリア
  • beforeApplyTransformations / afterApplyTransformations イベントで変換前後の処理を追加可能

6. XRMovableBody

ファイル: Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRMovableBody.cs

役割: XROriginと身体位置評価、衝突制約を統合したコンテナ

構成要素:

public class XRMovableBody
{
    /// <summary>
    /// The XR Origin whose Origin is transformed to move the body.
    /// </summary>
    public XROrigin xrOrigin { get; private set; }
    
    /// <summary>
    /// The Transform component of the XROrigin.Origin.
    /// </summary>
    public Transform originTransform => xrOrigin.Origin.transform;
    
    /// <summary>
    /// The object that determines the position of the user's body.
    /// </summary>
    public IXRBodyPositionEvaluator bodyPositionEvaluator { get; private set; }
    
    /// <summary>
    /// Object that can be used to perform movement constrained by collision (optional).
    /// </summary>
    public IConstrainedXRBodyManipulator constrainedManipulator { get; private set; }
}

重要メソッド:

// ユーザーの足元位置(ローカル座標)
public Vector3 GetBodyGroundLocalPosition()
{
    return bodyPositionEvaluator.GetBodyGroundLocalPosition(xrOrigin);
}
 
// ユーザーの足元位置(ワールド座標)
public Vector3 GetBodyGroundWorldPosition()
{
    return bodyPositionEvaluator.GetBodyGroundWorldPosition(xrOrigin);
}

重要ポイント:

  • IXRBodyPositionEvaluator: デフォルトはUnderCameraBodyPositionEvaluator(カメラの真下を足元とする)
  • IConstrainedXRBodyManipulator: デフォルトはCharacterControllerBodyManipulator(CharacterControllerがあれば自動設定)
  • これらのインターフェースにより、身体位置の定義や衝突処理をカスタマイズ可能

移動実現の完全フロー

以下に、1フレームでの移動処理の完全な流れを示す:

フェーズ1: 入力読み取りと方向決定

  1. ContinuousMoveProvider.Update()

    • ReadInput() で左右のスティック入力を読み取り(Vector2同士を加算)
  2. DynamicMoveProvider.ComputeDesiredMove(input)

    • 左手の設定に応じて m_LeftMovementPose を決定
      • HeadRelativem_HeadTransform.GetWorldPose()
      • HandRelativem_LeftControllerTransform.GetWorldPose()
    • 右手も同様に m_RightMovementPose を決定
    • 両手の入力量(sqrMagnitude)に応じてブレンド率を計算
    • m_CombinedTransform にブレンドした位置・回転を設定
    • 親クラス ContinuousMoveProvider.ComputeDesiredMove() を呼び出し

フェーズ2: 移動ベクトル計算

  1. ContinuousMoveProvider.ComputeDesiredMove(input) (親クラス)
    • スティック入力を3Dベクトルに変換: Vector3(strafe ? input.x : 0, 0, input.y)
    • 空中制御の計算:
      • 地上にいる場合: m_InAirVelocity = inputMove
      • 空中の場合: m_InAirVelocityをスムージング(m_InAirControlModifierで減衰)
    • forwardSource(=m_CombinedTransform)の forward 方向を取得
    • forward方向をXR Originの水平面に投影
    • Rig空間での移動ベクトルを計算し、World空間に変換
    • 移動速度とデルタタイムを乗算

フェーズ3: Locomotion状態管理

  1. MoveRig(translationInWorldSpace)
    • CharacterControllerの存在確認(FindCharacterController()
    • レガシー重力の追加(GravityProviderがない場合)
    • TryStartLocomotionImmediately() を呼び出し → LocomotionMediator.TryStartLocomotion() が呼ばれる → LocomotionProvider.OnLocomotionStateChanging(Moving) が呼ばれる → m_ActiveBodyTransformerに参照が設定される

フェーズ4: 変換のキューイング

  1. TryQueueTransformation(transformation)
    • locomotionState == Moving をチェック
    • XRBodyTransformer.QueueTransformation(transformation, priority) を呼び出し
    • LinkedListの適切な位置(優先度順)に挿入

フェーズ5: 変換の適用

  1. XRBodyTransformer.Update() (同フレームまたは次フレーム)
    • beforeApplyTransformations イベント発火
    • キューから順に transformation.Apply(m_MovableBody) を呼び出し
    • XROriginMovement.Apply() 内部で:
      • CharacterControllerがある場合: characterController.Move(motion)
      • ない場合: originTransform.position += motion
    • afterApplyTransformations イベント発火
    • キューをクリア

フェーズ6: 状態の終了

  1. ContinuousMoveProvider.Update() (入力がゼロの場合)

    • m_IsMovingXROrigin == false の場合、TryEndLocomotion() を呼び出し
    • LocomotionMediator が状態を Ended に変更
    • locomotionEnded イベント発火
  2. LocomotionMediator.Update() (次フレーム)

    • Ended 状態で1フレーム経過していたら Idle に戻す

設計の注目すべき点

1. 関心の分離 (Separation of Concerns)

  • 入力処理: DynamicMoveProvider - どの方向を「前」とするか
  • 移動計算: ContinuousMoveProvider - 入力から移動ベクトルへの変換
  • 状態管理: LocomotionMediator - 複数Providerの調停
  • 実際の変換適用: XRBodyTransformer - XR Originの実際の移動

各クラスが明確な単一責任を持ち、疎結合を維持している。

2. 拡張性 (Extensibility)

  • IXRBodyTransformation を実装すれば任意の変換を追加可能
    • 例: テレポート、ダッシュ、壁登り、重力反転など
  • LocomotionProvider を継承して新しい移動方式を追加可能
    • 例: ナビメッシュベースの移動、グラップリングフック、飛行など
  • IXRBodyPositionEvaluator で身体位置の定義をカスタマイズ
    • 例: 両足の中点、重心位置、手の位置など
  • IConstrainedXRBodyManipulator で衝突処理をカスタマイズ
    • 例: 物理Rigidbody、カスタムレイキャスト、ナビメッシュなど

3. 優先度システム (Priority System)

  • 複数の移動Providerが同時に動作しても、transformationPriorityで適用順を制御
  • 例: 重力(priority=100) → プレイヤー移動(priority=0) → 外力(priority=-10)

4. CharacterController統合

  • CharacterControllerBodyManipulatorにより、以下が自動的に機能:
    • 衝突検出とスライディング
    • 坂の登り降り
    • 段差の乗り越え(stepOffset)
    • skinWidth による壁からの距離維持
  • CharacterControllerがない場合は単純なtransform.positionの移動にフォールバック

5. 身体位置の抽象化

  • IXRBodyPositionEvaluatorにより、「ユーザーの足元はどこか」の定義を柔軟に変更可能
  • デフォルトのUnderCameraBodyPositionEvaluatorはカメラのXZ平面投影を使用
  • これにより、しゃがみ、ジャンプ、座位VRなど様々なシチュエーションに対応

6. イベント駆動アーキテクチャ

以下のイベントにより、カスタムロジックを簡単に追加可能:

  • locomotionStarted / locomotionEnded
  • beforeStepLocomotion / afterStepLocomotion
  • beforeApplyTransformations / afterApplyTransformations
  • locomotionStateChanged

7. 空中制御のスムージング

m_InAirVelocityのスムージングにより、以下を実現:

  • ジャンプ中の急な方向転換を防止(m_InAirControlModifierで制御)
  • 入力が止まった時のvignette表示の長期化を防ぐ(小さくなったらゼロにスナップ)
  • リアルな物理感覚とゲームプレイのバランス

カスタマイズのポイント

DynamicMoveProviderをベースにした拡張例

public class CustomMoveProvider : DynamicMoveProvider
{
    [SerializeField] float m_SprintMultiplier = 2f;
    [SerializeField] XRInputValueReader<float> m_SprintInput;
    
    protected override Vector3 ComputeDesiredMove(Vector2 input)
    {
        var baseMove = base.ComputeDesiredMove(input);
        
        // スプリント入力でスピードアップ
        var sprintValue = m_SprintInput.ReadValue();
        if (sprintValue > 0.5f)
            return baseMove * m_SprintMultiplier;
        
        return baseMove;
    }
}

カスタム変換の実装例

public class DashTransformation : IXRBodyTransformation
{
    public Vector3 dashDirection;
    public float dashDistance;
    
    public void Apply(XRMovableBody body)
    {
        var motion = dashDirection * dashDistance;
        
        // CharacterControllerを使った衝突検出付き移動
        if (body.constrainedManipulator != null)
        {
            body.constrainedManipulator.MoveBody(motion);
        }
        else
        {
            // フォールバック: 直接Transform移動
            body.originTransform.position += motion;
        }
    }
}
 
// 使用例
public class DashProvider : LocomotionProvider
{
    DashTransformation m_DashTransformation = new DashTransformation();
    
    void Update()
    {
        if (Input.GetButtonDown("Dash"))
        {
            TryStartLocomotionImmediately();
            
            m_DashTransformation.dashDirection = transform.forward;
            m_DashTransformation.dashDistance = 5f;
            
            TryQueueTransformation(m_DashTransformation, priority: -10); // 高優先度
        }
    }
}

まとめ

XR Interaction Toolkit 3.3.0の移動システムの主な特徴:

  1. 階層的なアーキテクチャ: Provider → Mediator → Transformer → Body の明確な責任分離
  2. 状態管理: LocomotionStateによる状態遷移
  3. 優先度付きキュー: 複数の移動ソースを統合可能
  4. 拡張性: インターフェースベースの設計により、カスタマイズしやすい構造
  5. CharacterController統合: 衝突検出と移動を担当
  6. 身体位置の抽象化: 様々なVR体験に対応する設計

DynamicMoveProviderは、このシステムの上に構築された、「移動の基準方向を動的に選択する」というシンプルな責務を持つクラスで、実際の移動処理は下層のシステムに委譲されている。この設計により、メンテナンス性と拡張性を両立している(ただし、Rigidbody との統合には別途対応が必要になる)。

参考ファイル

  • Assets/Samples/XR Interaction Toolkit/3.3.0/Starter Assets/Scripts/DynamicMoveProvider.cs
  • Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/Movement/ContinuousMoveProvider.cs
  • Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionProvider.cs
  • Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionMediator.cs
  • Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRBodyTransformer.cs
  • Library/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRMovableBody.cs