新InputSystem使ってみた

新しいInputSystemを使ってみたのですが、ハマったポイントがあったので、記事にします。

記事内で使用しているUnityのバージョンは、「2019.2.2f1」です。

tsubakit1.hateblo.jp
tsubakit1.hateblo.jp
gametukurikata.com
基本的には上の記事でほとんどのことは分かるのですが、改めて導入から解説してみます。


InputSystemを導入する

Window→Package Managerを開きます。
f:id:taipui:20200221122732p:plain
「InputSystem」をインストールします。
f:id:taipui:20200221123008p:plain
※InputSystemが表示されない方は、「Show preview packages」にチェックを入れると
表示されます。
 f:id:taipui:20200221123129p:plain
ProjectSettingsを開きます。
f:id:taipui:20200221123326p:plain
「Player」のActive Input Handlingの箇所を変更します。
f:id:taipui:20200221123433p:plain
Input System (Preview)かBothに変更します。

設定ファイルを作成します。
Input System Packageで、Create settings assetボタンを押します。
f:id:taipui:20200221123613p:plain

使ってみる

InputSystemは、従来の

Input.GetButton("hoge");

のように、そのまま入力を取ることもできるのですが、
今回は従来のInputManagerのように、
予めアクションを登録する方法を紹介します。
今までInputManagerでアクションを管理していたところが、
新しいInputSystemでは、InputActionというシステムで管理することになります。
そして、InputActionをまとめたファイルを「InputActions」と呼びます。

InputActionを設定する

InputActionsは、プロジェクト内にファイルとして作ります。
Creates→InputActionsを選択すると、
その場所にInputActionsファイルが作成されます。
f:id:taipui:20200221124243p:plain
記事内ではこのファイルに「NewControl」という名前を付けたとして進行します。
NewControlをダブルクリックすると、InputActionsのウィンドウが開きます。
ここでアクションを登録していきます。
f:id:taipui:20200221124339p:plain
ActionMapsは、ActionMapを管理する場所です。
ActionMapとは、これから登録していくActionをまとめたものとなります。
今回はアウトゲーム用とインゲーム用、2種類のActionMapを作ることにします。
f:id:taipui:20200221124451p:plain
作りました。
Action Mapsと書いてあるところの右の+ボタンで作成できます。

続いて、Actionを登録していきます。
OutGameでは、主にアウトゲームのUI周りの入力を想定しているので、

  • スタートボタン(Start)

  • 上選択ボタン(Up)

  • 下選択ボタン(Down)

  • 左選択ボタン(Left)

  • 右選択ボタン(Right)

  • 決定ボタン(Decide)

  • キャンセルボタン(Cancel)

を登録することにします。

ActionMapsを作成した時に、New actionというActionが既にあるので、
そのActionをStartという名前に変更し、Enterキーを割り当てます。
f:id:taipui:20200221125155p:plain

  • Start→Enterキー

  • Up→上キーとWキー

  • Down→下キーとSキー

  • Left→左キーとAキー

  • Right→右キーとDキー

  • Decide→Enterキー

  • Cancel→Backspaceキー

に登録することにします。

一つのアクションに複数の入力を対応させるには、
登録したいActionの右の+ボタンをクリックし、
Add Bindingを選択します。
f:id:taipui:20200221125652p:plain
f:id:taipui:20200221130057p:plain
追加しました。

今回はゲームパッドも対応させたいので、
ゲームパッドのボタンも登録していきます。
ゲームパッドXboxコントローラーを想定します。

  • Start→スタートボタン

  • Up→左スティック上と十字キー上と右スティック上

  • Down→左スティック下と十字キー下と右スティック下

  • Left→左スティック左と十字キー左と右スティック左

  • Right→左スティック右と十字キー右と右スティック右

  • Decide→Bボタン

  • Cancel→Aボタン

を割り当てます。
ゲームパッド用のボタンはGamePadのところにあります。
f:id:taipui:20200221131250p:plain
ひとまず感覚でそれっぽいボタンを設定していきます。
(知る方法はありますが後述)
f:id:taipui:20200221131737p:plain
登録が終わりました。

続いて、InGameの方も追加したいと思います。
InGameでは、インゲームのプレイヤーの入力を想定してActionを設定していきます。

  • 上移動

  • 下移動

  • 左移動

  • 右移動

  • アクション(Act)  

を登録したいので、

  • Up→上キーとWキーと左スティック上と十字キー上と右スティック上

  • Down→下キーとSキー左スティック下と十字キー下と右スティック下

  • Left→左キーとAキーと左スティック左と十字キー左と右スティック左

  • Right→右キーとDキーと左スティック右と十字キー右と右スティック右

  • Act→マウスの左クリック  

といきたいところですが、今回はゲームパッドも対応させたい、
それに、プレイヤーの移動はアナログ入力にしたい・・・ということで、

  • 移動→Move

  • アクション→Act

とし、

  • Move→上下左右キーとWASDキーと左スティックと十字キーと右スティック

  • Act→マウスの左クリックとBボタン

として登録することにします。

選択しているActionMapをInGameにし、Actionを登録していくのですが、
スティック/ボタン入力とキー入力とで設定方法が少し異なります。

スティック入力に関しては、
キーを設定する箇所で、Up/Down/Left/Rightではなく、
スティックそのものを選択します。
f:id:taipui:20200221134344p:plain
キー入力に関しては、Bindingを追加するところで、
先ほど解説したAdd Bindingではなく、
Add 2D Vector Compositeを選択します。
f:id:taipui:20200221134609p:plain
すると、上下左右のBindingがセットになったActionが作成されました。
f:id:taipui:20200221134659p:plain
あとは先ほど同様、設定していきます。
マウスの左ボタンはMouse→Left Buttonになります。
f:id:taipui:20200221135117p:plain
設定が終わりました。

右上らへんのAuto-Saveにチェックをつけていない場合は、
上らへんにあるSave Assetをクリックして保存しましょう。

※ちなみに、テラシュールブログさんの記事にあった
Control Schemaを使ってみましたが、
ゲームパッドの入力が取れなくなってしまったので、
今回は導入を見送りました。
 

実装する

先ほど設定したInputActionsを実際に使用してみます。

Actionをスクリプト側で使うには、
作成したアクション名の先頭に「On」を付けたpublicなメソッドを作成します。
今回はOutGameのActionMapをテストするスクリプトを作成します。

using UnityEngine;


public sealed class InputSystemTestForOutGame : MonoBehaviour
{
 //========================================
 // Methods.
 //========================================

 //----------------------------------------
 // Event methods.
 //----------------------------------------

 public void OnStart()
 {
   Debug.Log("start");
 }

 public void OnUp()
 {
   Debug.Log("up");
 }

 public void OnDown()
 {
   Debug.Log("down");
 }

 public void OnLeft()
 {
   Debug.Log("left");
 }

 public void OnRight()
 {
   Debug.Log("right");
 }

 public void OnDecide()
 {
   Debug.Log("decide");
 }

 public void OnCancel()
 {
   Debug.Log("cancel");
 }
}

そして、このスクリプトをシーン内のGameObjectにアタッチし、
同じ場所にPlayerInputをアタッチします。
Default MapはOutGameを選択します。
f:id:taipui:20200221141753p:plain
これで実行すると、入力に応じて、ログが出力されます。

InputActionsを使うことができました!
f:id:taipui:20200221142108p:plain
ただ、スティックのようなアナログ入力だと、
1フレームに何度も入力が入ってしまいます。
基本的に1フレーム1入力でよいので、
Update()内で入力を処理するよう変更します。

また、アナログ入力に関して、入力のコールバックを受け取りたいため、
PlayerInputを使うのではなく、InputActionsのスクリプトを用いる方法に変更します。

New Controlsを選択し、Inspectorからスクリプトを作成します。
f:id:taipui:20200221154659p:plain
すると、New Controlの場所に、同名のスクリプトが作成されます。
InputActionsファイルにNew Controlsとは違う名前を付けていると、
作成されるスクリプトやクラス名もその名前で作られます。
f:id:taipui:20200221154814p:plain
そして、テスト用のスクリプトを修正します。

using UnityEngine;
using UnityEngine.InputSystem;


public sealed class InputSystemTestForOutGame : MonoBehaviour, NewControls.IOutGameActions
{
  //========================================
  // Properties.
  //========================================

 private NewControls.OutGameActions Input { get; set; } = default;

 private bool PressedStart { get; set; } = false;

 private bool EnteredUp { get; set; } = false;

 private bool EnteredDown { get; set; } = false;

 private bool EnteredLeft { get; set; } = false;

 private bool EnteredRight { get; set; } = false;

 private bool PressedDecide { get; set; } = false;

 private bool PressedCancel { get; set; } = false;

  //========================================
  // Methods.
  //========================================

  //----------------------------------------
  // MonoBehaviour methods.
  //----------------------------------------

  private void Awake()
  {
    Input = new NewControls.OutGameActions(new NewControls());
    Input.SetCallbacks(this);
  }

  private void Update()
  {
    if (PressedStart)
    {
      Debug.Log("start");
      PressedStart = false;
    }
    else if (EnteredUp)
    {
      Debug.Log("up");
      EnteredUp = false;
    }
    else if (EnteredDown)
    {
      Debug.Log("down");
      EnteredDown = false;
    }
    else if (EnteredLeft)
    {
      Debug.Log("left");
      EnteredLeft = false;
    }
    else if (EnteredRight)
    {
      Debug.Log("right");
      EnteredRight = false;
    }
    else if (PressedDecide)
    {
      Debug.Log("decide");
      PressedDecide = false;
    }
    else if (PressedCancel)
    {
      Debug.Log("cancel");
      PressedCancel = false;
    }
 }

 private void OnDestroy()
 {
   Input.Disable();
 }

 private void OnEnable()
 {
   Input.Enable();
 }

 private void OnDisable()
 {
   Input.Disable();
 }

 //----------------------------------------
 // Event methods.
 //----------------------------------------

 public void OnStart(InputAction.CallbackContext context)
 {
   PressedStart = true;
 }

 public void OnUp(InputAction.CallbackContext context)
 {
   if (context.ReadValue<float>() >= 1.0f)
   {
     EnteredUp = true;
   }
 }

 public void OnDown(InputAction.CallbackContext context)
 {
   if (context.ReadValue<float>() >= 1.0f)
   {
     EnteredDown = true;
   }
 }

 public void OnLeft(InputAction.CallbackContext context)
 {
   if (context.ReadValue<float>() >= 1.0f)
   {
     EnteredLeft = true;
   }
 }

 public void OnRight(InputAction.CallbackContext context)
 {
   if (context.ReadValue<float>() >= 1.0f)
   {
     EnteredRight = true;
   }
 }

 public void OnDecide(InputAction.CallbackContext context)
 {
   PressedDecide = true;
 }

 public void OnCancel(InputAction.CallbackContext context)
 {
   PressedCancel = true;
 }
}

InputActionのコードを簡略化するため、

using UnityEngine.InputSystem;

を追加します。

OutGameのActionMapのインターフェースを継承します。

public sealed class InputSystemTestForOutGame : MonoBehaviour, NewControls.IOutGameActions

インターフェース名はActionMap名により変わります。

先ほどの手順で作成したInputActionsのスクリプト内のクラスを生成し、
有効/無効化するコードを追加します。

private NewControls.OutGameActions Input { get; set; } = default;

private void Awake()
{
  Input = new NewControls.OutGameActions(new NewControls());
  Input.SetCallbacks(this);
}

private void OnDestroy()
{
  Input.Disable();
}

private void OnEnable()
{
  Input.Enable();
}

private void OnDisable()
{
  Input.Disable();
}

Action用のメソッドの引数にコールバックを追加します。
アナログ入力の場合は、入力がfloatの0~1で取得できますので、
1の時のみ入力を判定するようにします。

public void OnStart(InputAction.CallbackContext context)
{
  PressedStart = true;
}

public void OnUp(InputAction.CallbackContext context)
{
  if (context.ReadValue<float>() >= 1.0f)
  {
    EnteredUp = true;
  }
}

public void OnDown(InputAction.CallbackContext context)
{
  if (context.ReadValue<float>() >= 1.0f)
  {
    EnteredDown = true;
  }
}

public void OnLeft(InputAction.CallbackContext context)
{
  if (context.ReadValue<float>() >= 1.0f)
  {
    EnteredLeft = true;
  }
}

public void OnRight(InputAction.CallbackContext context)
{
  if (context.ReadValue<float>() >= 1.0f)
  {
    EnteredRight = true;
  }
}

public void OnDecide(InputAction.CallbackContext context)
{
  PressedDecide = true;
}

public void OnCancel(InputAction.CallbackContext context)
{
  PressedCancel = true;
}

入力をプールする変数を用意し、それをUpdate()内で処理します。

private bool PressedStart { get; set; } = false;

private bool EnteredUp { get; set; } = false;

private bool EnteredDown { get; set; } = false;

private bool EnteredLeft { get; set; } = false;

private bool EnteredRight { get; set; } = false;

private bool PressedDecide { get; set; } = false;

private bool PressedCancel { get; set; } = false;

private void Update()
{
  if (PressedStart)
  {
    Debug.Log("start");
    PressedStart = false;
  }
  else if (EnteredUp)
  {
    Debug.Log("up");
    EnteredUp = false;
  }
  else if (EnteredDown)
  {
    Debug.Log("down");
    EnteredDown = false;
  }
  else if (EnteredLeft)
  {
    Debug.Log("left");
    EnteredLeft = false;
  }
  else if (EnteredRight)
  {
    Debug.Log("right");
    EnteredRight = false;
  }
  else if (PressedDecide)
  {
    Debug.Log("decide");
    PressedDecide = false;
  }
  else if (PressedCancel)
  {
    Debug.Log("cancel");
    PressedCancel = false;
  }
}

これにより、スティック入力が連続しなくなりました。
InputActionsのスクリプトを用いる際は、PlayerInputは使用しないので、
PlayerInputコンポーネントはRemoveしましょう。

InGame側も実装してみましょう。

using UnityEngine;
using UnityEngine.InputSystem;


public sealed class InputSystemTestForInGame : MonoBehaviour, NewControls.IInGameActions
{
  //========================================
  // Properties.
  //========================================

 private NewControls.InGameActions Input { get; set; } = default;
 
 private Vector2 Velocity { get; set; } = Vector2.zero;

 private bool PressedAct { get; set; } = false;

  //========================================
  // Methods.
  //========================================

  //----------------------------------------
  // MonoBehaviour methods.
  //----------------------------------------

  private void Awake()
  {
    Input = new NewControls.InGameActions(new NewControls());
    Input.SetCallbacks(this);
  }

  private void Update()
  {
    Debug.Log(Velocity);

    if (PressedAct)
    {
      Debug.Log("act");
      PressedAct = false;
    }
 }

 private void OnDestroy()
 {
   Input.Disable();
 }

 private void OnEnable()
 {
   Input.Enable();
 }

 private void OnDisable()
 {
   Input.Disable();
 }

 //----------------------------------------
 // Event methods.
 //----------------------------------------

 public void OnMove(InputAction.Callbackcontext context)
 {
   Velocity = context.ReadValue<Vector2>();
 }

 public void OnAct(InputAction.CallbackContext context)
 {
   PressedAct = true;
 }
}

 上下左右の移動は、x, yそれぞれ-1~1のVector2が取得できます。

public void OnMove(InputAction.Callbackcontext context)
{
  Velocity = context.ReadValue<Vector2>();
}

これで、インゲーム側もInputActionsを使うことができました!

基本的にはこれで問題ないのですが、
1台のPCに複数個ゲームパッドをつなぐ場合、
入力がどのゲームパッドからかが判別つきません。
なので、Device IDを使って判別することにします。

※場合によっては接続されているゲームパッドの数が合わないことがあり、
正常に判別できない場合があります。
参考程度に留めていただきますよう、よろしくお願いいたします。

今回は、InputUtilityというstaticなクラスに、
CallbackContextからプレイヤーのインデックスを取得するメソッドを作成しました。
(LINQを使用しています)

using UnityEngine.InputSystem;

using System.Linq;


public static class InputUtility
{
  //========================================
  // Methods.
  //========================================
 
  public static int GetGamepadIndex(InputAction.CallbackContext context)
  {
    return GetGamepadIndex(context.control.device.deviceId);
  }

  /// <summary>
  /// ゲームパッドのインデックスを取得する.
  /// </summary>
  /// <param name="deviceId">ゲームパッドのデバイスID.</param>
  /// <returns>ゲームパッドのインデックス(1オリジン).</returns>
  public static int GetGamepadIndex(int deviceId)
  {
    var deviceIds = InputSystem.devices
      .Where(device => device is Gamepad)
      .Select(device => device.deviceId)
      .ToArray();

    if ((deviceIds == null) || (deviceIds.Length == 0))
    {
      return 1;
    }

    var index = System.Array.IndexOf(deviceIds, deviceId);
    if (index == -1)
    {
      return 1;
    }

    return index + 1;
 }
}
public void OnAct(InputAction.Callbackcontext context)
{
  var index = InputUtility.GetGamepadIndex(context);
  Debug.Log(index + "P");
}

ちなみに、
PlayerInputのOpenInputDebuggerを押すと、入力のテストに便利な情報が見れます。
また、Input Debugでゲームパッドをダブルクリックすると、
ゲームパッドのキー割り当てやDevice IDを見ることができます。
f:id:taipui:20200221193637p:plain

まとめ

 今回、新しいInputSystemを使用してみましたが、感想として、
InputActionsでのActionの設定は便利だな~と思いました。
旧InputManagerは、複数ゲームパッドの入力の管理が地獄でしたので・・・

まだPreview版ですが、できることは多そうですし、結構使い心地はいいので、
早く正式版になってほしいなぁ・・・と思います。

今回は私も初めて使用してみたので、もっといいやり方があるかもしれません。
参考程度にしていただければ幸いです。