U
05 / 22 · 12 分
FAQ

CharacterController が動かない:安い順に七つの確認

短い答え

Unity で CharacterController が動かないのはなぜ?

キャラクターが歩かない原因のほとんどはエラーを出さないので、安い順に確かめるのが近道です:入力がスクリプトに届いているか、コンポーネントとカプセルが本当に在るか、移動ベクトルが 0 でないか。続いて Move() 自身が拒む場合——Step Offset、Slope Limit、Is Trigger の床——そして Time.deltaTime、Transform のスケール、Rigidbody との競合をたどってください。

W を押してもキャラクターはその場に立ち、歩きアニメーションさえ再生されていて、Console はシーンとしたまま——これで普通です。Unity の CharacterController が動かない原因は、ほとんどがエラーを出さないもの。原因は七つのグループ、チェックの安い順に並べました。上から三つで大半を占めます。

1. 入力がスクリプトまで届いていない

移動スクリプトはどれも同じ始まり方です:数値を読み、0 でなければ動かす。直す前に、その数値が届いていると証明しましょう。入力を止めるありふれた理由は四つ、どれもエラーにはなりません。

  1. 1

    Play 中、そして有効なオブジェクトか。スクリプトはゲーム実行中しか動かず、Hierarchy でグレーアウトした名前は本当に何もしません。Inspector 最上部の名前横のチェックは、コンポーネントのそれとは別のスイッチです。

  2. 2

    コンパイル出来ているか。プロジェクト内のどんなスクリプトでも赤い行が Console にあるうちは、Unity は前回のコードを実行中。新しい Update はそこにありません。未保存のファイルも同じです。

  3. 3

    正しいオブジェクトに付いているか。移動コードが読み書きするのは Transform 一つです。親や子モデル、あるいはカメラに付くと的を外して動き、たいていは何も動かないように見えます。

  4. 4

    Update であって FixedUpdate ではない。しかも正確な名前。Unity はコールバックを名前で呼ぶので、小文字の void update() は通っても決して呼ばれません。移動の入力は Update、物理だけ FixedUpdate です。

MoveProbe.cs — temporary proof that input arrived
using UnityEngine;

// Attach it to the SAME object as your movement script, press Play, hold W.
public class MoveProbe : MonoBehaviour
{
    CharacterController cc;

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

    void Update()
    {
        Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));

        // A green ray out of the character's head, growing as you press.
        // It draws in the Scene view, not the Game view.
        if (input.sqrMagnitude > 0.001f)
            Debug.DrawRay(transform.position + Vector3.up, input * 2f, Color.green);

        // Once a second so the Console does not flood. Clicking a line selects
        // the object that wrote it: trust this output, not your memory.
        if (Time.frameCount % 60 == 0)
            Debug.Log(name + " | input " + input
                      + " | deltaTime " + Time.deltaTime
                      + " | controller " + (cc != null)
                      + " | grounded " + (cc != null && cc.isGrounded), this);
    }
}

Console に数値が出れば入力は正常、問題は下流です。何も出ないならスクリプトが走っていないことで、移動の計算を直しても何も助かりません。

2. スクリプトが無効、またはコンポーネント不足

有効なオブジェクトでも Probe は無音。残りの探索は Inspector 一つで足り、そのついでにコントローラーを持つオブジェクトも確認します。Play 中にキャラを選んで Scene ビューで F。緑のカプセルがモデルの場所と違う地点に出るなら、それは別々のオブジェクトです。

  • コンポーネントのチェックボックス。外した MonoBehaviour は update されません——入力は読まれず Move も呼ばれない——OnTriggerEnter などの物理メッセージは届くのにもかかわらず。クリック一つで解決なので、ここを最初に見てください。

  • このオブジェクトに CharacterController が無く、コードは null を持ったまま何もせずに立っています。CharacterController 自体がコライダーで二つ目は不要ですが、Move を呼ぶスクリプトと同じオブジェクトに在ることは必要です。クラスに [RequireComponent(typeof(CharacterController))] を付ければ Unity が自動で足し、スクリプトがある限り削除も許しません。

  • カプセルに体積がありません。雑に合わせると Radius や Height が 0 になり、カプセルごと床の下へ。そうなれば Move は世界で押すべきものを失い、Unity は黙ったままです。人のサイズのモデルにはレッスルの Center 0, 1, 0、Radius 0.3、Height 2 が合います。

3. 移動ベクトルが 0 になっている

入力は届き、コンポーネントも有効、Move も毎フレーム走る——引数は Vector3.zero。これが最多で最も見えにくい原因です。0 はいたってまともな値で、警告の出る余地がありません。

  • speed に代入していません。public float speed; は 0 始まり——Unity は数値に既定値をくれないのに、Inspector が 0 を出てもエラーではないので、毎秒 0 メートルを完璧に実行します。この一件だけで「CharacterController が動かない」質問の過半です。

  • Vector3.forward は transform.forward ではありません。前者は定数 (0, 0, 1)——キャラがどちらを向こうと世界の北です。後者がそのキャラの実際の正面。廊下向きに回転したあと前者を使えば、隣の壁に押し付けているだけ。4 番の変装です。

  • Move はワールド空間の方向、transform.Translate は既定がローカル。キャラ自身の軸から transform.TransformDirection(input) で組み立ててください。同じ一行でも両者は別々に受け取るので、このスクリプトを向こうへ持っていくと意味が分からなくなります。

  • 決して開かない門番:作りかけの Vector3.zero、animation event が来ず false のままの canMove、下りた開始フラグ、0 の field と比べるスタミナ判定。if (!ready) return の形を Move の上に置くと、証拠を残さず動きを止めます。

PlayerMove.cs — the shape that fails loudly
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerMove : MonoBehaviour
{
    // A float with no value stays 0 forever, and nothing about that is red.
    // [SerializeField] keeps it visible so you can watch it during Play.
    [SerializeField] private float speed = 6f;
    [SerializeField] private float gravity = -20f;

    CharacterController cc;
    float verticalSpeed;

    void Awake()
    {
        cc = GetComponent<CharacterController>();
        if (cc == null)
            Debug.LogError(name + " has no CharacterController on this object", this);
    }

    void Update()
    {
        Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        if (input.sqrMagnitude > 1f) input.Normalize();

        // Local input to world direction, along THIS character's facing.
        // Vector3.forward here would be world north instead.
        Vector3 planar = transform.TransformDirection(input) * speed;

        // A CharacterController ignores physics, so gravity is manual.
        if (cc.isGrounded && verticalSpeed < 0f) verticalSpeed = -2f;
        verticalSpeed += gravity * Time.deltaTime;

        // Move takes metres for THIS frame, so deltaTime is multiplied on.
        // The return value is what stopped you: Sides is a wall, Below the floor.
        CollisionFlags hit = cc.Move((planar + Vector3.up * verticalSpeed) * Time.deltaTime);

        if (input.sqrMagnitude > 0.001f && (hit & CollisionFlags.Sides) != 0)
            Debug.Log(name + " is pressing into something: " + hit, this);
    }
}

4. Move は呼ばれているが衝突に止められている

CharacterController はカプセルが通れる場所にしか行きません。無理なときは理由を CollisionFlags で返しますが、それを読む人はほとんどいない。一度受け取るだけで謎は多く消えます。Sides は壁、Below は床、Above は天井。

  • Step Offset、既定 0.3。これを越えれば壁です——静かに完全に。0.35 メートルの木箱で停止し、痕跡は Sides のみ。少し上げるかスロープを。ただし段には頭上空間が必要で、梁が低いと登れません。

  • Slope Limit、既定 45 度。それより急な斜面は即拒否。入力は読まれ Move も走る、なのに起きない。犯人は描き過ぎた Terrain の縁です。

  • Skin Width、既定 0.08——カプセル周りの余裕。大きすぎると skin width の約二倍未満の扉に入れず、床にタッチせず浮くので isGrounded が点滅。小さすぎると mesh と mesh の継ぎ目に掛かります。

  • トリガーの上に立っている。Is Trigger のコライダーを CharacterController は素通りするので、そうした床は支えにならず、isGrounded は永遠に false、if (!cc.isGrounded) return の門番は Move を一度も呼びません。

  • ジオメトリの中で生成される。開始でカプセルが壁と重なると、Move は毎フレーム自分が埋まる面からの脱出に消え、キャラは何もない相手にもがきます。動く前に Scene ビューで開始位置を見てください。

5. Time.deltaTime を掛け違え・割り違えている

速度は毎秒何メートルか、Move が欲しいのはこのフレームのメートル数。deltaTime はその単位変換で、掛け違えと割り違えは画面ではまったくの別物です。

Three lines on one field — only one of them is a walkable character
// speed = 6, running at 60 fps, so Time.deltaTime is about 0.0166.
cc.Move(input * speed);                   // 6 metres EVERY frame: 360 m/s
cc.Move(input * speed / Time.deltaTime);  // 360 metres per frame: gone by frame 2
cc.Move(input * speed * Time.deltaTime);  // 0.1 m this frame, which is 6 m/s — correct
  • 小さい speed は詰まりではなく単に遅いだけ——ゲームの縮尺では区別が付きません。speed = 0.001 は毎秒 1 ミリ、6 メートルの部屋に約一百分。Unity の単位では 2 が散歩、6 が走です。

  • 掛ける代わりに割ると毎秒数千メートルで発射され、たいていカメラが引きずられます。deltaTime を省くとフレームレート依存の動き——60 Hz では正しいのに 120 Hz のスマホでは二倍速。

  • Time.timeScale = 0 は Time.deltaTime を完全に 0 にするので、掛けたものは全て 0。ポーズメニューで設定し戻さないと、unscaled time の Animator は動くのにキャラだけ凍ります。

6. キャラは向きを変えるのに前に進まない

回転できることはすでに答えです。入力は届きスクリプトも走り、前に出る一歩だけ足りない。この組み合わせは Transform のスケールを指します。

  • CharacterController はスケールを前提に作られておらず、Unity は Transform が 1, 1, 1 を想定します。ルートや親をスケールするとカプセルは移動計算の想定から外れ、キャラは床や壁に溶接されたように止まる。影響を受けない回転だけが動き続けます。

  • いずれかの軸が 0 の localScale、ミラー反転の負値はカプセルから体積を完全に奪います。大抵は import 経由——センチ単位のファイルを Scale Factor 0.01 のまま、mesh 反転用に親が (-1, 1, 1) など。importer の Model タブで設定するか、コントローラーはスケール 1 のルートに置き、スケール済み mesh を子にしてください。

  • 親が回転し子キャラにもローカル回転があると、transform.forward は親の向きを返し、「前」は誰も見ない方向になります。階層を扁平化してください。CharacterController を持つオブジェクトと表示されるオブジェクトは、二つの掛け算ではなく一つの回転を共有するべきです。

7. 同じ Transform を狙う Rigidbody や別のコントローラー

CharacterController は衝突も移動も自分で行います。同じ Transform に書き込むものはすべて競合し、キャラは結局どちらにも従いません。

  • 同じオブジェクトに Rigidbody と CharacterController。物理は自分流に床へ沈め、スクリプトは Update で自作の重力を押す。結果は沈む・震える・床に張り付くで、W をいくら押しても変わりません。どちらか一方、プレイヤーなら CharacterController。

  • コントローラー脇の Kinematic Rigidbody は木箱を押すための意図的な選択——ただし別のスクリプトも運転した瞬間に二人の所有者問題です。rb.velocity や AddForce をそのボディへ書き、Move と同時に Rigidbody.MovePosition を呼んだ瞬間、位置の著者はまた二人になります。

  • 二つ目の CharacterController は衝突しません——互いを素通り——だから NPC では止められず、静的な壁・閉じたドア・Rigidbody 付きの木箱は止めます。しかも止められている相手はカメラの裏側。Scene ビューでキャラまで飛んで、カプセルが何に触れているか確認を。

  • isGrounded が一度も true にならない。if (!cc.isGrounded) return という門番は痕跡なく全てを断ります。床がトリガー、Center や Height を触ったあと底が床から浮いている、床のレイヤーが Edit > Project Settings > Physics でキャラ側とのチェックを外れている。W を押しながら cc.isGrounded をログ——一行で答えが出ます。

90 秒で原因を絞り込む手順

  • ✓Console に赤いエラーが無いこと。あれば Unity は前回のコード実行中です。
  • ✓Play 中で、オブジェクトが有効、コンポーネントにもチェック。
  • ✓W 保持中に MoveProbe が数値を出すか。無音なら 1 と 2 へ。
  • ✓Inspector の speed は 0 ではなく見覚えのある数値。Play 中に確認。
  • ✓緑のカプセルがキャラを包み床に乗っている。中でも浮きでも無い。
  • ✓Time.deltaTime は掛算、割算ではない。
  • ✓そのオブジェクトと全ての親のスケールは (1, 1, 1)。ゼロも負値も無し。
  • ✓この Transform を動かすのは一つ。CharacterController か Rigidbody、両方ではない。
  • ✓静止中に cc.isGrounded が true を出し、前方に Step Offset より高く Slope Limit より急なものはない。

キャラが歩き返したら、次に気になるのは触れた瞬間の振る舞いです。それは 衝突とトリガー の方へ。そして動き自体のあるべき形——カメラ基準の入力、重力、なめらかな旋回——は キャラクターを移動させる に。

これを作るレッスン

移動、ジャンプ、ブロックへの着地

この記事はそれだけで完結するレシピです。コースでは同じものを、5 つのタブを貫くひとつのプロジェクトの一部として作ります — キャラクターと戦闘 のレッスン 02。