【Unity】Rigidbody+CapsuleColliderを用いたプレイヤー移動

当サイトで紹介する商品・サービス等の外部リンクは、アフィリエイト広告を含む場合があります。
スポンサーリンク
本記事を読むと以下の実行ができます

Rigidbody+CapsuleColliderだけでカメラ相対移動・回転・落下リスポーンに対応した3Dプレイヤー移動処理を実装できます。

Unityでキャラクター移動を実装する際、CharacterControllerが使われます。他の物理オブジェクトと自然に衝突させたい、坂道や段差の挙動を物理演算に任せたいといった場面では、CharacterControllerだと限界を感じることがあります。

本記事では、Rigidbody+CapsuleColliderへの移動処理を、カメラ相対移動・回転処理・落下時のリスポーンまで含めて解説します。unity rigidbody 移動の実装で「Transformによるめり込み」や「カメラのカクつき」に悩んでいる人にも役立つ内容です。

本記事は次の人におすすめ
  • CharacterControllerからRigidbodyへ移動処理を移行したい人
  • unity rigidbody 移動の実装方法を知りたい人
  • CharacterControllerとRigidbodyの違い・使い分けを知りたい人
  • Rigidbody移動で「Transformによるめり込み」「カメラのカクつき」に困っている人
  • カメラの向きを基準にした移動(カメラ相対移動)を実装したい人
スポンサーリンク

CapsuleCollider

まず、プレイヤーの当たり判定に使うCapsuleCollider(カプセルコライダー)について説明します。

CapsuleColliderは、上下に半球が付いた円柱状の当たり判定です。人型キャラクターの当たり判定としてよく使われており、実はCharacterControllerも内部的にはカプセル形状の判定を使っています。

Blenderで作成した3Dモデルをキャラクターとして導入する手順を紹介しています。

Hierarchyでプレイヤーのオブジェクトを選択し、InspectorのAdd ComponentからPhysics > Capsule Colliderを追加すると導入できます。

項目説明
Radiusカプセルの半径。キャラクターの横幅方向の判定に影響
Heightカプセルの高さ
Directionカプセルの向き(通常はY軸方向)
Center判定の中心位置。オブジェクトの原点からのオフセット
Material摩擦・反発の設定。地面でスムーズに移動させたい場合は摩擦0に近いマテリアルを使うことがあります
注意

CapsuleColliderだけではキャラクターは物理演算の影響を受けません。次に説明するRigidbodyと組み合わせて初めて、重力や衝突による移動制御ができるようになります。

Rigidbody

Rigidbodyは、オブジェクトに物理演算(重力・衝突・力の伝達など)を適用するためのコンポーネントです。Add ComponentからPhysics > Rigidbodyを追加すると、そのオブジェクトはUnityの物理エンジンの管理下に置かれます。

今回紹介するPlayerMove.csのAwake()で設定している値を例に、代表的な設定項目を紹介します。

項目役割
useGravitytrue重力の影響を受けるようにする
interpolationRigidbodyInterpolation.Interpolate物理演算(FixedUpdate)と描画(Update)のタイミングのズレを補間し、カメラのカクつきを抑える
collisionDetectionModeCollisionDetectionMode.ContinuousDynamic高速移動時に薄い壁などをすり抜けないようにする連続衝突判定
constraintsFreezeRotationX | FreezeRotationZX軸・Z軸の回転を固定し、キャラクターが物理演算で倒れてしまわないようにする

Rigidbodyのメリット

  • 他のRigidbodyを持つオブジェクトと自然に衝突・押し合いができる
  • 坂道や段差の挙動を物理演算にまかせられるため、個別にコードで実装しなくて済む
  • velocity(速度)やAddForce(力)を使った移動ができ、慣性のある自然な動きを表現しやすい
  • 動く床・爆風による吹き飛ばしなど、他の物理システムと組み合わせやすい

Rigidbodyが向かないケース

  • ジャンプの高さや着地位置をフレーム単位で厳密に制御したいシビアなアクションゲーム
  • ネットワーク対戦などで移動の再現性(決定論的な挙動)が必須な場合
  • 物理演算特有の揺れ・めり込みを避けたい、シンプルな一方向移動のみのケース

こうしたケースでは、無理にRigidbodyへ移行せずCharacterControllerを使ほうが扱いやすいです。移行はあくまで手段であり、目的ではないことに注意してください。

CharacterControllerとRigidbody+CapsuleColliderの違い

CharacterController rigidbody 違いで検索している人向けに、両者の特徴を比較します。

項目CharacterControllerRigidbody+CapsuleCollider
移動方法Move() / SimpleMove()velocity / MovePosition / AddForce
物理演算受けない(独自の疑似物理)Unityの物理エンジンに従う
他オブジェクトとの相互作用基本的に一方的に押すのみ相互に力を伝え合う(押される・乗る等)
段差・坂道パラメータで個別調整が必要物理演算が自然に処理する
移動の再現性高い(決定論的)物理演算の影響を受けやすい
実装の手軽さシンプルで扱いやすい設定項目が多く、挙動の理解が必要

要するに、CharacterControllerは「移動処理に特化した専用コンポーネント」、Rigidbodyは「物理演算そのものを使って移動も実現するコンポーネント」という違いです。

なぜCharacterControllerではなくRigidbodyにする理由

CharacterControllerからRigidbodyへの移行を検討すべき代表的な判断基準は次の通りです。

  • 動く床・押せるオブジェクトなど、他の物理オブジェクトとキャラクターを相互作用させたい
  • 坂道・段差の挙動を、個別のコードではなく物理演算にまかせたい
  • 爆風で吹き飛ばす、掴まれて引っ張られるなど、外力による移動を扱いたい
  • ジャンプや落下の挙動を、他のコンポーネント(例:ジャンプ専用コンポーネント)とY軸だけ分担するような設計にしたい

逆に、移動の精密な制御を最優先するアクションゲームでは、CharacterControllerのままのほうが扱いやすい場合もあります。unity charactercontroller rigidbody 移行を検討するときは、プロジェクトで物理的な相互作用がどれだけ必要かを基準に判断してください。

3Dキャラクターの移動処理

本記事では、ここまで説明したRigidbodyとCapsuleColliderを組み合わせて、CharacterControllerを使わない3Dキャラクターの移動処理を実装します。単に位置を動かすだけでなく、以下の3つの動きを備えた、実践で使えるレベルのプレイヤーコントローラーを目指します。

  • カメラ相対移動: カメラが向いている方向を基準に、入力方向を変換して移動する
  • 移動方向への回転: キャラクターが移動している方向へ滑らかに向きを変える
  • 落下時の自動リスポーン: マップの外や穴に落下した場合に、指定した位置へ自動的に戻す

これらはすべて、RigidbodyのFixedUpdate内での速度・回転操作として実装します。

ソースコード

using UnityEngine;

[RequireComponent(typeof(Rigidbody), typeof(PlayerInputHandler))]
public class PlayerMove : MonoBehaviour
{
    [SerializeField] private Transform cameraTransform;
    [SerializeField] private float moveSpeed = 4f;
    [SerializeField] private float rotationSpeed = 10f;
    [SerializeField] private float fallResetY = -50f;
    [SerializeField] private Vector3 respawnPosition = Vector3.zero;

    private Rigidbody rb;
    private PlayerInputHandler input;
    private Animator animator;

    private Vector3 moveDirection;
    private bool isMoving;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        input = GetComponent<PlayerInputHandler>();
        animator = GetComponentInChildren<Animator>();

        rb.useGravity = true;
        rb.interpolation = RigidbodyInterpolation.Interpolate;
        rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;

        if (cameraTransform == null && Camera.main != null)
        {
            cameraTransform = Camera.main.transform;
        }
    }

    private void Update()
    {
        if (rb.position.y < fallResetY)
        {
            Respawn();
            return;
        }

        moveDirection = CameraRelativeDirection(input.MoveInput);
        isMoving = moveDirection.sqrMagnitude > 0.0001f;

        animator.speed = isMoving ? 1f : 0f;
    }

    private void FixedUpdate()
    {
        if (isMoving)
        {
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
            rb.MoveRotation(Quaternion.Slerp(rb.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime));
        }

        Vector3 horizontalVelocity = moveDirection.normalized * moveSpeed;
        Vector3 velocity = rb.linearVelocity;
        velocity.x = horizontalVelocity.x;
        velocity.z = horizontalVelocity.z;
        rb.linearVelocity = velocity;
    }

    private Vector3 CameraRelativeDirection(Vector2 moveInput)
    {
        if (cameraTransform == null || moveInput.sqrMagnitude < 0.0001f)
        {
            return Vector3.zero;
        }

        Vector3 forward = cameraTransform.forward;
        forward.y = 0f;
        forward.Normalize();

        Vector3 right = cameraTransform.right;
        right.y = 0f;
        right.Normalize();

        return forward * moveInput.y + right * moveInput.x;
    }

    private void Respawn()
    {
        rb.linearVelocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
        rb.position = respawnPosition;
        transform.position = respawnPosition;
    }
}

解説

  • カメラ相対移動の仕組み
Vector3 forward = cameraTransform.forward;
forward.y = 0f;
forward.Normalize();

Vector3 right = cameraTransform.right;
right.y = 0f;
right.Normalize();

return forward * moveInput.y + right * moveInput.x;

カメラのforward(前方向)・right(右方向)ベクトルをそのまま使うと、カメラが下や上を向いているときにY成分が混ざってしまい、キャラクターが浮いたり沈んだりする移動方向になってしまいます。

そこで、forward.y = 0f・right.y = 0fとしてY成分を打ち消し、XZ平面(水平面)に投影してからNormalize()で正規化しています。こうすることで、カメラがどの角度を向いていても、キャラクターは常に水平方向にのみ移動します。

最後に、入力値moveInput.x(左右)・moveInput.y(前後)をそれぞれright・forwardに掛けて合成することで、「カメラの向きを基準にした移動方向」が完成します。この処理を扱った解説記事は少なく、三人称視点のゲームでスティック入力をそのままワールド座標として使ってしまい、カメラを回すと操作方向がズレるという不具合の原因にもなりやすいポイントです。

  • FixedUpdateでの速度制御とY軸の分担設計

移動処理はFixedUpdate()の中で行っています。Unityの物理演算はFixedUpdateのタイミングで実行されるため、RigidbodyのvelocityやMovePositionを操作する処理は必ずFixedUpdateに書く必要があります。Updateで操作すると、フレームレートによって物理演算とのタイミングがズレて挙動が不安定になります。

Vector3 horizontalVelocity = moveDirection.normalized * moveSpeed;
Vector3 velocity = rb.linearVelocity;
velocity.x = horizontalVelocity.x;
velocity.z = horizontalVelocity.z;
rb.linearVelocity = velocity;

ここで注目したいのは、velocity.xとvelocity.zだけを書き換えて、velocity.yにはあえて触れていない点です。rb.linearVelocity(旧velocity)を丸ごと上書きしてしまうと、ジャンプや落下によるY方向の速度まで毎フレーム0にリセットされ、ジャンプ処理と競合してしまいます。

コード内のコメントにもある通り、PlayerMoveはX/Z軸(水平方向の移動)だけを担当し、Y軸(ジャンプ・落下)は別のコンポーネントに委ねる設計にしています。この役割分担により、PlayerMoveと別のジャンプ用コンポーネントのどちらが先にFixedUpdateで実行されても、お互いの担当軸を上書きし合わない安全な作りになります。AddForceではなくvelocityを直接操作しているのも、この分担を明確に保つためです。

  • 移動方向への回転処理
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
rb.MoveRotation(Quaternion.Slerp(rb.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime));

キャラクターを移動方向へ滑らかに回転させる処理です。Quaternion.LookRotation(moveDirection)で「移動方向を向くための目標の回転」を求め、Quaternion.Slerpで現在の回転から目標の回転へ少しずつ補間しています。

ポイントは、transform.rotationを直接書き換えるのではなくrb.MoveRotation()を使っている点です。RigidbodyがアタッチされたオブジェクトをTransform経由で直接動かすと、物理演算のスレッドと描画のタイミングがズレて衝突判定が不安定になります。回転も移動と同様に、Rigidbody用のAPIを通して行うのが基本です。

  • 落下時のリスポーン処理
private void Update()
{
    if (rb.position.y < fallResetY)
    {
        Respawn();
        return;
    }
    ...
}

private void Respawn()
{
    rb.linearVelocity = Vector3.zero;
    rb.angularVelocity = Vector3.zero;
    rb.position = respawnPosition;
    transform.position = respawnPosition;
}

マップの外や穴に落下してしまった場合に備えて、Y座標がfallResetYを下回ったら自動的にリスポーンする処理です。ジャンプ台の位置調整ミスやコライダーの隙間から落下してしまうことは実際の開発でもよく起きるため、あらかじめ用意しておくと安心です。

Respawn()では、位置をリセットするだけでなくlinearVelocity(速度)とangularVelocity(角速度)も必ずゼロに戻しています。速度をリセットせずに位置だけ戻すと、落下中に蓄積した速度がそのまま残り、リスポーン直後に想定外の方向へ吹き飛んでしまうため注意してください。

実演

3DモデルにPlayerMove.csをアタッチします。

この3DモデルはCapsuleColliderを追加してください。
アタッチした後に実行するとで冒頭で紹介した動作を実行できます。

よくあるハマりどころと対策

  • Transform移動によるめり込み

Rigidbodyが付いたオブジェクトをtransform.position += …のように直接移動させると、物理演算とタイミングがズレて壁や地面にめり込んでしまうことがあります。これはunity rigidbody transform 移動でよく報告される不具合です。

本記事のコードのように、移動はFixedUpdate内でrb.linearVelocityまたはrb.MovePosition()を使って行います。Transformを直接書き換える処理を移動ロジックに混在させないことが重要です。

  • カメラのカクつき

Rigidbodyの位置はFixedUpdate(物理演算のタイミング)で更新されますが、カメラの描画はUpdate(描画のタイミング)で行われます。この2つのタイミングにはズレがあるため、追従カメラがカクついて見えることがあります。

Awake()で設定しているrb.interpolation = RigidbodyInterpolation.Interpolate;が、このズレを補間して滑らかに見せる役割を持っています。加えて、追従カメラ側のスクリプトもUpdateではなくLateUpdateでプレイヤーを追従させることで、キャラクターの移動が反映された後にカメラが追従するようになり、カクつきをさらに抑えられます。

まとめ

CharacterControllerからRigidbody+CapsuleColliderへの移動処理の移行手順として、以下を解説しました。

  • CapsuleColliderとRigidbodyの基本的な役割と設定値
  • CharacterControllerとの違いと、移行すべきかどうかの判断基準
  • カメラ相対移動・FixedUpdateでのX/Z軸のみの速度制御・回転処理・落下リスポーンの実装
  • Rigidbodyへの移行は、他の物理オブジェクトとの相互作用が必要になったときに大きな効果を発揮します。一方で、精密な移動制御が最優先の場合は無理に移行する必要はありません。プロジェクトの要件に合わせて、CharacterControllerとRigidbodyを使い分けてください。

次のステップとして、Y軸を分担しているジャンプ処理や、カメラ相対移動と組み合わせる三人称カメラの実装もあわせて検討してみると、より完成度の高いプレイヤー操作を作り込めます。

タイトルとURLをコピーしました