見出し画像

【Unity】InputSystemアレルギーを解消!実は有能なInputsystemを使ってみよう!

inputsystemに抵抗がある方向けに
まずは実装できるレベルまでの記事です
意外と情報の少ないInputsystemについて基本からやっていきますので
是非ご覧になってください
「目次」で必要な個所に飛んでいただくと、便利かもしれません
ではスタート


この記事のゴール

今回は3Dのプロジェクトでプレイヤーを動かして、ポーズボタンを押すとポーズ画面に切り替わりプレイヤー操作からUI操作(メニュー操作)に切り替わるってところまでやってみます
プレイヤーオブジェクトにはアニメーターをセットして
「攻撃」に割り当てたボタンを押したときに攻撃をするようにします

InputSystemには他にも多くの機能やプロパティが
実装されていますがメジャーな機能に絞って記事を書きたいと思います
ここの大筋が理解できれば案外そのほかは応用がきくと思います

InputSystemのメカニズムであったり詳細な設定は省きます
とにかくこの記事を読み終わったときに上記目標が達成できることをゴールに設定していますのであしからず

step1 InputSystemの確認とActionMapの作成

先ずはActionMapを作っていくことから
ゲームのジャンルによると思いますが、
プレイヤー側のインプットを整理することから始めましょう
デフォルトでも構いませんが今回は作成します
hierarchyから作成 → Inputsystem → InputActionAsset を作成
(名前は何でもOK)
ActionMapはざっくり言うと、入力のモードのようなイメージ
探索モード、戦闘モード、UIコントロール など
コントローラーやキーボードに割り当てるボタンを
それぞれのモードに割り振りできます
ここで作成したプレイヤーのモードを後ほど
スクリプトで動的に切り替えることができます
今回は,Play画面(アクションモード) ⇔ メニュー画面(UI操作)
といった具合にします

Inputsystemの最大の利点は異なる
デバイスの入力情報を一度で設定できること
今回はPC版を想定していますがswitchやPS5、
モバイル端末の仮想コントローラー出力を想定している場合でも
このActionMapの設定だけで完結するのが利点です

今回はこんな感じで実装しています

ActionMaps

ActionとUIを作成
チュートリアルなので挙動は移動と攻撃するだけです

step2 プレイヤー操作のスクリプトを書く

スクリプトを書くといっても全然難しくないです
シンプルに考えていきましょう
c#スクリプトを新規作成
クラス名はPlayerControllerとでもしましょうか

『using UnityEngine.InputSystem; 』を忘れずに

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
   // Attackボタンが押されたときの処理
    public void OnAttack(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            Debug.Log("Attack!");   
        }
    }
}

まずはこれだけ。
【InputAction.CallbackContext】でActionMapに設定した(入力)を受け取ります
これに挙動を書き加えていくわけですが まずはこれで十分です
条件文context.started は「ボタンが押されたとき」
他にperformedとcanceldがあります
それぞれ「ボタンを押している間」、「ボタンを離したとき」
という認識で概ねOKです
(厳密には違いますがニュアンスを理解する上でOK)

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
   // Attackボタンが押されたときの処理
    public void OnAttack(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            Debug.Log("Attack!");   
            Attack(); //例えば攻撃の挙動とか
        }
    }

 // 左スティックやWASDでVector2も受け取れます
  public void OnMove(InputAction.CallbackContext context)
    {
        Vector2 movementInput = context.ReadValue<Vector2>();
        float horizontal = movementInput.x;
        float vertical = movementInput.y;

        Debug.Log("Horizontal: " + horizontal + ", Vertical: " + vertical);
    }
}

左スティックの入力やWASDでVector2も受け取れます
OnMoveはcontextにVector2を受け取っています
「〇〇.RedadValue<Vector2>()」の部分ですね
移動やカメラワークもこれでOK

最終的に今回はこんなスクリプトにしてみました

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody), typeof(Animator))]
public class PlayerController : MonoBehaviour
{
    private Rigidbody rb;
    private Animator animator;
    public float movementSpeed = 5f;
    private bool moving;
    private float horizontal;
    private float vertical;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        animator = GetComponent<Animator>();
    }

    public void Update()
    {
        if (moving)
        {
            Vector3 movement = new Vector3(horizontal, 0f, vertical).normalized * movementSpeed;
            rb.AddForce(movement, ForceMode.VelocityChange);

            // 移動速度をアニメーターの "Speed" パラメーターに渡す
            float currentSpeed = Mathf.Clamp01(movement.magnitude / movementSpeed); // 0から1の範囲にクランプ
            animator.SetFloat("Speed", currentSpeed);

            // プレイヤーの向きを移動方向に合わせる
            if (movement.magnitude > 0)
            {
                Quaternion targetRotation = Quaternion.LookRotation(movement, Vector3.up);
                transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, Time.deltaTime * 1000f);
            }
        }
        else
        {
            // 移動が止まった場合、速度のパラメーターをリセットする
            animator.SetFloat("Speed", 0f);
        }
    }

    // Attackボタンが押されたときの処理
    public void OnAttack(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            animator.SetInteger("Attack", 1);
        }
        else if (context.canceled)
        {
            animator.SetInteger("Attack", 0);
        }
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        Vector2 movementInput = context.ReadValue<Vector2>();
        horizontal = movementInput.x;
        vertical = movementInput.y;

        if (context.started)
        {
            moving = true;
        }
        else if (context.canceled)
        {
            moving = false;
        }
    }
}

少し行数が増えた気がしますが
やっていることは、とてもシンプルです
OnMove()の入力で受け取ったVector2の数値を
horizontal,verticalに入れて、Update()でRigidbodyにAddForceで力を加えて動かしているだけです

OnAttack()にはアニメーターにINT型の「1」を渡すようにしました
ボタンを離したときに「ボタンを離した」という信号を受け取り
アニメーターに「0」を渡すようにしてあります
(トリガーの方が実装が楽ですが今回はチュートリアルということで)

作成したスクリプトはプレイヤーのオブジェクトにアタッチしておいてください

Step3 PlayerInputコンポーネントを使ってスクリプトと入力を紐づける

そしたらPlayerオブジェクトにPlayerInputコンポーネントを
アタッチしましょう
Playerオブジェクトを選択してinspector上でAddComponetから
PlayerInputを追加

PlayerInput

Actionsに自分で作成したInputActionAssetをアタッチ
(デフォルトならそのままでもOK)
デフォルトActionMapは最初にどのActionMapで始まるかということなので
今回はPlayerを選択
最後にBehaviorですがメッセージを送ったり色々挙動がありますが
コールバックで引数管理をしたいのでInvorkUnityEventを選択します

InvokeUnityEventsでイベント階層が選択できる

するとイベント階層が選択できるようになりますので
Actionの階層を広げるとActionMapで設定した項目が一覧で出てきます

MoveとAttackにPlayerオブジェクトをアタッチしてPlayerControllerクラス内のOnMove()とOnAttack()をそれぞれ設定してください

作成したスクリプトと挙動を選択

これで準備はOK
ではここまでで一度実行してみましょう

TestPlay

問題なく入力を受け付けていますね
(攻撃モーションが(´・ω・`)ショボーン)
おそらくここまで実装するのに10分程度だと思います
(インストールや環境設定の時間は除く)

Step4 メニュー画面の実装

スタートボタンを押してポーズ画面にする挙動を実装していきましょう
ポーズ中はゲーム内時間が停止して
「ゲーム再開」「セーブ」「ロード」「コンフィグ」などのメニューを選択して、決定できる仕組みを実装します
なお、この記事では各機能の実装はチュートリアルしません
あくまで「プレイモードの切り替えとUI操作」を
InputSystemで制御することを理解していきます

では、さっそく
一度設定したInputActionを再度設定していきます
Actionモード ⇔ UIモード の行き来をするために
Actionモードに「Pause」のアクションを追加します
AddBindにて入力は「ボタン」でGamePadのメニューボタンと
キーボードの「M」キーを追加しましょう

ActionMaps

続けてActionMapのUIを設定していきます
UIのInputは思いのほか多様ですね、スティックや十字キーの入力のほか、マウスのポインター、モバイル端末の直接的なタップもUIの入力情報として考えられます
概ねUI操作で考えられる操作はデフォルトのActionMapにセッティング済みですので、そのまま使うのが〇
「Navigate」「submit」「cancel」「Pause」を見ていきましょう

UI側のActionMaps

基本的にはActionの時に設定した考え方と一緒です
「Action」に対して何の「デバイス」の、どの「ボタンを割り当てる」、そしていつの「タイミング」で入力を呼び出しますか?
をこのActionMapsで設定する。
これさえ押さえれればほとんど使いこなせます

さて、ActionMapsの設定が完了したので、hierarchyにUIを作成しましょう
今回はUIのボタンを作成します
作成中のゲームに合わせてポーズ画面に表示するボタンを用意します
「Menu」オブジェクトの配下に
今回は「Resume」「Setting」「Save」「Load」「Title」ボタンを作成しました
画面上のRectTransformにてそれぞれのボタンを配置を決めます

Step5 アクションモードとUIモードを切り替える

さて、ここまで出来たらあとは動的にActionMapsを切り替えるだけです

まずはInputControllerクラスを作成しましょう(名前はお好きに)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class InputController : MonoBehaviour
{
    public PlayerInput playerInput;

    private bool isPaused = false;
    private GameObject menuWindow;

    private void Start()
    {
        if (playerInput == null)
        {
            Debug.LogError("PlayerInput is not assigned!");
            return;
        }

        playerInput.onActionTriggered += TogglePause;

        // シーン開始時にMenuUIオブジェクトが非アクティブ状態になるようにする
        menuWindow = GameObject.FindGameObjectWithTag("MenuUI");
        if (menuWindow != null)
        {
            menuWindow.SetActive(false);
        }
        else
        {
            Debug.LogError("MenuWindow is not assigned and no object with 'MenuUI' tag found!");
        }
    }

    public void TogglePause(InputAction.CallbackContext context)
    {
        if (context.action.name == "Pause" && context.started)
        {
            if (!isPaused)
            {
                isPaused = true;
                Time.timeScale = 0;

                // MenuUIオブジェクトを探して代入する
                if (menuWindow == null)
                {
                    menuWindow = GameObject.FindGameObjectWithTag("MenuUI");
                }

                if (menuWindow != null)
                {
                    menuWindow.SetActive(true);
                    TogglePlayerInput("UI"); // UI ActionMap を有効化する
                }
                else
                {
                    Debug.LogError("MenuWindow is not assigned and no object with 'MenuUI' tag found!");
                }
            }
            else
            {
                isPaused = false;
                Time.timeScale = 1;

                if (menuWindow != null)
                {
                    menuWindow.SetActive(false);
                    TogglePlayerInput("Action"); // Action ActionMap を有効化する
                }
            }
        }
    }

    private void TogglePlayerInput(string actionMapName)
    {
        if (playerInput == null)
        {
            Debug.LogError("PlayerInput is not assigned!");
            return;
        }

        playerInput.SwitchCurrentActionMap(actionMapName); // 指定されたActionMapに切り替える
    }

    private void OnDestroy()
    {
        if (playerInput != null)
        {
            playerInput.onActionTriggered -= TogglePause;
        }
    }
}

少しだけ解説します
using UnityEngine.InputSystem;は忘れずに

・Start()ではPlayerInputつまり作成したInputAction AssetとMenuオブジェクトを探しておきます
・TogglePause()では「ポーズボタンを押した」入力情報をもとに
をトグルさせています、この時 TogglePlayerInput();で有効にしたいアクションマップの名前をstringで送ります
ちなみに
if (context.action.name == "Pause" && context.started)
この条件文で「”Pause”ボタンが押されたよ」という挙動が走り出します
ついでにゲーム時間の停止・再開もTime.timeScaleでトグルさせて今明日
・TogglePlayerInput(string actionMapName)では
受け取ったアクションマップを
playerInput.SwitchCurrentActionMap(actionMapName); 
でアクションマップを更新します今回は二つのモードの切り替えですが
この一文でアクションマップを変更することができるので、これは覚えておくといいと思います
あとは何やら書いてありますが、あまり気にしないでください
初めての方はこのスクリプトをコピペでもOKです

これはアクティブ状態が変わらないGameManagerオブジェクトやプレイヤーオブジェクトにアタッチしてください
インスペクター上でPlayerInputをアタッチするのも忘れずに

Step6 メニューボタンの選択と決定を実装する

最後の工程です
Step5では ActionMapsのスイッチを実装しましたが
Step6ではUIの操作をしていきます
UI(Canvas)配下あるボタンはInteractableの有効・無効
が選択できます

UI Button


このInteractableが有効のUIは動的に「選択可能なUI」として
取得することができます
今回はメニューボタンを縦に並べて操作してみます

Menuの操作をするスクリプトを作成しましょう
今回は「MenuController」とでもしておきます

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;

public class MenuController : MonoBehaviour
{
    private Selectable[] selectables; // メニューの選択可能な要素の配列
    private int currentIndex = 0; // 現在選択されている要素のインデックス

    void Start()
    {
        // アクティブなUI要素を取得して、selectables配列を更新する
        selectables = GetComponentsInChildren<Selectable>(true);

        if (selectables.Length > 0)
        {
            // 最初の要素を選択する
            currentIndex = 0;
            selectables[currentIndex].Select();
        }
    }

    // 上下左右の入力に応じてメニューをナビゲートする
    public void Navigate(Vector2 direction)
    {
        if (selectables == null || selectables.Length == 0)
        {
            // selectablesがnullまたは空の場合、処理を終了する
            return;
        }

        if (direction.y > 0) // 上向きの入力
        {
            currentIndex = (currentIndex == 0) ? selectables.Length - 1 : currentIndex - 1;
        }
        else if (direction.y < 0) // 下向きの入力
        {
            currentIndex = (currentIndex == selectables.Length - 1) ? 0 : currentIndex + 1;
        }
        else if (direction.x > 0) // 右向きの入力
        {
            currentIndex = (currentIndex == selectables.Length - 1) ? 0 : currentIndex + 1;
        }
        else if (direction.x < 0) // 左向きの入力
        {
            currentIndex = (currentIndex == 0) ? selectables.Length - 1 : currentIndex - 1;
        }

        // currentIndexが配列の範囲外の場合、適切なインデックスに補正
        if (currentIndex < 0)
        {
            currentIndex = selectables.Length - 1;
        }
        else if (currentIndex >= selectables.Length)
        {
            currentIndex = 0;
        }

        // ナビゲーションで選択状態を更新
        selectables[currentIndex].Select();
    }
}

では解説しますね
using UnityEngine.InputSystem;
を忘れずに!(もう慣れましたね♪)

Start()では先ほどUIのボタンで設定したInteractableのオブジェクトを探して配列にしています
配列の0番目を初期選択状態にしています

inputsystemで入力を受け取るのは
Navigate()です
Vector2を受け取りそれぞれの方向の入力を現在のindexに加算しているだけです

では挙動を確認してみましょう

ポーズ → UI操作

ここまで記事を読んでくださった方なら何となく理解されていると思いますがNavigate(Vector2 direction) この記述さえ覚えてしまえば
ActionMapsの入力を受け取り、
入力されたデータを自由に扱うことができます
すごい簡単ですよね

まとめ

InputsystemはとにかくActionMapsの設定をすることから始まります
慣れないうちはデフォルトのアクションマップを自分なりにいじる程度でも
結構いろんなゲームにそのまま使えます
スクリプトも必要になりますがこの記事で説明した通り、シンプルな挙動であればとても簡単な記述で入力を受け取ることができます

複数のデバイスで操作を想定するゲーム制作の場合
まとめて設定できるInputsystemは実は有能です
Inputsystemアレルギーの方が多いというのも知っていますが
触ってみると結構簡単で扱いやすいシステムになっています
是非この記事をきっかけにInputsystemで複数デバイス対応のゲームを制作してみてください

わからない点がありましたらコメントで質問いただければ
わかる範囲でお答えします

この記事が参考になったよ♪ という方はスキやフォローいただくと
励みになります!
ぜひぜひよろしくお願いいたします

この記事が気に入ったらサポートをしてみませんか?