アクションマップを使用してゲーム動作に対してボタンを割り当てる
検証環境
- Windows
-
- Windows 11
- Unity エディター
-
- 2020.3.25f1
- 入力システムパッケージ
-
- 1.2.0
この Tips の前提設定
この Tips の説明の前提として以下の設定を事前に行っています。
アクションマップについて
キーボードやマウス、ゲームパッドにおけるユーザー入力プログラムでは、あるボタンを押したときに特定の行動をする、という記述が基本でした。 アクションマップでは例えば「ジャンプする」という行動を定義しておいてそこにコントローラーのボタンやキーボードのキーを割り当てる、という形で設定することができます。 これにより、プログラムでは特定の行動を行ったときの処理を記述すればよく、 後付けで別のコントローラーのボタンを割り当てたとしても動作プログラムを変更することなく適用することができます。
アクションマップの作成
ここではアクションマップを作成してユーザーの入力情報をテキストに表示させてみたいと思います。 アクションマップを利用したユーザーの入力情報の取得方法についてはいくつか存在します。
プロジェクトの任意のフォルダを右クリックして「Input Action」を作成します。
作成するフォルダの場所は任意ですがプロジェクトに合わせて管理してください。
ファイル名についても任意ですがここでは InputActionSample
としています。
作成したファイルをダブルクリックすると以下のようなウィンドウが表示されます。
まずは Action Maps の + ボタンをクリックしてアクションマップを作成します。 作成単位としては場面場面で操作の内容が変わるのであればその単位で定義する形となります。 例として横スクロールアクションゲームであればアクション中とメニュー中で操作の内容がかわるのでそれぞれでアクションマップを作る形となります。
ここでは例として横スクロールアクションという定義で名前を「SideScrollActionMap」とします。
次に Actions を作成します。サンプルなので多くは作りませんがここでは移動のための「Move」と攻撃のための「Attack」のアクションを作成します。 すでに1つ作成されているので名前を変えたり、新しく作る場合は右上の + ボタンをクリックして入力してください。
最初に Move の設定を行います。 コントローラーの想定としてはスティック、十字キー、キーボードのカーソルキーを使うことを想定します。 横スクロールアクションだと左右のみの場合もありますが、ここでは上キーでジャンプ、下キーでしゃがみを考慮して4方向使う想定とします。
Move を選択すると右に Action Type の選択があるのでこれを「値」にします。
すると下に Control Type が表示されるので Vector2 を選択します。これは上下を Y, 左右を X に割り当てるためです。
次にこの行動に割り当てるキーを選択します。真ん中の「No Binding」を選択して右の Path を選択します。 ここでは GamePad の LeftStick を選択します。
すると Move に GamePad の LeftStick がバインドされます。
他のコントローラーもバインディングさせるには Move の右の + ボタンから「Add Binding」を選択します。
No Binding が追加されるので今度は GamePad の Dpad を割り当てます。
このようにして対応させるコントローラーの種類とキーやスティックを追加していきます。 特定のゲーム機専用に設定することも可能です。
Stick や Dpad は上下左右前提のボタンなのでこれで追加可能ですがキーボードの場合はすべて単一キーなので上下左右の定義がありません。 キーボードの上下左右を設定するには + ボタンから「Add Up Down Left Right Composite」を選択します。
すると以下の図のように 2D Vector が追加され Up Down Left Right それぞれに割り当てができるようになります。
例えば Up ならキーボードの 「Up Arrow」を設定します。 ちなみにキーを探すのが面倒な場合は「Listen」ボタンをクリックした状態で対象のキーを押すと簡単に選択できます。
Up に Up Arrow が設定されました。
同様に Down, Left, Right も設定すれば完了です。
もちろんカーソルキーだけでなく WASD なども設定可能です。
次に Attack の設定を行います。Attack は単一ボタンなので割り当てるのは簡単です。 まず Attack を選択して Action Type がボタンになっていることを確認します。
次に No Binding を選択して Path から割り当てたいボタンを選択します。
他にも追加したい場合は + ボタンから「Add Binding」を選択します。
後は必要な数だけ追加して下さい。ボタンとして扱うのでキーボードもゲームコントローラーと同じように設定できます。
全ての設定が終わったら「Save Asset」をクリックして保存します。 このウィンドウは閉じて構いません。
最後にプロジェクトの inputactions ファイル (ここでは先ほど作成した InputActionSample ファイル) を選択している状態でインスペクターにある「Generate C# Class」にチェックを入れてください。 パラメータが追加されますがそのまま「適用する」ボタンをクリックします。
すると同名のスクリプトファイルが生成されます。 これはプログラムからアクションマップを使用するのに便利なクラスが含まれます。
入力情報の受け取り方
アクションマップを基に入力情報を受け取る方法はいくつかあります。 本 Tips では3パターン説明していますが、実際にゲームを作る際はどれか1つに絞った方が良いでしょう。 バラバラに使ってしまうと管理が面倒になってしまいます。
また、1つのシーンで複数の入力受け取り方法を使うと内部的に処理が競合して正しく動作しない場合があります。
Send Messages で入力情報を受け取る
ここでは1つ目として「Send Messages」で入力情報を受け取る方法について説明します。
今回は入力した情報をテキストに表示させてみたいのでテキストオブジェクトを配置します。
また、本 Tips では複数の入力情報取得を試すので、別途コンポーネントをセットするための空オブジェクトを作成します。 名前はなんでもいいです。
次に空オブジェクトに「Player Input」コンポーネントを追加します。 「Player Input」はアクションマップとスクリプトを繋ぐための重要なコンポーネントです。
「コンポーネントを追加」で「入力」カテゴリに「Player Input」があるのでそれを追加します。
Player Input コンポーネントが追加されたら「Actions」に作成したアクションマップをセットします。 プロジェクトからドロップするか右にある + アイコンから選択してください。
Default Map がアクションマップで作成したものになっているか確認します。
Behavior が「Send Messages」になっていることを確認します。
次にスクリプトを作成します。ファイル名はなんでもいいですがここでは InputSendMessage
とします。
スクリプトは以下のようにします。
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class InputSendMessage : MonoBehaviour
{
<summary>情報を表示させるテキストオブジェクト。</summary>
[SerializeField] private Text TextObject;
<summary>
Move アクションが実行されたときに呼ばれる。
</summary>
<param name="inputValue">入力量。</param>
public void OnMove(InputValue inputValue)
{
var vec = inputValue.Get<Vector2>();
TextObject.text = $"Move:({vec.x:f2}, {vec.y:f2})\n{TextObject.text}";
}
<summary>
Attack アクションが実行されたときに呼ばれる。
</summary>
public void OnAttack(InputValue inputValue)
{
TextObject.text = $"Attack:{inputValue.isPressed}\n{TextObject.text}";
}
}
- 該当オブジェクトに Send Messages を設定している Player Input がアタッチされている
- MonoBehaviour を継承している
の条件を満たしている場合、「OnXXXXXXXX」というメソッドを定義すると、 指定したアクション操作を行ったときに対象メソッドが呼ばれるようになります。 「XXXXXXXX」にはアクションマップで作成した Actions の名前を指定します。 ここでは「Move」と「Attack」の Actions を作成しているのでそれぞれ「OnMove」「OnAttack」というメソッド名になります。
OnMove
の引数に設定されている InputValue
から入力されている量を取得することができます。
Control Type は「Vector 2」にしているので入力値は InputValue.Get<Vector2>
で受け取る形になります。
OnAttack
も同様に InputValue.isPressed
で押しているかどうかを取得することができます。
スクリプトを保存したら Player Input コンポーネントを持つオブジェクトにアタッチします。 表示用テキストオブジェクトもセットしておきます。
ゲームを実行して見てください。 今回ゲームパッドやキーボードの定義を入れているのでどちらを操作しても動作するはずです。
動かしてみると分かりますが、直前の状態から値に変化のあった場合のみメソッドが呼ばれることが分かります。
例えば、スティックを左に動かしている間はひたすら OnMove
が呼ばれますが左に倒しきる(-1,0)と OnMove
は呼ばれなくなります。
Attack ボタンも押した瞬間のみ反応し押しっぱなしにしてもメソッドは呼ばれません。
なので使い方としては OnXXXXXXXX が呼ばれたタイミングでゲーム処理を行うのではなく入力内容のみを保持しておきゲームの Update 処理でそれらの値を使う、というのが理想かと思います。
ちなみに現状態ではボタンを離したタイミングでは OnAttack
は呼ばれないのでボタンを離した判定をすることができません。
これに対応するにはアクションマップの設定でボタンを定義している Attack アクションを選択し、「Interactions」から「Press」を追加します。
その後追加された Press の Trigger Behavior を「Press And Release」に設定して保存してください。
実行するとボタンを離したタイミングでも OnAttack
が呼ばれることを確認できます。
isPressed
も false
になるので離したタイミングかどうかを判定することも可能です。
ちなみにこの Interactions は次以降では使わないので削除しておいてください。
Invoke Unity Events で入力情報を受け取る
2つ目の入力情報の受け取り方法として Invoke Unity Events があるのでこれを試してみます。 前述のとおり複数の入力方法を使用すると処理が競合する可能性があるので、他の処理が有効になっている場合はいったん無効にしてください。
まずは入力情報を表示できるようにテキストオブジェクトを配置します。
Invoke Unity Events 関連の処理を行う空オブジェクトを作成します。
空オブジェクトに「入力 > Player Input」を追加します。
Actions に作成したアクションマップファイル (ここでは InputActionSample) を設定、Default Map に作成した Action Map (ここでは SideScrollActionMap) を設定します。 Behavior を「Invoke Unity Events」に設定します。
スクリプトを作成します。名前は任意ですがここでは InputInvokeUnityEvents
とします。
スクリプトは以下のようにします。
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class InputInvokeUnityEvents : MonoBehaviour
{
<summary>情報を表示させるテキストオブジェクト。</summary>
[SerializeField] private Text TextObject;
<summary>
Move 操作を行ったときに呼ばれる。
</summary>
<param name="context">コールバック内容。</param>
public void OnMove(InputAction.CallbackContext context)
{
var vec = context.ReadValue<Vector2>();
TextObject.text = $"Move:({vec.x:f2}, {vec.y:f2})\n{TextObject.text}";
}
<summary>
Move 操作を行ったときに呼ばれる。
</summary>
<param name="context">コールバック内容。</param>
public void OnAttack(InputAction.CallbackContext context)
{
var value = context.ReadValueAsButton();
TextObject.text = $"Attack:{value}\n{TextObject.text}";
}
}
Send Messages の時と同じようにユーザーが操作した時に呼ばれるメソッド名を OnMove
, OnAttack
にしていますが、
Invoke Unity Events ではこのメソッド名は自由に設定することができます。
それぞれ呼ばれたときに引数として InputAction.CallbackContext
が渡されますのでそこから入力状態を取得することができます。
アクションで「値」を設定している場合は ReadValue
メソッド、「ボタン」の場合は ReadValueAsButton
メソッドで受け取れます。
スクリプトを保存したら Player Input を設定しているオブジェクトにアタッチし、表示用テキストオブジェクトをセットします。
次に Player Input の「イベント」「アクションマップ名 (ここでは SideScrollActionMap)」を展開すると作成したアクション「Move」「Attack」が表示されるはずです。
まずは Move の + ボタンをクリックして追加します。
左下のオブジェクトには自身のオブジェクトを設定し、Function には先ほど作成した OnMove
メソッドを設定します。
Attack イベントも同様に設定してください。
ゲームを実行し動作を確認してください。
基本的には Send Messages と同じ値が変化したときにだけ呼ばれますが、なぜか同じタイミングでメソッドが2回呼ばれることがあります。 原因は分かりませんが、おそらく開始処理 (started) と継続処理 (performed) が同時に実行されているからではないかと思います。 ですが Send Messages の時と同様に入力した値だけ保持して実際のゲーム処理は別途 Update 処理で行うようにすれば問題ないと思います。
自動生成したスクリプトを使用して入力情報を受け取る
3つ目はアクションマップファイルから生成したスクリプトを使用して入力情報を取得する方法について説明します。
他の取得処理と競合する可能性があるので他の取得処理は無効にしておいてください。
入力情報を表示するテキストオブジェクトを配置します。
また、入力情報取得用の空オブジェクトを作成しておきます。 今回は自動生成したスクリプトを使用するので Player Input を追加する必要はありません。
アクションマップから自動生成したスクリプトはあくまでもライブラリ的なものなので別途制御スクリプトを作成します。
名前は任意ですがここでは InputScript
とします。
スクリプトは以下のようにします。
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class InputScript : MonoBehaviour
{
<summary>情報を表示させるテキストオブジェクト。</summary>
[SerializeField] private Text TextObject;
<summary>アクションマップから自動生成されたクラス。</summary>
private InputActionSample _actionMap;
private void Awake()
{
// 各操作を行ったときに呼ばれるイベントを設定する
_actionMap = new InputActionSample();
_actionMap.SideScrollActionMap.Move.performed += context => OnMove(context);
_actionMap.SideScrollActionMap.Attack.performed += context => OnAttack(context);
}
private void OnEnable()
{
// このオブジェクトが有効になったときにアクションマップを有効にする
_actionMap.Enable();
}
private void OnDisable()
{
// このオブジェクトが無効になったときにアクションマップが余計な動作を起こさないように無効にする
_actionMap.Disable();
}
<summary>
Move 操作をした時に呼ばれるメソッドです。
</summary>
<param name="context">コールバックパラメータ。</param>
public void OnMove(InputAction.CallbackContext context)
{
// Move の入力量を取得
var vec = context.ReadValue<Vector2>();
TextObject.text = $"Move:({vec.x:f2}, {vec.y:f2})\n{TextObject.text}";
}
<summary>
Attack 操作をした時に呼ばれるメソッドです。
</summary>
<param name="context">コールバックパラメータ。</param>
public void OnAttack(InputAction.CallbackContext context)
{
// Attack ボタンの状態を取得
var value = context.ReadValueAsButton();
TextObject.text = $"Attack:{value}\n{TextObject.text}";
}
}
アクションマップから自動生成されたクラス InputActionSample
をフィールドに定義します。
このクラスにはアクションマップで設定した各アクションが定義されており、それらのアクションを行ったときに呼ばれるイベントなどを設定することができます。
Awake
メソッド内では InputActionSample
のインスタンスを生成しアクション時に呼ばれるイベントを設定しています。
これらの操作を行ったときに OnMove
, OnAttack
メソッドが呼ばれるようになります。
ただしここではイベントを設定しただけなので OnEnable
が呼ばれたタイミングで Enable
メソッドを呼んでアクションマップを有効にする必要がります。
また、ユーザーの入力動作はグローバル的な動作として扱われるので、
このオブジェクトが無効になった後にアクションマップが余計な動作をしないように OnDisable
メソッド内で Disable
メソッドを呼んで無効にしています。
スクリプトを保存したら作成した空オブジェクトにアタッチし、表示用テキストオブジェクトをセットします。
ゲームを実行し動作を確認してください。
操作してみると分かりますが、Move 操作が (0, 0) になったタイミングでは OnMove
メソッドが呼ばれません。
理由はよくわかりませんが、performed のイベントでは実際にキー入力が有効になっているものしか受け取らないようです。
ちなみに Attack についてもアクションマップで Interactions を設定していなければボタンを離したタイミングでは OnAttack
は呼ばれません。
これに対処するには canceled
イベントを設定する必要があります。
(0, 0) の時に特別な処理を行わないのであればそのまま OnMove
メソッドを呼ぶ形で構いません。Attack も同様です。
private void Awake()
{
// 各操作を行ったときに呼ばれるイベントを設定する
_actionMap = new InputActionSample();
_actionMap.SideScrollActionMap.Move.performed += context => OnMove(context);
_actionMap.SideScrollActionMap.Attack.performed += context => OnAttack(context);
_actionMap.SideScrollActionMap.Move.canceled += context => OnMove(context); // 追加
_actionMap.SideScrollActionMap.Attack.canceled += context => OnAttack(context); // 追加
}
実行して Move:(0, 0) や Attack:False が表示されることを確認してください。