ここでは、「移動」の処理について、XRIT の DynamicMoveProvider と Rigidbody 版の RigidbodyMoveProvider を比較しつつ、Rigidbody 版の詳細を確認していく。
概要
| 項目 | DynamicMoveProvider | RigidbodyMoveProvider |
|---|---|---|
| 基底クラス | ContinuousMoveProvider → LocomotionProvider | MonoBehaviour |
| 物理コンポーネント | CharacterController | Rigidbody + CapsuleCollider |
| 移動適用 | Update() 内で XROriginMovement (Transform 操作) | FixedUpdate() 内で rb.linearVelocity 操作 |
| 重力管理 | GravityProvider または CharacterController | Rigidbody の物理演算に完全委託 |
| XRIT Locomotion 統合 | あり(LocomotionMediator 経由) | なし |
アーキテクチャの違い
DynamicMoveProvider(CharacterController ベース)
DynamicMoveProvider
└─ ContinuousMoveProvider (XRIT)
└─ LocomotionProvider (XRIT)
└─ MonoBehaviour
フロー:
Update()
├─ DynamicMoveProvider.ComputeDesiredMove()
│ └─ m_CombinedTransform の向きをブレンドして設定
│ └─ base.ComputeDesiredMove() (ContinuousMoveProvider)
│ └─ 移動ベクトル(world space)を計算
└─ MoveRig(translationInWorldSpace)
└─ TryQueueTransformation(XROriginMovement)
└─ XRBodyTransformer が CharacterController.Move() を呼ぶ
LocomotionProvider を継承しているため、LocomotionMediator を通じた状態管理(Moving / Ended)と、他の Locomotion プロバイダ(Climb・Turn・Jump 等)との優先度管理が行われる。
RigidbodyMoveProvider(Rigidbody ベース)
RigidbodyMoveProvider
└─ MonoBehaviour
フロー:
Update()
└─ ComputeHorizontalVelocity(leftValue, rightValue)
└─ 水平速度ベクトルをキャッシュ(m_DesiredHorizontalVelocity)
FixedUpdate()
└─ rb.linearVelocity の XZ 成分を上書き
(Y 成分は Rigidbody の物理演算に委ねる)
LocomotionMediator を一切使用しない独立した実装。PhysicsHand02 シーンでは他の Locomotion プロバイダもすべて物理ベースに置き換えるため、XRIT との統合は不要。
方向計算の比較
両者とも「左右スティックの入力量に応じて、基準方向(頭 or 手)をブレンドする」という同じロジックを持つ。実装アプローチが異なる。
DynamicMoveProvider のアプローチ:中間 GameObject を使ったブレンド
// Awake() で空の GameObject を作成し、forwardSource として登録
m_CombinedTransform = new GameObject("[Dynamic Move Provider] Combined Forward Source").transform;
forwardSource = m_CombinedTransform;
// ComputeDesiredMove() でブレンドした回転を m_CombinedTransform に適用
var combinedRotation = Quaternion.Slerp(m_RightMovementPose.rotation, m_LeftMovementPose.rotation, leftHandBlend);
m_CombinedTransform.SetPositionAndRotation(combinedPosition, combinedRotation);
// あとは base.ComputeDesiredMove() に委ねる
return base.ComputeDesiredMove(input);ContinuousMoveProvider の forwardSource という既存のフックを利用し、ブレンド結果を Transform として渡すことで、基底クラスの移動計算をそのまま再利用している。
RigidbodyMoveProvider のアプローチ:速度ベクトルを直接計算
Vector3 ComputeHorizontalVelocity(Vector2 leftValue, Vector2 rightValue)
{
// 入力量の比率でブレンド係数を決定
var leftHandBlend = totalSqrMagnitude > Mathf.Epsilon ? leftSqr / totalSqrMagnitude : 0.5f;
// 左右の基準回転を Slerp でブレンド
var leftRot = GetSourceRotation(m_LeftHandMovementDirection, m_LeftControllerTransform);
var rightRot = GetSourceRotation(m_RightHandMovementDirection, m_RightControllerTransform);
var blendedForward = Quaternion.Slerp(rightRot, leftRot, leftHandBlend) * Vector3.forward;
// XROrigin の up 平面に投影して水平方向の forward を得る
var projectedFwd = Vector3.ProjectOnPlane(blendedForward, up);
var forwardRotation = Quaternion.FromToRotation(originTransform.forward, projectedFwd);
// 入力ベクトルに回転を掛けて速度を得る
var inputMove = Vector3.ClampMagnitude(new Vector3(strafe ? totalInput.x : 0f, 0f, totalInput.y), 1f);
return originTransform.TransformDirection(forwardRotation * inputMove) * m_MoveSpeed;
}GameObject を介さず、回転のブレンド→水平投影→速度計算を一気通貫で行う。
移動の適用方法の違い
このプロジェクトにおいて、両者の最も大きな差異のひとつがここにある。
CharacterController.Move()(DynamicMoveProvider)
// XROriginMovement.Apply() の内部(Unity XRIT パッケージ内)
characterController.Move(motion);
// または
origin.transform.position += motion;Update()タイミングで呼ぶ(物理エンジンのサイクルと無関係)CharacterController自体が衝突判定・スライディングを処理するRigidbodyは不要(CharacterController と Rigidbody は通常共存させない)isGroundedプロパティで接地判定可能
Rigidbody.linearVelocity の XZ 上書き(RigidbodyMoveProvider)
void FixedUpdate()
{
var v = m_Rigidbody.linearVelocity;
v.x = m_DesiredHorizontalVelocity.x;
v.z = m_DesiredHorizontalVelocity.z;
// v.y は上書きしない → 重力・ジャンプの Y 速度を保持
m_Rigidbody.linearVelocity = v;
}FixedUpdate()タイミングで適用(物理エンジンと同期)- Rigidbody + CapsuleCollider が衝突・スライディングを処理
- Y 速度を保持することで重力とジャンプを物理エンジンに委ねられる
Update()とFixedUpdate()が別タイミングのため、m_DesiredHorizontalVelocityを介してバッファリングが必要
重力の扱いの違い
DynamicMoveProvider(GravityProvider 連携)
GravityProvider(独立コンポーネント)
├─ isGrounded の監視
├─ m_GravityDrivenVelocity の蓄積
└─ CharacterController.Move() に重力分を含めて渡す
OnLocomotionStateChanging()でGravityProviderに重力のロック/アンロックを通知- 空中では
inAirControlModifierで水平入力をスムージング(急な方向転換を抑制) enableFlyフラグで重力を無効化できる
RigidbodyMoveProvider(物理エンジン委託)
Rigidbody
└─ useGravity = true(Inspector で設定)
└─ 物理エンジンが Y 方向に重力加速度を自動適用
- 重力の計算コードが一切不要
- ただし衝突時の挙動(バウンド・摩擦)は Rigidbody の Physics Material に依存
- 物理的に正確だが、ゲーム的な調整(空中制御の減衰など)は自前実装が必要
XR Origin スケール対応
DynamicMoveProvider(対応あり)
// ContinuousMoveProvider.ComputeDesiredMove() 内
var speedFactor = m_MoveSpeed * deltaTime * originTransform.localScale.x;ユーザーのスケール(身長補正など)に応じて移動速度が自動調整される。
RigidbodyMoveProvider(対応なし)
return originTransform.TransformDirection(velocityInRigSpace) * m_MoveSpeed;
// localScale の補正なしXR Origin のスケールを変更した場合、速度が体感的にずれる可能性がある。
LocomotionMediator との関係
DynamicMoveProvider(統合あり)
LocomotionProvider継承によりLocomotionState(Idle / Starting / Moving / Ended)を管理TryQueueTransformation()で他プロバイダと排他制御(例:テレポート中は移動しない)locomotionStarted/locomotionEndedイベントで UI(Vignette など)と連携
RigidbodyMoveProvider(統合なし)
LocomotionMediatorを一切使わない- 他プロバイダとの排他制御は行わない
- PhysicsHand02 シーンでは全 Locomotion を物理ベースで統一するため、これで問題なし
シナリオ別の選択目安
以下はあくまで私のプロジェクトでの判断基準であり、要件によって異なる選択が適切になる場合もある。
| シナリオ | このプロジェクトでの判断 |
|---|---|
| 標準的な VR 移動(XRIT 標準構成) | DynamicMoveProvider が適している |
| Rigidbody による物理インタラクションが必要 | RigidbodyMoveProvider が適している |
| 物理ハンドと環境の押し合いが必要 | RigidbodyMoveProvider が適している |
| Climb / Grab との物理的な統合が必要 | RigidbodyMoveProvider が適している |
| テレポートなど XRIT 標準 Locomotion との共存 | DynamicMoveProvider が適している |
参考ファイル
Assets/Samples/XR Interaction Toolkit/3.3.0/Starter Assets/Scripts/DynamicMoveProvider.csAssets/SimplePhysicsLocomotion/Scripts/RigidbodyMoveProvider.csLibrary/PackageCache/com.unity.xr.interaction.toolkit@.../Runtime/Locomotion/Movement/ContinuousMoveProvider.cs