はじめに

Unity 2020.1 から新しい入力システム( Input System )がプレビューから検証済みに移行しました。

今回は、この Input System を使ってFPSゲーム風の簡単なサンプルを作成していきます。

開発環境

  • Unity 2020.1.2f1

環境構築

Input System を使用できるように設定していきます。

まず、Input System のパッケージをインストールします。

次に、Project Settings -> Player -> Active Input Handling の入力方式を従来の Input Manager から Input System に変更します。

変更すると自動でエディターが再起動されます。
再起動が終わったら Input System を使う準備は完了です。

シーンの作成

先に Input System に直接関係のない部分の実装をしていきます。

まず、地面となる Plane を配置します。

次に、 Main CameraTransform を以下の画像のように変更します。
今回はFPS視点なので、 Main Camera がプレイヤーとなります。

次に、射撃のターゲットとなる Cube を3つ配置します。

そして、3つのターゲットに、撃たれた時の処理を定義したコンポーネントをアタッチします。
今回は以下のような、撃たれるごとに色が変わるコンポーネントを作成しました。

using UnityEngine;

namespace Upft
{
    /// <summary>
    /// ターゲットオブジェクトクラス
    /// </summary>
    public class UpftTargetObject : MonoBehaviour
    {
        /// <summary>
        /// Renderer
        /// </summary>
        private Renderer _renderer;

        /// <summary>
        /// Start
        /// </summary>
        private void Start()
        {
            _renderer = GetComponent<Renderer>();
            
            _renderer.material.color = Color.green;
        }

        /// <summary>
        /// 色を変更する
        /// </summary>
        public void ChangeColor()
        {
            if (_renderer.material.color == Color.red)
            {
                _renderer.material.color = Color.green;
            }
            else
            {
                _renderer.material.color = Color.red;
            }
        }
    }
}

Main Camera 配下にクロスヘア用の Sprite を配置します。

これで、入力処理以外の実装が完了しました。

プレイヤーの入力処理の実装

ここから、 Input System を使ったプレイヤーの入力処理を実装していきます。

まず、プレイヤーとなる Main CameraPlayer Input コンポーネントをアタッチします。

Player Input コンポーネントの Create Actions... ボタンをクリックして、 Input Actions を作成します。

Input System では、ユーザーの入力 -> Input Actions -> Player Input という流れで入力が伝わっていきます。
ユーザーの入力と Player Input をつないでいる Input Actions を正しく設定することで、複数のデバイスによる入力を同じように扱うことができるようになります。

作成した Input Actions には、デフォルトでプレイヤーの移動やUI操作などの入力が設定されています。
今回は、 Player の中の MoveLookFire を使います。

Player Input コンポーネントの Actions に作成した Input Actions を設定します。

Player Input コンポーネントの BehaviorInvoke Unity Events に変更します。

Player Input コンポーネントの Events -> Player と開いていくと、Input Actions に定義されていた MoveLookFire のコールバックがあります。
これらのコールバックに設定する処理を実装していきます。

今回は、以下のようなクラスを作成しました。

using UnityEngine;
using UnityEngine.InputSystem;

namespace Upft
{
    /// <summary>
    /// プレイヤー制御クラス
    /// </summary>
    public class UpftPlayerController : MonoBehaviour
    {
        /// <summary>
        /// 移動速度
        /// </summary>
        [SerializeField] private float _moveSpeed = 10f;

        /// <summary>
        /// カメラ回転速度
        /// </summary>
        [SerializeField] private float _lookSpeed = 50f;

        /// <summary>
        /// Player Input
        /// </summary>
        private PlayerInput _playerInput = null;
        
        /// <summary>
        /// 現在の移動入力値
        /// </summary>
        private Vector2 _currentMoveInputValue = Vector2.zero;
        
        /// <summary>
        /// 現在のカメラ回転入力値
        /// </summary>
        private Vector2 _currentLookInputValue = Vector2.zero;

        /// <summary>
        /// 前回のカメラの向き
        /// </summary>
        private Vector3 _preRotation = Vector3.zero;

        /// <summary>
        /// Input Actions - Move
        /// </summary>
        private const string ACTION_MOVE = "Move";

        /// <summary>
        /// Input Actions - Look
        /// </summary>
        private const string ACTION_LOOK = "Look";

        /// <summary>
        /// Input Actions - Fire
        /// </summary>
        private const string ACTION_FIRE = "Fire";

        /// <summary>
        /// Device - ゲームパッド
        /// </summary>
        private const string DEVICE_GAMEPAD = "Gamepad";
        
        /// <summary>
        /// Start
        /// </summary>
        private void Start()
        {
            Cursor.lockState = CursorLockMode.Locked;

            if (TryGetComponent(out _playerInput))
            {
                _playerInput.actions[ACTION_MOVE].started += OnMove;
                _playerInput.actions[ACTION_MOVE].performed += OnMove;
                _playerInput.actions[ACTION_MOVE].canceled += OnMove;
                
                _playerInput.actions[ACTION_LOOK].started += OnLook;
                _playerInput.actions[ACTION_LOOK].performed += OnLook;
                _playerInput.actions[ACTION_LOOK].canceled += OnLook;
                
                _playerInput.actions[ACTION_FIRE].started += _ => OnFire();
            }
        }

        /// <summary>
        /// Update
        /// </summary>
        private void Update()
        {
            move();
            look();
        }

        private void move()
        {
            var moveForward = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(_currentMoveInputValue.x, 0, _currentMoveInputValue.y);
            transform.position += moveForward * _moveSpeed * Time.deltaTime;
        }

        private void look()
        {
            _preRotation.y += _currentLookInputValue.x * _lookSpeed * Time.deltaTime;
            _preRotation.x -= _currentLookInputValue.y * _lookSpeed * Time.deltaTime;
            _preRotation.x = Mathf.Clamp(_preRotation.x, -89, 89);
            transform.localEulerAngles = _preRotation;
        }

        /// <summary>
        /// 移動処理
        /// </summary>
        public void OnMove(InputAction.CallbackContext context)
        {
            _currentMoveInputValue = context.ReadValue<Vector2>();
        }

        /// <summary>
        /// カメラ回転処理
        /// </summary>
        public void OnLook(InputAction.CallbackContext context)
        {
            _currentLookInputValue = context.ReadValue<Vector2>();
        }

        /// <summary>
        /// 射撃処理
        /// </summary>
        public void OnFire()
        {
            var ray = new Ray(transform.position, transform.forward);
            if (Physics.Raycast(ray, out var hit, 100))
            {
                if (hit.collider.TryGetComponent(out UpftTargetObject target))
                {
                    target.ChangeColor();
                }
            }
        }
    }
}

キーを押した時と離した時のみコールバックが呼ばれるので、長押しでの移動を実現するために、現在の入力値をメンバー変数で保持して、 Update で毎フレーム参照するようにしています。

/// <summary>
/// 現在の移動入力値
/// </summary>
private Vector2 _currentMoveInputValue = Vector2.zero;

動かしてみる

ゲームパッドにも対応させる

マウスとキーボードによる操作の実装が完了しました。
次は、ゲームパッドによる操作にも対応させていきます……という流れで記事を書いていく予定でしたが、ここまでの実装ですでにゲームパッドでの操作にも対応できてしまいました。

なので、ここからはちょっとした調整をしていこうと思います。
実際にゲームパッドをPCにつないで操作してみてください。マウスに比べてカメラの回転速度が遅いと感じると思います。
折角なのでマウスでもゲームパッドでも同じくらいの速度で動かせるようにしてみます。

Input ActionsLook -> Right Stick [Gamepad] -> Properties -> Processors を設定します。
これを設定することで、マウスの入力値はそのままに、右スティックで入力した値のみ加工することができます。
今回は Scale Vector 2 を追加することで、入力値に倍率をかけます。

おわりに

今回は、 Input System を使ってFPSゲーム風のサンプルを作成してみました。
非常に便利な機能なので、ぜひ皆さんも使ってみてください。