徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】新入力システム Input System を使ってみたよ

今回はUnityの新しくなった Input System を使ってみたお話。
「とりあえず使った」ぐらいの精度なので、プロパティの各動作などを詳しく調査はしていません。
Input System ってこういうものかぐらいの感覚でお楽しみください。
f:id:urahimono:20211005124854p:plain


この記事にはUnity2020.3.19f1を使用しています。
.Netのバージョン設定には.Net4.x を使用しています。

そろそろ入力システムを一新しようぜ

さて僕は来月のデジゲー博に向けて絶賛ゲームを開発中です。
ゲームパッドを複数台使って多人数で遊べるゲームを作っていますよ。

その際の入力システムを Asset の GamepadInput を使っていました。

assetstore.unity.com

パッと組み込めるので好んで使っていたのですが、もうこの処理って古い気が……。
いろいろ調べてみたころ、公式で新しい InputSystem が公開されているじゃありませんか!

forpro.unity3d.jp

ゲーム開発中ということもあり、あまり時間を取ることはできないけど、レガシーコードをそのままにしておくのは良くないことさ。
簡単に組み込めるようなら組み込んでみよっと。

InputSystem なんてものは無いとエラーが出る

では、早速コードを書いていきましょう。
えーと、InputSystemを使うために using を宣言しましょう。
using UnityEngine.InputSystem; っと。

f:id:urahimono:20211005121054j:plain
ん、オートコンプリートで出てこないな。
気にせずに入力していこう。
f:id:urahimono:20211005121116j:plain
ん、無いって?
そんなはずはない!
InputSystem は Unity2020.3.19 に組み込まれているはずだ!
これは VisualStudio のバグだな!

f:id:urahimono:20211005121212j:plain
Unity側でもエラーが出るじゃないか!
これは Unity のバグだな!

ん、先ほどのUnityの公式ブログになんか書いてあるな。

Input Systemをインストールする
Package Managerを開き、左上のPackagesをUnity Registryに変更してInput Systemを探し、インストールします。

あっ、Package Manager でインストールしなくてはいけないのか!

f:id:urahimono:20211005121319j:plainf:id:urahimono:20211005121316j:plain

……。
みんなドキュメントはちゃんと読もうぜ!

ちゃんと Input System インストールしてっと。
ダイアログで 新しいInput System を有効にするかの確認がでるのでYESを押してUnityを再起動。
これで Input System が使えるぞ!

ドキュメントにも書いてあるけど、

新しいInput System用のバックエンドに切り替えると、従来のUnityEngine.InputのAPIは使用できなくなりますので注意してください(使用しようとするとInvalidOperationExceptionが発生します)。
バックエンドの設定はProject SettingsのPlayer > Other Settings > Active Input Handlingに保存されており、後から変更できます。Bothに設定するとInput Systemと旧UnityEngine.Inputの両方のAPIが使用できるようになります。

と書いてあるように旧型入力方式は使えなくなるので注意なのさ!
旧型を使う場合は、PlayerSettingで再設定しようね。

デモを見ながら、InputSystemを使ってみよう!

さて、それで Input System はどう使うのだろうか。
ドキュメントを見るのと同時に、デモ用のサンプルが用意されているので、
それを見ながら使い方を見ていきますよ!
こういう時にデモを用意してくれるのは助かりますね!

デモのサンプルはインストール時と同じく、 Package Manager から Import 出来ますよ!
f:id:urahimono:20211005121500j:plain

Import したら Assets 以下の Samples にファイルが追加されましたね。
f:id:urahimono:20211005121512j:plain

早速みていきましょうか。

旧型のInput.GetKey() や Input.GetMouse() のような使い方

ではまず旧型の入力システムで使用していた、 Input.GetKey()Input.GetMouse() 相当の使い方がないかを調べてみましょう。

Simple DemoSimpleDemo_UsingState を見てみましょう。
これに似たような使い方がありました。
試してみましょう。

まずはキーボードから。

using UnityEngine;
using UnityEngine.InputSystem;
public class GameManager : MonoBehaviour
{
    private void Update()
    {
        Keyboard keyboard = Keyboard.current;

        // ↑を押している
        if (keyboard.upArrowKey.isPressed)
            transform.position += Vector3.forward * Time.deltaTime;

        // ↓を押した瞬間
        if (keyboard.downArrowKey.wasPressedThisFrame)
            transform.position += Vector3.back;
    }

} // class GameManager

f:id:urahimono:20211005121757g:plain

Keyboard.current でキーボード情報を取得して、
各キーボードのキーに対応しているプロパティからキー情報を取得して使用する形ですね。
主に使うのはこれらですね。

  • isPressed
    キーが押されているか否か
  • wasPressedThisFrame
    キーを押した瞬間が否か
  • wasReleasedThisFrame
    キーを離した瞬間が否か

キーボードが複数ある場合は、 Keyboard.all ですべてのキーボードが取得できるのでそちらを使う形ですね。

お次はマウス。

private void Update()
{
    Mouse mouse = Mouse.current;

    // 左クリックをしている
    if (mouse.leftButton.isPressed)
        transform.position += Vector3.forward * Time.deltaTime;

    // 右クリックをした瞬間
    if (mouse.rightButton.wasPressedThisFrame)
        transform.position += Vector3.back;
    
    // マウス座標を取得
    Debug.Log($"{mouse.position.ReadValue()}");
}

Mouse.current でマウス情報を取得して、
あとは各ボタンのプロパティを取得して使うだけですね。

キーボードのキーも、マウスのボタンも ButtonControl クラスに統一されているので、
すべて同じように使えるのが分かりやすくていいですね。

今回は使っていませんが、 position.ReadValue() でマウス座標が持ってこれる見たいですね。
WarpCursorPosition() などカーソル位置を制御する関数もあるので、調べてみると楽しそう。

最後はゲームパッド。

private void Update()
{
    Gamepad gamepad = Gamepad.current;
    if (gamepad == null)
        return;

    Vector2 stickValue = gamepad.leftStick.ReadValue();
    Vector3 vector3    = new Vector3(stickValue.x, 0.0f, stickValue.y);

    transform.position += vector3 * Time.deltaTime;
}

ボタンの操作はキーボードやマウスと同じなの割愛。
stick や pad の値は ReadValue() で取得できるようです。
ゲームパッドは、キーボードやマウスと違って接続されていない可能性も大いにあるため、
nullチェックはしたほうがよさそうですね。

これぐらいの情報があれば、旧型の入力システムから新しいInputSystemに移行は出来そうだ。

InputAction を使ってみよう

旧型の入力システムを差し替えるだけだったら、別に新しいInputSystemに無理に変換する必要はないですよね。
新しい機能を使ってこそ価値があるというものです。
というわけで、 InputAction を使ってみましょう。
参考にするのは、 SimpleDemo の SimpleController_UsingActions シーンです。

そもそも、どの移動したり攻撃したりするキー情報をコードで設定するのはナンセンスですよ。
一人で作っているのならまだしも、ゲームデザイナーさんなどと組んでゲームを開発している場合はコードに書いてしまうとゲームデザイナーさんが変更などをしにくですからね。
Inspector 上で設定することが出来るのが上策です。
旧型の入力システムにも似たような機能はありましたが、少しわかりにくかったですからね。

クラスのシリアライズするこんな変数を用意しましたよ。

public class GameManager : MonoBehaviour
{
    [SerializeField]
    private InputAction m_moveAction   = default;
    [SerializeField]
    private InputAction m_attackAction = default;
}

移動用と攻撃用の InputAction です。
Inspector 上で見るとこんな感じ。
f:id:urahimono:20211005122013j:plain

移動用の入力には何を使うのかを Inspector 上で設定できます。
しかも複数個設定できるという点が便利。
今回の例では、移動にはゲームパッドのPadキーとキーボードのWSADを指定しています。
コード側ではどのキーで設定されているのか、複数設定されているのかを気にしなくていいのでとても便利です。
組み込むとこんな感じになります。

using UnityEngine;
using UnityEngine.InputSystem;
public class GameManager : MonoBehaviour
{
    [SerializeField]
    private InputAction m_moveAction   = default;
    [SerializeField]
    private InputAction m_attackAction = default;

    private Animator m_animator  = default;

    private void Awake()
    {
        m_animator = GetComponentInChildren<Animator>();
    }
    private void Start()
    {
        m_moveAction.Enable();
        m_attackAction.Enable();
    }
    private void Update()
    {
        // 移動ベクトル取得。
        Vector2 stickValue = m_moveAction.ReadValue<Vector2>();
        Vector3 vector3 = new Vector3(stickValue.x, 0.0f, stickValue.y);
        transform.position += vector3 * Time.deltaTime;

        // 攻撃ボタンが押された。
        if (m_attackAction.triggered)
            m_animator.SetTrigger("Slash Attack 01");
    }
} // class GameManager

f:id:urahimono:20211005122048g:plain

Enable() で有効にするのをお忘れなく。
方向などのタイプの入力情報を取得するには ReadValue<>() を使い、
ボタンが押されたかどうかを triggered で取得できます。

デモのコードでは、 performed, started, canceled, などボタンを離した瞬間やボタンを一定時間押し続けた際の挙動なども設定できる見たいです。
こちらな関してはちょっと調査不足なので割愛ですー。
f:id:urahimono:20211005122104j:plain

PlayerInput と InputActionAsset を使ってみよう

ゲームを作っていけば、入力する情報のパラメータはおのずと共通化されてくるはずです。
それなのに、一つ一つのコンポーネントに対して先ほどのように InputAction に値を設定していたのでは気が滅入ります。
これらの入力情報を InputActionAsset にまとめることができます。
そしてその情報を使うのが PlayerInput コンポーネントです。
早速使ってみましょう。

InputActionAsset の作成には、
Project のウィンドウで右クリックメニューからか、 PlayerInput の Inspector 上から出来ますよ。

f:id:urahimono:20211005122204j:plainf:id:urahimono:20211005122206j:plain

EditAsset をクリックすることで、 InputAction の時のように入力情報を自由に設定できます。
Player用やUI用などカテゴリーを分けて設定することができ、シーンごとに入力方式を変えたい!
なんてことも出来て助かりますよ。

f:id:urahimono:20211005122233j:plainf:id:urahimono:20211005122235j:plain

この設定した InputActionAsset を PlayerInputActions に設定すれば使う準備は完了です!

PlayerInput の使い方

PlayerInput の使い方はいくつかあるみたいです。
見てみましょう。

UnityEvent を使う。

UI などでもおなじみの UnityEvent を使う方法ですね。
InputAction.CallbackContext を引数で持つ関数を登録しておけば、入力情報が取得できます。
f:id:urahimono:20211005122304j:plain

using UnityEngine;
using UnityEngine.InputSystem;
public class GameManager : MonoBehaviour
{
    private Vector2 m_moveValue      = default;
    private bool    m_attackTriggerd = default;
    public void OnMove(InputAction.CallbackContext aContext)
    {
        Debug.Log($"移動用の入力がきた! {aContext}", gameObject);
        m_moveValue = aContext.ReadValue<Vector2>();
    }
    public void OnAttack(InputAction.CallbackContext aContext)
    {
        Debug.Log($"攻撃用の入力がきた! {aContext}", gameObject);
        m_attackTriggerd = aContext.started;
    }
} // class GameManager

f:id:urahimono:20211005122317j:plain

Map 情報から取得する

UnityEvent もいいですが、人によっては処理の流れが追いにくいなどの理由で敬遠される方もいるかもしれません。
コード側だけで解決したい場合は、 PlayerInput から InputAction の マップ情報を取得できます。

PlayerInput.actions に Asset の InputAction が Map形式で登録されているので、そちらのアクセスすればOKです。
どうやらアクション名の大文字小文字は判定していないみたいですね。

using UnityEngine;
using UnityEngine.InputSystem;
public class GameManager : MonoBehaviour
{
    private PlayerInput m_playerInput = default;
    private Animator    m_animator    = default;

    private void Awake()
    {
        m_playerInput = GetComponent<PlayerInput>();
        m_animator    = GetComponentInChildren<Animator>();
    }

    private void Update()
    {
        // 移動ベクトル取得。
        Vector2 stickValue  = m_playerInput.actions["move"].ReadValue<Vector2>();
        Vector3 vector3     = new Vector3(stickValue.x, 0.0f, stickValue.y);
        transform.position += vector3 * Time.deltaTime;

        // 攻撃ボタンが押された。
        if (m_playerInput.actions["attack"].triggered)
            m_animator.SetTrigger("Slash Attack 01");
    }
} // class GameManager

InputActionAsset の コードを生成する

今回 InputSystem を調べていた中で一番驚いた機能です。
InputActionAsset からコードを生成出来るのです。

InputActionAsset の "Generate C# Class" を有効にして Apply することで
クラスコードが作成されるのだ。
しかも名前空間なども指定可能。
これは恐ろしい機能だ。

f:id:urahimono:20211005122456j:plainf:id:urahimono:20211005122458j:plain

コードがあるのなら、プログラマー的にはこっちのものである。

using UnityEngine;
public class GameManager : MonoBehaviour
{
    private MyInput     m_myInput  = default;
    private Animator    m_animator = default;

    private void Awake()
    {
        m_myInput  = new MyInput();
        m_animator = GetComponentInChildren<Animator>();
    }
    private void Start()
    {
        m_myInput.Enable();
    }

    private void Update()
    {
        
        // 移動ベクトル取得。
        Vector2 stickValue  = m_myInput.Player.Move.ReadValue<Vector2>();
        Vector3 vector3     = new Vector3(stickValue.x, 0.0f, stickValue.y);
        transform.position += vector3 * Time.deltaTime;

        // 攻撃ボタンが押された。
        if (m_myInput.Player.Attack.triggered)
            m_animator.SetTrigger("Slash Attack 01");
    }
} // class GameManager

この場合は PlayerInput も必要ないし、アセットの管理も必要ない。
クラスとしてコード上に存在しているため、どこからでもアクセスできる。
コード主義のプログラマーにとってはやりやすい環境である。

それぞれのリスク

三つのやり方を紹介しましたが、それぞれリスクはあります。
それは各アクションの名前を変更・削除した時のものです。

UnityEvent を使っている場合は、
Action名を変えても設定しているEvent情報に変化はありませんし、
削除しても、設定情報をなくなるだけです。

ですが、 Map 方式を使っている場合は、
アクセスするキーが変わったり無くなったりしてしまうので、
null チェックをしていない場合は、その部分で Exception が発生してしまいます。

最後のクラスコードを生成している場合は、プロパティ名が変わってしまうため、
当然コンパイルエラーが発生します。

後者の方式を取っているときは、アクション名の変更や削除は細心の注意を払う必要がありますね。

簡単ではありますが、新しい InputSystem を使ってみました。
まだまだ細かい点は詳しく調査する必要がありますし、ローカルマルチプレイヤーゲームの入力する場合はなかなかクセのある挙動をするため、現在絶賛苦戦中です。
デジゲー博用のゲームが作り終わった後にでも、もう一度まとめたい内容ですね。