U
タブ 02キャラクターと戦闘9 レッスン — タップで開く
02 / 09 · 12 分
キャラクターと戦闘

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

ここまでのプロジェクト

Assets/

Scenes/

Arena.unity

KnightCharacterController

Scripts/

CharacterStats.csCharacterSwitcher.csSimpleMover.cs (on Knight)→PlayerMovement.cs

すでにあるものこのレッスンで追加このレッスンで置き換え

tab 01 の SimpleMover で Knight は歩けるようになりましたが、壁をすり抜け、重力を無視し、常にワールドの軸に沿って動きます。このレッスンではそれを置き換えます——対象は Knight だけです。

Mage と Archer は SimpleMover のまま残します。それが狙い——このレッスンが終われば、同じシーンで 1 と 2 を押して二つの動きの違いを体感できます。

移動はいつでも同じ三段階です:入力を読む、方向に変える、Time.deltaTime を掛けて適用する。

アニメーション図

キー入力は Vector3 になり、そのベクトルは毎秒何メートルかに変わります。

移動スクリプトの全容

PlayerMovement.cs — remove SimpleMover from Knight first
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
    [Header("Move")]
    [SerializeField] private float walkSpeed = 4f;
    [SerializeField] private float runSpeed = 8f;
    [SerializeField] private float turnSmooth = 0.08f;

    [Header("Jump & gravity")]
    [SerializeField] private float jumpHeight = 1.4f;
    [SerializeField] private float gravity = -20f;

    private CharacterController cc;
    private Camera cam;
    private float verticalVelocity;
    private float turnVelocity;

    void Awake()
    {
        cc = GetComponent<CharacterController>();
        cam = Camera.main;
    }

    void Update()
    {
        // ---- 1. read input
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 input = new Vector3(h, 0f, v).normalized;

        // ---- 2. move relative to the camera
        Vector3 move = Vector3.zero;
        if (input.sqrMagnitude > 0.01f)
        {
            float targetAngle = Mathf.Atan2(input.x, input.z) * Mathf.Rad2Deg
                              + cam.transform.eulerAngles.y;

            float angle = Mathf.SmoothDampAngle(
                transform.eulerAngles.y, targetAngle, ref turnVelocity, turnSmooth);
            transform.rotation = Quaternion.Euler(0f, angle, 0f);

            move = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
        }

        float speed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed;

        // ---- 3. gravity and jump
        if (cc.isGrounded && verticalVelocity < 0f)
            verticalVelocity = -2f;                       // stick to the ground

        if (cc.isGrounded && Input.GetButtonDown("Jump"))
            verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);

        verticalVelocity += gravity * Time.deltaTime;

        // ---- 4. one single Move call
        Vector3 velocity = move * speed + Vector3.up * verticalVelocity;
        cc.Move(velocity * Time.deltaTime);
    }
}

Knight に付ける

  1. 1

    Hierarchy で Knight を選び、SimpleMover コンポーネントの三点メニューから → Remove Component をクリックします。Knight は新しいスクリプトに置き換わり、Mage と Archer は今まで通りです。

  2. 2

    Project の Assets/Scripts 内で Assets▸Create▸C# Script を作成し、名前は PlayerMovement —— ファイル名がクラス名と完全に一致しないと、Unity はアタッチを拒否します。

  3. 3

    開いたら、中身を上のスクリプトで丸ごと置き換えて保存します。

  4. 4

    Project から PlayerMovement を Knight にドラッグします。Character Controller が自然に現れます——これがスクリプト冒頭の [RequireComponent] の働きです。

  5. 5

    そのコントローラーをモデルにぴったり合わせないと、カプセルが半分床に沈みます:Center 0, 1, 0 · Radius 0.3 · Height 2。Scene ビューで緑のカプセルが Knight をすっぽり包んでいるはず。

中の四つの考え方

  • カメラ基準の移動:W は「カメラから遠ざかる」で、ワールドの +Z 方向ではありません。カメラの Y 角度を足せば、どのアングルでも操作が正しく感じられます。

  • SmoothDampAngle は急に変化する角度をなめらかな回転に変え、359° → 0° の回り込みも正しく扱います。

  • 重力は毎フレーム verticalVelocity に加算されます。だから落下は等速のスライドではなく加速します。

  • Sqrt(h * -2 * g) は「ちょうど h メートル跳ぶ」ための物理の式です——つまり Inspector の値はメートル単位なので、調整が気持ちいい。

isGrounded が点滅し続ける

  • 接地中は verticalVelocity を小さな負の数(-2)に設定すると、コントローラーが床に押さえつけられたままになります。

  • CharacterController の Slope Limit と Step Offset を確認してください。Step Offset を 0.3 にすると低い段差を歩けます。

  • 少しの coyote time を足します(縁を出てから 0.15 秒以内ならまだジャンプできる)。プレイヤーには気づかれませんが、ゲームはずっとフェアに感じられます。

スイッチャーを使い続けられるように

CharacterSwitcher は SimpleMover を有効化・無効化していました。Knight にはもう無いので、リストは両方のスクリプトに共通するものを保持する必要があります。

CharacterSwitcher.cs — one word changes
// before
[SerializeField] private List<SimpleMover> characters = new List<SimpleMover>();

// after — MonoBehaviour is the base class of both scripts
[SerializeField] private List<MonoBehaviour> characters = new List<MonoBehaviour>();

// everything below still works unchanged, because .enabled lives on MonoBehaviour
characters[i].enabled = (i == index);
coyote time
private float coyote;

// ...
if (cc.isGrounded) coyote = 0.15f;
else coyote -= Time.deltaTime;

if (coyote > 0f && Input.GetButtonDown("Jump"))
{
    verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
    coyote = 0f;
}