Open10

Unityの入力管理

無職の学び舎無職の学び舎
  1. Project Stting > Inputに利用するパッド数×ボタン数の設定を定義
    どんなコントローラーが繋がれるかわからないため、Xbox基準など何かしら基準を決めて定義する
  2. 毎フレーム入力を見て、全入力をキャッシュするパッドクラスを用意する
  3. コマンドリストとコマンドとキーのマッピングデータを用意する
  4. パッドクラスはコマンドID的なものを渡すと、そのコマンドが成立しているかどうかを返す
無職の学び舎無職の学び舎

コントローラーによって入力が異なるので、それをゲーム内のプログラムで吸収するのは骨が折れる。
やるとしたら全てのInput Managerに全Axis、全ボタンを定義しておき
プログラム側コントローラーの種類判定して
このコントローラーのこのボタンだったらこの入力というマッピングをしないといけなそう。

ゲーム内でやるよりゲーム外で対応した方がよさそう案件ではある。

無職の学び舎無職の学び舎

新しいInputSystemもあるっぽいけど、レガシーな方でいくこととして。

とりあえずInputManagerに全Axis(16個)と全Button(20)個を登録。

無職の学び舎無職の学び舎

ゲーム内で使用するAxisやButton、キーボードのKeyに関してenumで別途定義
名前は何でもよかったけど、Xbox360コントローラーをベースとした。

  /// <summary>
  /// 軸の種類
  /// </summary>
  public enum AxisType
  {
    LX,
    LY,
    RX,
    RY,
    DX,
    DY,
    LR,
    LT,
    RT,
  }

  /// <summary>
  /// ボタンの種類
  /// </summary>
  public enum ButtonType
  {
    A,
    B,
    X,
    Y,
    L1,
    R1,
    LS,
    RS,
    Start,
    Back,
  }

  /// <summary>
  /// キーボード入力をマッピングするための定義
  /// キーボードのAが押されたときに、パッドのAが押された事にするなどマッピングする
  /// </summary>
  public enum KeyType
  {
    A,
    B,
    X,
    Y,
    L1,
    L2,
    R1,
    R2,
    LS,
    RS,
    Start,
    Back,
    U,
    D,
    L,
    R,
  }
無職の学び舎無職の学び舎

コントローラーのスティック(Axis)に該当するクラスを作る

  • ゲーム内で定義したAxisと実際のAxis(Input Managerに定義)の紐づけをしている
  • Axisの入力値や、入力の瞬間(Down)や入力が終わった瞬間(Up)、入力時間(Time)なども保持している
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

namespace MyGame.InputManagement
{
  /// <summary>
  /// ゲームパッドのスティック(軸)に該当するクラス
  /// </summary>
  public class Axis
  {
    /// <summary>
    /// 軸の種類
    /// </summary>
    private AxisType type;

    /// <summary>
    /// Input.GetAxisで入力を取得する際に指定する軸の名称(Joy1_Axis1など) 
    /// </summary>
    private string name = "";

    /// <summary>
    /// Y軸などは入力値の符号を反転させたいケースがあるので、そのためのフラグ
    /// </summary>
    private bool invert = false;

    /// <summary>
    /// 初回入力時のみtrueになる
    /// </summary>
    public bool IsDown = false;

    /// <summary>
    /// 入力し続けた状態で、入力がなくなったタイミングでtrueになる
    /// </summary>
    public bool IsUp = false;

    /// <summary>
    /// 入力がある間、常にtrue
    /// </summary>
    public bool IsHold {
      get { return (0 < Time); }
    }

    /// <summary>
    /// 軸の入力値-1 ~ 0の値をとる
    /// </summary>
    public float Value = 0;

    /// <summary>
    /// 継続して入力されている時間(秒)を保持する
    /// </summary>
    public float Time = 0;

    /// <summary>
    /// コンストラクタでは実際のゲームパッドの軸との関連付けをする
    /// </summary>
    public Axis(AxisType type, string name, bool invert)
    {
      Setup(type, name, invert);
    }

    /// <summary>
    /// 軸の設定を行う、この設定によりどの軸からの入力を受け取るか決まる。
    /// </summary>
    public void Setup(AxisType type, string name, bool invert)
    {
      this.type = type;
      this.name = name;
      this.invert = invert;
    }

    /// <summary>
    /// 軸の入力状態から各種パラメータの状態を更新する
    /// </summary>
    public void Update()
    {
      // 軸の入力を受け取る
      var value = Input.GetAxis(this.name);
      Value = (invert) ? -value : value;

      // 押された瞬間、離された瞬間、入力し続けている時間などを更新
      IsDown = IsUp = false;

      // 入力がある
      if (0 < Mathf.Abs(value)) {
        if (Time <= 0) {
          IsDown = true;
        }

        Time += UnityEngine.Time.deltaTime;
      }

      // 入力がない
      else {
        if (0 < Time) {
          IsUp = true;
        }
        Time = 0;
      }
    }

#if _DEBUG
    public void OnGUIDebug()
    {
      using (new GUILayout.HorizontalScope(GUI.skin.box)) {
        GUILayout.Label(Enum.GetName(typeof(AxisType), this.type));
        GUILayout.Label($"Value:{MyMath.Round(Value, 3)}");
        GUILayout.Label($"Down:{IsDown}");
        GUILayout.Label($"Up:{IsUp}");
        GUILayout.Label($"Hold:{IsHold}");
        GUILayout.Label($"Time:{MyMath.Round(Time, 3)}");
      }
    }
#endif
  }

}
無職の学び舎無職の学び舎

Axisクラスと同様にButtonに該当するクラスをつくる。
やりたい事はAxisと同じで、ゲーム内で定義したボタンとInpuManagerの定義の紐づけと、実際の入力情報の保持が役割

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

namespace MyGame.InputManagement
{
  /// <summary>
  /// ゲームパッドのボタンに該当するクラス
  /// </summary>
  public class Button
  {
    /// <summary>
    /// ボタンの種類
    /// </summary>
    private ButtonType type;

    /// <summary>
    /// Input.GetButtonで入力を取得する際に指定するボタンの名称(Joy1_Button0など)
    /// </summary>
    private string name;

    /// <summary>
    /// 初回入力時のみtrueになる
    /// </summary>
    public bool IsDown = false;

    /// <summary>
    /// 入力しつづけた状態で、入力がなくなったタイミングでtrueになる
    /// </summary>
    public bool IsUp = false;

    /// <summary>
    /// 入力がある間、常にtrue
    /// </summary>
    public bool IsHold {
      get { return (0 < Time); }
    }

    /// <summary>
    /// 継続して入力されている時間(秒)を保持する
    /// </summary>
    public float Time = 0;

    /// <summary>
    /// コンストラクタで実際のボタンとの関連付けを行う
    /// </summary>
    public Button(ButtonType type, string name)
    {
      Setup(type, name);
    }

    /// <summary>
    /// ボタンの設定を行う、この設定によりどの軸からの入力を受け取るか決まる。
    /// </summary>
    public void Setup(ButtonType type, string name)
    {
      this.type = type;
      this.name = name;
    }

    /// <summary>
    /// ボタンの入力状態から各種パラメータの状態を更新する
    /// </summary>
    public void Update()
    {
      bool value = Input.GetButton(this.name);
      IsDown = IsUp = false;

      // 入力があった場合
      if (value) {
        if (Time <= 0) IsDown = true;
        Time += UnityEngine.Time.deltaTime;
      }

      // 入力がない場合
      else {
        if (0 < Time) IsUp = true;
        Time = 0;
      }
    }

#if _DEBUG
    public void OnGUIDebug()
    {
      using (new GUILayout.HorizontalScope(GUI.skin.box)) {
        GUILayout.Label(Enum.GetName(typeof(ButtonType), this.type));
        GUILayout.Label($"Down:{IsDown}");
        GUILayout.Label($"Up:{IsUp}");
        GUILayout.Label($"IsHold:{IsHold}");
        GUILayout.Label($"Time:{MyMath.Round(Time, 3)}");
      }
    }
#endif
  }

}
無職の学び舎無職の学び舎

キーボードのKeyに対応するクラスも作る。

キーボードのXが押されたらコントローラーのAが押された事にするみたいなマッピングをするための布石

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

namespace MyGame.InputManagement
{
  /// <summary>
  /// キーボードのキーに該当するクラス
  /// </summary>
  public class Key
  {
    /// <summary>
    /// キーの種類
    /// </summary>
    private KeyType type;

    /// <summary>
    /// 対応するキーボードのKeyCode
    /// </summary>
    private KeyCode code;

    /// <summary>
    /// 初回入力時のみtrueになる
    /// </summary>
    public bool IsDown = false;

    /// <summary>
    /// 入力し続けた状態で、入力がなくなったタイミングでtrueになる
    /// </summary>
    public bool IsUp = false;

    /// <summary>
    /// 入力がある間、常にtrue
    /// </summary>
    public bool IsHold {
      get { return (0 < Time); }
    }

    /// <summary>
    /// 何かしら入力があるかどうか
    /// </summary>
    public bool HasInput {
      get {
        return (IsUp || IsHold);
      }
    }

    /// <summary>
    /// 継続して入力されている時間(秒)を保持する
    /// </summary>
    public float Time = 0;

    /// <summary>
    /// コンストラクタではキーボードとの関連付けをする
    /// </summary>
    public Key(KeyType type, KeyCode code)
    {
      Setup(type, code);
    }

    /// <summary>
    /// 軸の設定を行う、この設定によりどの軸からの入力を受け取るか決まる。
    /// </summary>
    public void Setup(KeyType type, KeyCode code)
    {
      this.type = type;
      this.code = code;
    }

    /// <summary>
    /// 軸の入力状態から各種パラメータの状態を更新する
    /// </summary>
    public void Update()
    {
      // 軸の入力を受け取る
      var value = Input.GetKey(this.code);

      IsDown = IsUp = false;

      // 入力がある
      if (value) {
        if (Time <= 0) {
          IsDown = true;
        }

        Time += UnityEngine.Time.deltaTime;
      }

      // 入力がない
      else {
        if (0 < Time) {
          IsUp = true;
        }
        Time = 0;
      }
    }

#if _DEBUG
    public void OnGUIDebug()
    {
      using (new GUILayout.HorizontalScope(GUI.skin.box)) {
        GUILayout.Label(Enum.GetName(typeof(KeyType), this.type));
        GUILayout.Label($"Down:{IsDown}");
        GUILayout.Label($"Up:{IsUp}");
        GUILayout.Label($"Hold:{IsHold}");
        GUILayout.Label($"Time:{MyMath.Round(Time, 3)}");
      }
    }
#endif
  }
}
無職の学び舎無職の学び舎

一個のゲームパッドに該当するクラス

上記で用意したAxisやButton、Keyを持つ。
ゲームパッドにキーボードのKeyを持たせるのはイメージ的に悩ましかったが
入力の受付はゲームパッドの役割とすることにした。

ゲームパッドのSetupで、どの軸がどの軸に該当するのかといった設定をしている
この設定次第で360用の設定だったり、PS4用の設定だったりにできる。

そしてこの設定をキーコンフィグなどのデフォルト設定を用意しておき
設定からセットアップ可能にする予定

この設定をセーブデータにもって置き、セーブデータからキーコンフィグを設定すれば
ゲーム内でのキーコンフィグにも対応可能という目論みである。

またUpdateでは各種Axis、Buttonの入力内容を更新する。
その後キーボードの入力も更新し、キーボードの入力があれば、該当する軸やボタンの入力があった事にするためにKeyの状態をAxis、Buttonの状態にマージするようにしている。

ゲーム内ではあくまでもAxisとButtonの入力を見るだけでよい。

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

namespace MyGame.InputManagement
{

  public class GamePad
  {
    /// <summary>
    /// パッドに存在する軸のテーブル
    /// </summary>
    private Dictionary<AxisType, Axis> axes = new Dictionary<AxisType, Axis>();

    /// <summary>
    /// パッドに存在するボタンのテーブル
    /// </summary>
    private Dictionary<ButtonType, Button> buttons = new Dictionary<ButtonType, Button>();

    /// <summary>
    /// 利用するキーボードのキーテーブル
    /// </summary>
    private Dictionary<KeyType, Key> keys = new Dictionary<KeyType, Key>();

    /// <summary>
    /// 接続されているパッドがあるかどうかのフラグ
    /// </summary>
    public bool IsConnectedPad { get; private set; } = false;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public GamePad(int padNo)
    {
      // 接続されているパッドがあるかどうかのフラグ
      IsConnectedPad = padNo <= (Input.GetJoystickNames().Length - 1);

      // 軸のセットアップ
      this
        .SetupAxis(padNo, AxisType.LX, 1, false)
        .SetupAxis(padNo, AxisType.LY, 2, true)
        .SetupAxis(padNo, AxisType.RX, 4, false)
        .SetupAxis(padNo, AxisType.RY, 5, true)
        .SetupAxis(padNo, AxisType.DX, 6, false)
        .SetupAxis(padNo, AxisType.DY, 7, false)
        .SetupAxis(padNo, AxisType.LR, 3, false)
        .SetupAxis(padNo, AxisType.LT, 9, false)
        .SetupAxis(padNo, AxisType.RT, 10, false);

      // ボタンのセットアップ
      this
        .SetupButton(padNo, ButtonType.A, 0)
        .SetupButton(padNo, ButtonType.B, 1)
        .SetupButton(padNo, ButtonType.X, 2)
        .SetupButton(padNo, ButtonType.Y, 3)
        .SetupButton(padNo, ButtonType.L1, 4)
        .SetupButton(padNo, ButtonType.R1, 5)
        .SetupButton(padNo, ButtonType.LS, 8)
        .SetupButton(padNo, ButtonType.RS, 9)
        .SetupButton(padNo, ButtonType.Back, 6)
        .SetupButton(padNo, ButtonType.Start, 7);

      // キーのセットアップ
      this
        .SetupKey(KeyType.A, KeyCode.Z)
        .SetupKey(KeyType.B, KeyCode.X)
        .SetupKey(KeyType.X, KeyCode.A)
        .SetupKey(KeyType.Y, KeyCode.S)
        .SetupKey(KeyType.L1, KeyCode.LeftShift)
        .SetupKey(KeyType.R1, KeyCode.LeftControl)
        .SetupKey(KeyType.L, KeyCode.LeftArrow)
        .SetupKey(KeyType.R, KeyCode.RightArrow)
        .SetupKey(KeyType.U, KeyCode.UpArrow)
        .SetupKey(KeyType.D, KeyCode.DownArrow);
    }

    /// <summary>
    /// パッドの入力状態を更新
    /// </summary>
    public void Update()
    {
      // 接続されているパッドがある場合は、パッド入力を更新
      if (IsConnectedPad) {
        Util.ForEach(this.axes, (type, axis) => { axis.Update(); });
        Util.ForEach(this.buttons, (type, button) => { button.Update(); });
      }

      // キーボード入力の状態を更新
      Util.ForEach(this.keys, (type, key) => { key.Update(); });

      // キーボード入力をAxisにマージ
      this
        .MergeKeyToAxis(KeyType.L, AxisType.DX, -1.0f)
        .MergeKeyToAxis(KeyType.R, AxisType.DX, 1.0f)
        .MergeKeyToAxis(KeyType.U, AxisType.DY, 1.0f)
        .MergeKeyToAxis(KeyType.D, AxisType.DY, -1.0f);

      // キーボード入力をButtonにマージ
      this
        .MergeKeyToButton(KeyType.A, ButtonType.A)
        .MergeKeyToButton(KeyType.B, ButtonType.B)
        .MergeKeyToButton(KeyType.X, ButtonType.X)
        .MergeKeyToButton(KeyType.Y, ButtonType.Y)
        .MergeKeyToButton(KeyType.L1, ButtonType.L1)
        .MergeKeyToButton(KeyType.R1, ButtonType.R1);
    }

    /// <summary>
    /// 軸の入力値を取得
    /// </summary>
    public float GetAxis(AxisType type)
    {
      if (this.axes.TryGetValue(type, out Axis axis)) return axis.Value;
      return 0;
    }

    /// <summary>
    /// 軸が押されたかどうか
    /// </summary>
    public bool GetAxisDown(AxisType type)
    {
      if (this.axes.TryGetValue(type, out Axis axis)) return axis.IsDown;
      return false;
    }

    /// <summary>
    /// 軸が離されたかどうか
    /// </summary>
    public bool GetAxisUp(AxisType type)
    {
      if (this.axes.TryGetValue(type, out Axis axis)) return axis.IsUp; 
      return false;
    }

    /// <summary>
    /// 軸が押されているかどうか
    /// </summary>
    public bool GetAxisHold(AxisType type)
    {
      if (this.axes.TryGetValue(type, out Axis axis)) return axis.IsHold;
      return false;
    }

    /// <summary>
    /// 軸の押されている時間(秒)
    /// </summary>
    public float GetAxisTime(AxisType type)
    {
      if (this.axes.TryGetValue(type, out Axis axis)) return axis.Time;
      return 0;
    }

    /// <summary>
    /// ボタンが押されているかどうか
    /// </summary>
    public bool GetButton(ButtonType type)
    {
      if (this.buttons.TryGetValue(type, out Button button)) return button.IsHold;
      return false;
    }

    /// <summary>
    /// ボタンが押されたかどうか
    /// </summary>
    public bool GetButtonDown(ButtonType type)
    {
      if (this.buttons.TryGetValue(type, out Button button)) return button.IsDown;
      return false;
    }

    /// <summary>
    /// ボタンが離されたかどうか
    /// </summary>
    public bool GetButtonUp(ButtonType type)
    {
      if (this.buttons.TryGetValue(type, out Button button)) return button.IsUp;
      return false;
    }

    /// <summary>
    /// ボタンが押されている時間(秒)
    /// </summary>
    public float GetButtonTime(ButtonType type)
    {
      if (this.buttons.TryGetValue(type, out Button button)) return button.Time;
      return 0;
    }

    /// <summary>
    /// キーボードの入力があった場合に、入力内容をボタン入力にマージする
    /// </summary>
    private GamePad MergeKeyToButton(KeyType keyType, ButtonType buttonType)
    {
      Key    key;
      Button button;

      if(!this.keys.TryGetValue(keyType, out key)) return this;
      if(!this.buttons.TryGetValue(buttonType, out button)) return this;

      if (key.HasInput) {
        button.IsDown = key.IsDown;
        button.IsUp   = key.IsUp;
        button.Time   = key.Time;
      }

      return this;
    }

    /// <summary>
    /// キーボードの入力があった場合に、入力内容を軸入力にマージする
    /// </summary>
    private GamePad MergeKeyToAxis(KeyType keyType, AxisType axisType, float value)
    {
      Key key;
      Axis axis;

      if (!this.keys.TryGetValue(keyType, out key)) return this;
      if (!this.axes.TryGetValue(axisType, out axis)) return this;

      if (key.HasInput) {
        axis.IsDown = key.IsDown;
        axis.IsUp   = key.IsUp;
        axis.Time   = key.Time;
        axis.Value  = value;
      }

      return this;
    }

    /// <summary>
    /// 軸インスタンスを生成し、軸の設定をする。
    /// </summary>
    private GamePad SetupAxis(int padNo, AxisType type, int no, bool invert)
    {
      this.axes[type] = new Axis(type, $"Joy{padNo + 1}_Axis{no}", invert);
      return this;
    }

    /// <summary>
    /// ボタンインスタンスを生成し、ボタンの設定をする。
    /// </summary>
    private GamePad SetupButton(int padNo, ButtonType type, int no)
    {
      this.buttons[type] = new Button(type, $"Joy{padNo + 1}_Button{no}");
      return this;
    }

    /// <summary>
    /// キーボードのキーインスタンスを生成し、ボタンの設定をする。
    /// </summary>
    private GamePad SetupKey(KeyType type, KeyCode code)
    {
      this.keys[type] = new Key(type, code);
      return this;
    }
    
#if _DEBUG
    public void OnGUIDebug()
    {
      using (new GUILayout.VerticalScope(GUI.skin.box)) {
        Util.ForEach(this.axes, (type, axis) => { axis.OnGUIDebug(); });
      }

      using (new GUILayout.VerticalScope(GUI.skin.box)) {
        Util.ForEach(this.buttons, (type, button) => { button.OnGUIDebug(); });
      }
    }
#endif
  }

}

無職の学び舎無職の学び舎

この時点で基本的な入力、キーボード入力の統合の夢は果たせた。

あとは左スティックの左と、十字キーの左のどちらが押されても左が押された事にする
みたいな事もしたいが

それは別途コマンドクラスなどを用意する事を検討する。

例えばMoveコマンドというクラスを用意し
Moveコマンド内で左スティックと十字キーの入力を統合し

ゲーム内ではMoveコマンドが成立しているかどうかを判定するような流れを妄想中