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との違い
| 比較 | CharacterController | Rigidbody |
|---|---|---|
| 主な目的 | キャラクターの操作を優先 | 物理シミュレーション |
| 典型的な移動 | CharacterController.Move() | 速度・力・物理移動APIなど |
| 重力 | Move()では自動適用されない | 重力設定などで物理演算される |
| 段差・坂 | Step Offset・Slope Limitなど | コライダー・物理設定などで対応 |
| 向いている例 | TPS/FPSの歩行プレイヤー | ボール、投げる箱、物理的に押される物体 |
両方が必要というわけではありません。まずはプレイヤーの移動方式をどちらにするか決めましょう。このチュートリアルではPlayerにRigidbodyを追加しません。
まずInspectorで設定する
- 3DシーンにPlane(地面)とCapsule(Player)を配置します。Playerの位置は
(0, 1, 0)を目安にします。 - PlayerにはCharacterControllerを追加します。Capsuleの見た目のMeshと、CharacterControllerが持つ当たり判定は別物です。既存のCapsule Colliderが付いていれば削除し、当たり判定の重複を避けます。
- CharacterControllerの
Center、Height、Radiusが見た目に合うよう調整します。最初はCenter=(0,0,0)、Height=2、Radius=0.5程度を目安にできます。 Skin Widthは接触時の余裕、Step Offsetは越えられる段差の目安、Slope Limitは登れる斜面の角度の上限です。値の意味を確認しながら調整してください。- PlaneにはColliderが必要です。通常、作成したPlaneにはMesh Colliderが付属します。
注意:CharacterControllerの中心や寸法は、見た目のモデルと必ずしも一致しません。Gizmosを表示してカプセル形状を確認してください。
Input SystemでMoveとJumpを用意する
Input Systemパッケージを導入し、Input Actionsアセット(例:PlayerControls.inputactions)にAction Map Player を作ります。
| Action | Action Type | Control Type | Binding |
|---|---|---|---|
| Move | Value | Vector2 | 2D Vector composite:W↑、S↓、A←、D→ |
| Jump | Button | Button | Space |
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ジャンプを完成させたら、キャラクターが向いた方向やカメラの方向へ移動させる拡張に進みましょう。
関連記事
公式参考資料
- Unity Scripting API — CharacterController
- Unity Scripting API — CharacterController.Move
- Unity Scripting API — CharacterController.isGrounded
- Unity Input System — Player Input










ディスカッション
コメント一覧
まだ、コメントがありません