【Unity】Input Systemでキャラクターを移動させる

こじゃらこじゃら

Input Systemを使ってキャラを走らせたりジャンプさせたりする方法を教えてほしいの。

このはこのは

Input SystemCharacter Controllerと組み合わせれば簡単かもしれないわ。

Input Systemを用いてキャラクターを操作する方法についての解説記事です。

本記事では、Input SystemCharacter Controllerを組み合わせて、キャラクターを操作する方法を紹介します。

Character Controller自体は、主にゲームオブジェクトを移動させる役割のみを持つコンポーネントで、コントローラーからの入力を受け取る機能はありません。

そのため、Character Controllerによってキャラクターを移動させるためには、次のような実装が必要になります。

実装の流れ
  • Input Systemのセットアップ
  • Character Controllerのセットアップ
  • Input Systemからの操作入力を受け取る処理の実装
  • 操作入力を元にCharacter Controllerを移動させる処理の実装

なお、Character Controllerを用いてキャラクターを移動させるやり方は、Unity公式アセットであるStarter Assetsでも使われているので、こちらのサンプルプロジェクトを覗いてみても良いでしょう。

本記事では、Input SystemとCharacter Controllerを用いてキャラクターを移動させたりジャンプさせたりする方法を具体的な手順とともに解説していきます。

動作環境
  • Unity 2022.1.5f1
  • Input System 1.3.0

スポンサーリンク

前提条件

事前にInput Systemがインストールされ、有効化されているものとします。

ここまでの手順が分からない方は、以下記事をご覧ください。

また、本記事ではPlayer Inputを用いてInput Systemの操作入力を受け取るものとして手順を解説します。

Player Inputの使い方が分からない方は、以下記事をご覧ください。

また、シーンには次のように操作対象のキャラクター(カプセル)が配置されているものとします。

例では、このカプセルをInput Systemの入力に応じて移動させるものとします。

Player InputおよびInput Action Assetの設定

Player Inputが生成するデフォルトActionを用いるものとします。

まず、操作対象のゲームオブジェクトを選択し、Player Inputコンポーネントをアタッチします。

Player InputコンポーネントのCreate Assetsボタンから、Input Actionアセットを適当な場所に保存します。

すると、次のようなデフォルトのInput Actionアセットが生成されます。

例では、PlayerマップのMoveアクションを移動量の操作入力として使用することとします。

また、ジャンプ操作を実現するためにJumpアクションを追加で定義することとします。

上の動画では、キーボードのスペースキーとゲームパッドのSouthボタンをジャンプボタンに割り当てています。

Input Actionの定義方法がよく分からない方は、以下記事も併せてご覧ください。

Character Controllerの適用

ゲームオブジェクトを移動させるために、対象オブジェクトにCharacter Controllerコンポーネントをアタッチします。

必要に応じて、Character Controllerの各種パラメータを調整してください。パラメータの詳細な説明は、以下公式リファレンスにて説明されていますので、必要に応じてご確認ください。

このはこのは

次はいよいよスクリプトを書いていくわ。頑張って付いてきてね。

こじゃらこじゃら

はい!がんばりますっ!

キャラクター操作スクリプトの実装

Input Systemから操作入力を受け取り、キャラクターを移動させたりジャンプさせたりするスクリプトを実装していきます。

実装すべき内容は以下の通りです。

実装の流れ
  • Input Systemの入力値を受け取る
  • 落下速度の計算
  • 移動速度の計算
  • Character Controllerに移動量を指定する
  • 向きの計算・反映

ここまでの流れを一通り実装したスクリプトの例を示します。ここではカメラの向きは敢えて考慮していません。向きを考慮した例は後ほど示します。

PlayerController.cs
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    [Header("移動の速さ"), SerializeField]
    private float _speed = 3;

    [Header("ジャンプする瞬間の速さ"), SerializeField]
    private float _jumpSpeed = 7;

    [Header("重力加速度"), SerializeField]
    private float _gravity = 15;

    [Header("落下時の速さ制限(Infinityで無制限)"), SerializeField]
    private float _fallSpeed = 10;

    [Header("落下の初速"), SerializeField]
    private float _initFallSpeed = 2;

    private Transform _transform;
    private CharacterController _characterController;

    private Vector2 _inputMove;
    private float _verticalVelocity;
    private float _turnVelocity;
    private bool _isGroundedPrev;

    /// <summary>
    /// 移動Action(PlayerInput側から呼ばれる)
    /// </summary>
    public void OnMove(InputAction.CallbackContext context)
    {
        // 入力値を保持しておく
        _inputMove = context.ReadValue<Vector2>();
    }

    /// <summary>
    /// ジャンプAction(PlayerInput側から呼ばれる)
    /// </summary>
    public void OnJump(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間かつ着地している時だけ処理
        if (!context.performed || !_characterController.isGrounded) return;

        // 鉛直上向きに速度を与える
        _verticalVelocity = _jumpSpeed;
    }

    private void Awake()
    {
        _transform = transform;
        _characterController = GetComponent<CharacterController>();
    }

    private void Update()
    {
        var isGrounded = _characterController.isGrounded;

        if (isGrounded && !_isGroundedPrev)
        {
            // 着地する瞬間に落下の初速を指定しておく
            _verticalVelocity = -_initFallSpeed;
        }
        else if (!isGrounded)
        {
            // 空中にいるときは、下向きに重力加速度を与えて落下させる
            _verticalVelocity -= _gravity * Time.deltaTime;

            // 落下する速さ以上にならないように補正
            if (_verticalVelocity < -_fallSpeed)
                _verticalVelocity = -_fallSpeed;
        }

        _isGroundedPrev = isGrounded;

        // 操作入力と鉛直方向速度から、現在速度を計算
        var moveVelocity = new Vector3(
            _inputMove.x * _speed,
            _verticalVelocity,
            _inputMove.y * _speed
        );
        // 現在フレームの移動量を移動速度から計算
        var moveDelta = moveVelocity * Time.deltaTime;

        // CharacterControllerに移動量を指定し、オブジェクトを動かす
        _characterController.Move(moveDelta);

        if (_inputMove != Vector2.zero)
        {
            // 移動入力がある場合は、振り向き動作も行う

            // 操作入力からy軸周りの目標角度[deg]を計算
            var targetAngleY = -Mathf.Atan2(_inputMove.y, _inputMove.x)
                * Mathf.Rad2Deg + 90;

            // イージングしながら次の回転角度[deg]を計算
            var angleY = Mathf.SmoothDampAngle(
                _transform.eulerAngles.y,
                targetAngleY,
                ref _turnVelocity,
                0.1f
            );

            // オブジェクトの回転を更新
            _transform.rotation = Quaternion.Euler(0, angleY, 0);
        }
    }
}

上記スクリプトをPlayerController.csという名前で保存し、操作対象のオブジェクト(CharacterControllerがアタッチされているオブジェクト)にアタッチします。

必要に応じてインスペクターから各種設定を行ってください。

設定が終わったら、Player Inputからサンプルスクリプトに入力値を渡すようにします。

Player InputのBehaviourInvoke Unity Eventsに変更し、Events > Player > MoveにOnMoveメソッドを、Events > Player > JumpOnJumpメソッドを登録します。

最終的に次のような設定がPlayer InputにされていればOKです。

実行結果

ここまでの手順を成功させると、次のようにプレイヤーを移動させたりジャンプさせたりできるようになります。

キーボード操作のほか、ゲームパッド操作にも対応しています。

スクリプトの解説

先述のサンプルスクリプトの処理について一通り解説します。

入力値の受け取り

Player Inputから入力値を受け取る部分は以下コードです。

/// <summary>
/// 移動Action(PlayerInput側から呼ばれる)
/// </summary>
public void OnMove(InputAction.CallbackContext context)
{
    // 入力値を保持しておく
    _inputMove = context.ReadValue<Vector2>();
}

/// <summary>
/// ジャンプAction(PlayerInput側から呼ばれる)
/// </summary>
public void OnJump(InputAction.CallbackContext context)
{
    // ボタンが押された瞬間かつ着地している時だけ処理
    if (!context.performed || !_characterController.isGrounded) return;

    // 鉛直上向きに速度を与える
    _verticalVelocity = _jumpSpeed;
}

移動の入力値受け取りでは、入力値をフィールドに保持するだけの処理を行っています。ここでCharacter Controllerによる移動処理を行わない理由は、OnMoveメソッドのコールバックは基本的に入力値が変化した時しか呼ばれず、変化しないときは静止してしまうためです。

ジャンプボタンの入力受け取りでは、ボタンが押された瞬間(context.performedがtrue)かつ着地している状態(CharacterControllerのisGroundedプロパティがtrue)のみジャンプ操作として、鉛直上向きに速度を与えるようにしています。

落下速度の計算

Updateイベント内の以下のコードにて、重力加速度を考慮した落下速度(鉛直方向の速度)計算を行っています。

var isGrounded = _characterController.isGrounded;

if (isGrounded && !_isGroundedPrev)
{
    // 着地する瞬間に落下の初速を指定しておく
    _verticalVelocity = -_initFallSpeed;
}
else if (!isGrounded)
{
    // 空中にいるときは、下向きに重力加速度を与えて落下させる
    _verticalVelocity -= _gravity * Time.deltaTime;

    // 落下する速さ以上にならないように補正
    if (_verticalVelocity < -_fallSpeed)
        _verticalVelocity = -_fallSpeed;
}

_isGroundedPrev = isGrounded;

着地判定フラグ(isGrounded)をチェックし、空中にいるときは下向きに重力加速度を与え、徐々に下向きに加速させるような速度計算を行っています。

着地しているときは、自由落下したときの初速を常に与えるようにしています。これは、後述するCharacterControllerによる移動時に、傾斜面や段差などから落ちる動きを実現するためです。

移動速度の計算

キャラクターの移動速度を計算する処理は次の部分です。

// 操作入力と鉛直方向速度から、現在速度を計算
var moveVelocity = new Vector3(
    _inputMove.x * _speed,
    _verticalVelocity,
    _inputMove.y * _speed
);

移動入力のxy成分をそれぞれxz方向の移動量として計算します。入力値は通常-1~1の範囲となるので [1] 、これに速さを掛けています。

y軸方向には、前述の落下速度計算によって得られた速度を指定しています。

Character Controllerへの移動量反映

以下コードにて、実際の移動が行われます。

// 現在フレームの移動量を移動速度から計算
var moveDelta = moveVelocity * Time.deltaTime;

// CharacterControllerに移動量を指定し、オブジェクトを動かす
_characterController.Move(moveDelta);

移動速度に前フレームからの経過時間Time.deltaTimeを掛けて移動量を求め、この移動量分だけ移動するようにCharacterController.Moveメソッドを実行しています。

このメソッドは、指定された方向に移動させ、コライダーがあったらぶつかって止まるように振る舞うことができます。

前述の移動速度の計算より、着地しているときは斜め下向きに移動しようとしているため、次のように段差を降りるような動作も実現できます。

向きの計算・反映

最後に、以下コードで移動方向に振り向くような動きを入れています。

if (_inputMove != Vector2.zero)
{
    // 移動入力がある場合は、振り向き動作も行う

    // 操作入力からy軸周りの目標角度[deg]を計算
    var targetAngleY = -Mathf.Atan2(_inputMove.y, _inputMove.x)
        * Mathf.Rad2Deg + 90;

    // イージングしながら次の回転角度[deg]を計算
    var angleY = Mathf.SmoothDampAngle(
        _transform.eulerAngles.y,
        targetAngleY,
        ref _turnVelocity,
        0.1f
    );

    // オブジェクトの回転を更新
    _transform.rotation = Quaternion.Euler(0, angleY, 0);
}

移動入力があった場合にのみ、振り向かせたい角度をMathf.Atan2メソッドで計算しています。

この時、角度を直接指定しても良いですが、急ではなく徐々に振り向くようにさせたかったので、Mathf.SmoothDampAngleメソッドで徐々に回転させるようにしています。

Mathf.SmoothDampAngleメソッドの具体的な使い方は、以下記事でも解説しています。

カメラの向きを考慮した例

先述のサンプルスクリプトでは、カメラが回転した場合も、ワールド空間の軸を基準に移動してしまっていました。

これをカメラの向き基準で移動できるようにした例も紹介しておきます。

PlayerControllerWithCamera.cs
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerControllerWithCamera : MonoBehaviour
{
    [Header("移動の速さ"), SerializeField]
    private float _speed = 3;

    [Header("ジャンプする瞬間の速さ"), SerializeField]
    private float _jumpSpeed = 7;

    [Header("重力加速度"), SerializeField]
    private float _gravity = 15;

    [Header("落下時の速さ制限(Infinityで無制限)"), SerializeField]
    private float _fallSpeed = 10;

    [Header("落下の初速"), SerializeField]
    private float _initFallSpeed = 2;

    [Header("カメラ"), SerializeField]
    private Camera _targetCamera;

    private Transform _transform;
    private CharacterController _characterController;

    private Vector2 _inputMove;
    private float _verticalVelocity;
    private float _turnVelocity;
    private bool _isGroundedPrev;

    /// <summary>
    /// 移動Action(PlayerInput側から呼ばれる)
    /// </summary>
    public void OnMove(InputAction.CallbackContext context)
    {
        // 入力値を保持しておく
        _inputMove = context.ReadValue<Vector2>();
    }

    /// <summary>
    /// ジャンプAction(PlayerInput側から呼ばれる)
    /// </summary>
    public void OnJump(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間かつ着地している時だけ処理
        if (!context.performed || !_characterController.isGrounded) return;

        // 鉛直上向きに速度を与える
        _verticalVelocity = _jumpSpeed;
    }

    private void Awake()
    {
        _transform = transform;
        _characterController = GetComponent<CharacterController>();

        if (_targetCamera == null)
            _targetCamera = Camera.main;
    }

    private void Update()
    {
        var isGrounded = _characterController.isGrounded;

        if (isGrounded && !_isGroundedPrev)
        {
            // 着地する瞬間に落下の初速を指定しておく
            _verticalVelocity = -_initFallSpeed;
        }
        else if (!isGrounded)
        {
            // 空中にいるときは、下向きに重力加速度を与えて落下させる
            _verticalVelocity -= _gravity * Time.deltaTime;

            // 落下する速さ以上にならないように補正
            if (_verticalVelocity < -_fallSpeed)
                _verticalVelocity = -_fallSpeed;
        }

        _isGroundedPrev = isGrounded;
        
        // カメラの向き(角度[deg])取得
        var cameraAngleY = _targetCamera.transform.eulerAngles.y;

        // 操作入力と鉛直方向速度から、現在速度を計算
        var moveVelocity = new Vector3(
            _inputMove.x * _speed,
            _verticalVelocity,
            _inputMove.y * _speed
        );
        // カメラの角度分だけ移動量を回転
        moveVelocity = Quaternion.Euler(0, cameraAngleY, 0) * moveVelocity;
        
        // 現在フレームの移動量を移動速度から計算
        var moveDelta = moveVelocity * Time.deltaTime;

        // CharacterControllerに移動量を指定し、オブジェクトを動かす
        _characterController.Move(moveDelta);

        if (_inputMove != Vector2.zero)
        {
            // 移動入力がある場合は、振り向き動作も行う

            // 操作入力からy軸周りの目標角度[deg]を計算
            var targetAngleY = -Mathf.Atan2(_inputMove.y, _inputMove.x)
                * Mathf.Rad2Deg + 90;
            // カメラの角度分だけ振り向く角度を補正
            targetAngleY += cameraAngleY;

            // イージングしながら次の回転角度[deg]を計算
            var angleY = Mathf.SmoothDampAngle(
                _transform.eulerAngles.y,
                targetAngleY,
                ref _turnVelocity,
                0.1f
            );

            // オブジェクトの回転を更新
            _transform.rotation = Quaternion.Euler(0, angleY, 0);
        }
    }
}

使い方は、一つ目のスクリプトと一緒です。主な変更点は次の2か所です。

移動量計算

// 操作入力と鉛直方向速度から、現在速度を計算
var moveVelocity = new Vector3(
    _inputMove.x * _speed,
    _verticalVelocity,
    _inputMove.y * _speed
);
// カメラの角度分だけ移動量を回転
moveVelocity = Quaternion.Euler(0, cameraAngleY, 0) * moveVelocity;

向き計算

// 操作入力からy軸周りの目標角度[deg]を計算
var targetAngleY = -Mathf.Atan2(_inputMove.y, _inputMove.x)
    * Mathf.Rad2Deg + 90;
// カメラの角度分だけ振り向く角度を補正
targetAngleY += cameraAngleY;

カメラの角度分だけ回転させて補正する処理が追加されています。

実行結果

カメラの向きに応じて移動方向も変化するようになりました。

さいごに

Input SystemCharacter Controllerを用いてキャラクターを動かす方法を解説しました。

本記事で紹介した方法はあくまで基本的な部分で、例えばアニメーションやその他のアクション動作などはありません。このような追加の動きは、本記事で紹介した方法を理解すると応用可能になるでしょう。

また、サードパーティー製のCharacter ControllerアセットをInput Systemに対応させたい場合は、移動量を受け取る部分をInput Systemのものに置き換える(またはスクリプトを修正する)といった対応が必要になるかもしれません。

入力値の受け取りとキャラクターの移動という2つのロジックに綺麗に分離されているアセットであれば、前者を変更すれば良く、Input Systemの移行も比較的楽になることでしょう。

参考サイト

スポンサーリンク