Unity 6 CharacterController入門|Rigidbodyとの違い、Move・接地・重力・ジャンプを理解しよう

広告

Unityで3Dキャラクターを動かす方法はいくつかあります。中でもCharacterControllerは、プレイヤーを思いどおりの速度で動かしつつ、壁や地面との衝突を考慮したい場合に使いやすいコンポーネントです。

ただし、CharacterControllerを付けただけでは歩きません。Move()を呼んで移動量を与え、重力やジャンプの速度も自分で計算します。この記事ではこの役割分担を、Unity 6のInput Systemと一緒に理解します。

この記事のゴール:PlayerにCharacterControllerを付け、WASDで水平移動し、Spaceでジャンプできる最小構成を作ること。移動はワールド座標基準です(カメラ基準の移動は扱いません)。

CharacterControllerとは

CharacterControllerは、カプセル形状の当たり判定を持ち、Move()で指定した移動を壁・床などの衝突に応じて制限するコンポーネントです。Rigidbodyのように力を与えて移動させるものではありません。AddForce()で押しても通常のRigidbodyのようには動かず、Move()などで移動を指示します。

重要:CharacterControllerの衝突は万能な物理挙動ではありません。オブジェクトを自然に押したり、慣性・摩擦・反発をリアルに再現したりするには、別の設計が必要です。

Rigidbodyとの違い

比較CharacterControllerRigidbody
主な目的キャラクターの操作を優先物理シミュレーション
典型的な移動CharacterController.Move()速度・力・物理移動APIなど
重力Move()では自動適用されない重力設定などで物理演算される
段差・坂Step Offset・Slope Limitなどコライダー・物理設定などで対応
向いている例TPS/FPSの歩行プレイヤーボール、投げる箱、物理的に押される物体

両方が必要というわけではありません。まずはプレイヤーの移動方式をどちらにするか決めましょう。このチュートリアルではPlayerにRigidbodyを追加しません。

まずInspectorで設定する

  1. 3DシーンにPlane(地面)とCapsule(Player)を配置します。Playerの位置は (0, 1, 0) を目安にします。
  2. PlayerにはCharacterControllerを追加します。Capsuleの見た目のMeshと、CharacterControllerが持つ当たり判定は別物です。既存のCapsule Colliderが付いていれば削除し、当たり判定の重複を避けます。
  3. CharacterControllerのCenter、Height、Radiusが見た目に合うよう調整します。最初は Center=(0,0,0)、Height=2、Radius=0.5 程度を目安にできます。
  4. Skin Widthは接触時の余裕、Step Offsetは越えられる段差の目安、Slope Limitは登れる斜面の角度の上限です。値の意味を確認しながら調整してください。
  5. PlaneにはColliderが必要です。通常、作成したPlaneにはMesh Colliderが付属します。

注意:CharacterControllerの中心や寸法は、見た目のモデルと必ずしも一致しません。Gizmosを表示してカプセル形状を確認してください。

Input SystemでMoveとJumpを用意する

Input Systemパッケージを導入し、Input Actionsアセット(例:PlayerControls.inputactions)にAction Map Player を作ります。

ActionAction TypeControl TypeBinding
MoveValueVector22D Vector composite:W↑、S↓、A←、D→
JumpButtonButtonSpace

PlayerにPlayer Inputを追加し、Actionsにアセットを割り当て、Default MapをPlayer、BehaviorをInvoke Unity Eventsにします。Events > Player > Move と Jump に、次のスクリプトの該当メソッドを割り当てます。Move と Jump の引数は InputAction.CallbackContext です。使用中のInput SystemのバージョンによってInspector表示は多少異なる場合があります。

スクリプトを作る

PlayerCharacterMovement.csを作成し、Playerに追加します。PlayerにはCharacterControllerとPlayer Inputが付いている前提です。

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerCharacterMovement : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float jumpHeight = 1.5f;
    [SerializeField] private float gravity = -9.81f;
    [SerializeField] private float groundedDownSpeed = -2f;

    private CharacterController controller;
    private Vector2 moveInput;
    private float verticalSpeed;
    private bool jumpRequested;

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
    }

    // Player Input > Events > Player > Move に登録
    public void OnMove(InputAction.CallbackContext context)
    {
        moveInput = context.ReadValue<Vector2>();
    }

    // Player Input > Events > Player > Jump に登録
    public void OnJump(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            jumpRequested = true;
        }
    }

    private void Update()
    {
        // 直近のMoveで地面に触れたかを確認
        bool grounded = controller.isGrounded;

        // 接地中に下向き速度が過大にならないようにする
        if (grounded && verticalSpeed < 0f)
        {
            verticalSpeed = groundedDownSpeed;
        }

        if (jumpRequested && grounded)
        {
            // v^2 = 2gh。gravityは負の値
            verticalSpeed = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }
        jumpRequested = false;

        verticalSpeed += gravity * Time.deltaTime;

        // 画面方向ではなく、ワールドX/Z方向へ移動
        Vector3 horizontal = new Vector3(moveInput.x, 0f, moveInput.y);
        horizontal = Vector3.ClampMagnitude(horizontal, 1f);

        Vector3 velocity = horizontal * moveSpeed;
        velocity.y = verticalSpeed;

        // Moveが受け取るのは「速度」ではなく、このフレームの「移動量」
        controller.Move(velocity * Time.deltaTime);
    }
}

gravityには負の値を設定してください。ここでは一度のMove()に水平・垂直移動をまとめています。jumpRequestedでJumpイベントを記憶し、Update()で処理するため、入力イベント発生と移動処理のタイミングを分けています。ジャンプ押下の直後に接地していなければ要求は破棄する、というシンプルな仕様です(ジャンプ入力の先行受付は未実装)。

コードの流れを理解する

OnMove()のReadValue<Vector2>()で左右・前後の入力を受け取ります。new Vector3(moveInput.x, 0, moveInput.y)で3Dの水平移動方向に置き換えます。ClampMagnitudeで斜め方向が極端に速くなるのを避けます。

moveSpeedは「1秒あたり何ユニット進むか」という速度です。一方、Move()は1回で何ユニット動かすかを要求します。そのためvelocity * Time.deltaTimeで、1フレーム分の移動量にします。UnityのAPIリファレンスも、Moveは重力を使わないと明記しています。

ジャンプはMathf.Sqrt(jumpHeight * -2f * gravity)で初速度を求めます。その後、各フレームでverticalSpeed += gravity * Time.deltaTimeとし、上昇速度が減ってから落下に転じるようにします。正確な着地位置や最高到達点はフレーム間隔や衝突状態に左右されます。

isGroundedで何がわかる?

controller.isGroundedは最後に行ったMove(またはSimpleMove)において下側の地面に接触したかを示します。ゲーム開始直後や、まだMoveが一度も適切に行われていない瞬間は、期待する接地状態と一致しないことがあります。

ここではUpdate()冒頭で直近の移動結果を読み、ジャンプ可否を判断しています。Move()を呼んだ後には次の接地状態が更新されます。より厳密な接地や着地判定を作る場合には、CollisionFlagsや別の地面チェックを検討しましょう。

地面で -2f にする処理は必要?

groundedDownSpeed = -2fは必須APIでも魔法の定数でもありません。地面に触れている間、落下速度が大きく積算され続けるのを防ぎ、下向きにわずかな移動を維持するための実装上の工夫です。教科書で使っていない場合も、別の接地処理なら成立します。値や必要性は移動方式に依存します。ジャンプの初速度を与えた後、無条件に-2fへ戻すようなコードにしないことが重要です。

よくあるトラブル

症状確認すること
キーを押しても動かないPlayer InputのActions/Default Map、Eventsの登録、ActionのBinding
空中で止まる/落ちないMoveには重力がない。verticalSpeedを計算しているか
ジャンプしないJumpがButtonか、イベント登録済みか、context.performedが来るか、接地しているか
壁を通り抜ける壁と地面にColliderがあるか、Moveを使っているか
斜めが速い水平入力ベクトルを正規化・ClampMagnitudeしているか
階段を登れないStep Offsetと段差寸法、カプセル寸法を確認
坂を登れないSlope Limitと坂の角度を確認
キャラクターと当たり判定がずれるHeight、Center、Radius、Transformのスケールを確認

transform.position += ...で直接移動させると、CharacterControllerのMoveによる衝突制約を経由しません。通常の歩行処理では混ぜないようにしましょう。

まとめ

CharacterControllerは「物理演算に任せて歩かせる」のではなく、「ゲーム側で移動量を決め、衝突を考慮した位置調整をしてもらう」ための仕組みです。

覚えたいのは、入力→速度の計算→1フレームの移動量→Moveの順番。そして重力は自分で計算することです。まずワールド方向のWASD移動とSpaceジャンプを完成させたら、キャラクターが向いた方向やカメラの方向へ移動させる拡張に進みましょう。

関連記事

公式参考資料

訪問数 5 回, 今日の訪問数 5回

広告