概要
このドキュメントでは、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: 入力読み取りと方向決定
-
ContinuousMoveProvider.Update()
ReadInput()で左右のスティック入力を読み取り(Vector2同士を加算)
-
DynamicMoveProvider.ComputeDesiredMove(input)
- 左手の設定に応じて
m_LeftMovementPoseを決定HeadRelative→m_HeadTransform.GetWorldPose()HandRelative→m_LeftControllerTransform.GetWorldPose()
- 右手も同様に
m_RightMovementPoseを決定 - 両手の入力量(sqrMagnitude)に応じてブレンド率を計算
m_CombinedTransformにブレンドした位置・回転を設定- 親クラス
ContinuousMoveProvider.ComputeDesiredMove()を呼び出し
- 左手の設定に応じて
フェーズ2: 移動ベクトル計算
- 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空間に変換
- 移動速度とデルタタイムを乗算
- スティック入力を3Dベクトルに変換:
フェーズ3: Locomotion状態管理
- MoveRig(translationInWorldSpace)
- CharacterControllerの存在確認(
FindCharacterController()) - レガシー重力の追加(GravityProviderがない場合)
TryStartLocomotionImmediately()を呼び出し →LocomotionMediator.TryStartLocomotion()が呼ばれる →LocomotionProvider.OnLocomotionStateChanging(Moving)が呼ばれる →m_ActiveBodyTransformerに参照が設定される
- CharacterControllerの存在確認(
フェーズ4: 変換のキューイング
- TryQueueTransformation(transformation)
locomotionState == MovingをチェックXRBodyTransformer.QueueTransformation(transformation, priority)を呼び出し- LinkedListの適切な位置(優先度順)に挿入
フェーズ5: 変換の適用
- XRBodyTransformer.Update() (同フレームまたは次フレーム)
beforeApplyTransformationsイベント発火- キューから順に
transformation.Apply(m_MovableBody)を呼び出し - XROriginMovement.Apply() 内部で:
- CharacterControllerがある場合:
characterController.Move(motion) - ない場合:
originTransform.position += motion
- CharacterControllerがある場合:
afterApplyTransformationsイベント発火- キューをクリア
フェーズ6: 状態の終了
-
ContinuousMoveProvider.Update() (入力がゼロの場合)
m_IsMovingXROrigin == falseの場合、TryEndLocomotion()を呼び出しLocomotionMediatorが状態をEndedに変更locomotionEndedイベント発火
-
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/locomotionEndedbeforeStepLocomotion/afterStepLocomotionbeforeApplyTransformations/afterApplyTransformationslocomotionStateChanged
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の移動システムの主な特徴:
- 階層的なアーキテクチャ: Provider → Mediator → Transformer → Body の明確な責任分離
- 状態管理:
LocomotionStateによる状態遷移 - 優先度付きキュー: 複数の移動ソースを統合可能
- 拡張性: インターフェースベースの設計により、カスタマイズしやすい構造
- CharacterController統合: 衝突検出と移動を担当
- 身体位置の抽象化: 様々なVR体験に対応する設計
DynamicMoveProviderは、このシステムの上に構築された、「移動の基準方向を動的に選択する」というシンプルな責務を持つクラスで、実際の移動処理は下層のシステムに委譲されている。この設計により、メンテナンス性と拡張性を両立している(ただし、Rigidbody との統合には別途対応が必要になる)。
参考ファイル
Assets/Samples/XR Interaction Toolkit/3.3.0/Starter Assets/Scripts/DynamicMoveProvider.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/Movement/ContinuousMoveProvider.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionProvider.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/LocomotionMediator.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRBodyTransformer.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@5f736ad4ccd8/Runtime/Locomotion/XRMovableBody.cs