Input Systemを学び直す

山村達彦氏(以下、山村):こんにちは、Unityの山村です。今回やる内容ですが、「Input System再入門」ということで、Input Systemをみなさんと一緒に学び直してみようと思います。

その前にちょっと聞きたいんですが、Input Systemって使ったことありますか? 以前は「New Input System」と言われていて、Input System自体はもう2年ぐらい前に出てきているんですけれど。「もうマスターしたよ」という人もいれば、なんとなくふわっと使っているという人もいると思いますし、実際に使ってみて、「今までのInput Managerと比べるとなんか使いにくいな」と思っている人もいるかもしれないです。

特に「なんか使いにくいな」と思ったり、「微妙によくわからん」みたいなことを思っている人、もしくはふわっと使っている私のような人のために、今回は今まで使ってきたInput Systemをもう1回学び直してみようと思います。

(スライドを示して)今回やる内容ですが、基本的にはInput Systemを使っていろいろやりますが、特にこういった動きを作ってみようと思います。

UFO君というキャラクターを作りました。これが移動したり射撃したり、ボタンを押している時にダッシュしたり。あとはメニューを出したり、キーコンフィグを出したりとか。そういったことをどうやればいいのか、Input Systemの中にいろいろな機能がありますが、これらをどういうふうに使えばいいのかという話を、実際に動かしながらやっていこうと思います。

(スライドを示して)これにあたってどんなものを使うのか大雑把に言うと、こんな感じの内容をやる予定です。「射撃する」に対しては、InputActionを使っているという話をします。その後に「移動する」部分についてもいくつか話をして、「ダッシュ」にかこつけて、よくわからないと評判のInitial State Checkの話、PassThroughとValueの違いについて話します。

「UFOの向きを変更する」ということで、実はキャラクターはマウスカーソルで動くものとGamepadで動くものとの2種類の書き方があり、そのあたりの切り替えの話もします。あとは「戦闘モードと通常モードの切り替え」や「UIの対応」、あと「キーコンフィグ」。キーコンフィグはけっこう単純なんだけどちょっと理解しにくいので、そのあたりも話していこうと思います。

Input Systemのインストール

最初にやるのは、「とりあえず動かしてみよう」という話です。この場合、Input Systemをインストールする必要があります。

どういうふうに(インストール)するのかは、マニュアルに書いてあるとおり、パッケージマネージャーに入れてインストールするだけです。

その時に、今まではInput Managerを使っていましたが、これをInput Systemに切り替える必要があります。

(スライドを示して)インポートされた内容がこちらです。今は単純にパッケージマネージャーでパッケージをインポートしただけの状態です。そこからいろいろとやっていきます。

まずは、「とりあえず動かしましょう」ということで、Input Systemが入っている状態でボタンを押してなにかする、みたいなところまでやっていきます。基本的にはInput Managerはほとんどコードで動かすシステムになっています。

(ここに)適当に作った、本当に何もないコードがあります。ここにゴニョゴニョと追加して、いろいろな動かし方を見ていこうと思います。ちなみに今回は、この場でキャラクターの移動制御と弾を撃つ制御を自作するのは大変だったので、銃を撃つコンポーネントと移動するコンポーネントを事前にもう作ってあります。

「弾を撃つ」部分を作る

まずは、とりあえず動かしましょう。最初は「弾を撃つ」部分を作ってみます。Input Systemは、いくつかのGamepadやタッチパネルを取得できるので、レフトトリガーの状態を取得して、今この場でプレスされたら弾を撃つ、みたいな。今はレフトトリガーを押していますが、これで実行すれば弾が撃てるようになります。こんな感じで作っていくことができるわけです。

例えば、マウスカーソルの左ボタンを追加して、左トリガーを押したか左ボタンを押したかみたいに作っていけば、左ボタンもしくはレフトトリガーを押すことによって弾が出るようになる。今は左クリックを押していますが、これで弾が出ています。ここまでは簡単ですね。これは「実際にこういう感じで書くこともできる」という話です。

このまま進んでいくと、デバイスが増えて抽象化するたびに、どんどん複雑になっていくというのはそのとおりです。

なので、もうちょっと次の話。プレイヤーキャラクターがやりたいこと、動かしてほしい内容を抽象化して、それに対して○ボタン、もしくはSボタンを割り当てて、キーが押されたらFireを押したことにして、Actionを行うようにしていきます。

(スライドを示して)基本的にはこちらがInput Systemでやってほしい流れですね。ということで、実際に新しいInput Systemの内容を作っていきましょう。先ほどの流れを作るためには、Input Action Assetを作る必要があります。タイミングによって出てくる位置が違うことがあるので、探すのが微妙に面倒くさいんですけど。

今回やりたいことは射撃なので、Fireを作ってLeft Triggerと左ボタンを関連付けてバインディングします。

Player Inputを設定すると、Player Inputがオブジェクトに対して、「今どんなボタンを押したよ」とメッセージを送ってくれるようになります。Send Messagesとか、いろいろ種類がありますが、自分のおすすめはC#のInvoke(Invoke C Sharp Events)、もしくはUnity Events(Invoke Unity Events)です。(今回は)C#のInvokeで行きましょう。

あとはPlayer Inputからイベントを受け取れるようにします。まずInputのコンポーネントを用意して、関連付けして。押した瞬間のイベントにメソッドを登録すると、Fireボタンが押された時にFireが呼べるようになるので、このタイミングで射撃を行う。

これで実行すると、クリック、もしくはトリガーを押すことによって弾が出るようになります。今C#のコードから見ているのはFireです。(画面を示して)Fireに対してバインディングされているのが2つのデバイスです。これが押されたタイミングでコードが呼ばれて弾が出ると。そういうことができるようになりました。

Moveを作り移動させる

次は移動をやってみましょう。Moveを追加して。今度はボタンじゃなくて上下左右キーとかスティックにしたいですね。なので、Action TypeをValueに変更して、受け取る値をVector 2、つまりX軸とY軸の2軸の値にします。

まず、左スティックを移動キーに割り当てつつ、上下左右キーも割り当てちゃいます。

先ほどと同じような感じでMoveを割り当てます。今度はPerformedでイベントの解除も登録して、メソッドも作ります。

この入力した値ですが、objのReadValueのVector2で取得します。移動なので移動方向を作り、Moveの処理に移動方向を指定して動いてもらうと。スティックを動かしてもグルグル回せます。

キーを離しても動き続けてしまう問題

ただ1個だけ問題があって、特にキーボードで動かしている場合。左を押した後、キーを離しているはずなんですけど、動き続けちゃうんですね。これにはコードの問題とInput Systemの問題の2つがあります。

Input System側の問題として、OnMoveって毎フレーム呼ばれるものではありません。(スライドを示して)これは呼ぶタイミングの図ですWaitingからスタートして、値が変化したらStarted、押した瞬間というやつが押されて、押している間はPerformedが呼ばれると思いきや、Performedは値が変化した時、もしくは入力が更新されたタイミングに呼ばれます。なのでキーボードを押した瞬間は、Performedが呼ばれるんですけれど、押しっぱなしにしている場合はそうではなくて。

つまり、最後に入力した値が常に更新され続けてしまう状態になります。キーボードを離した時にPerformedを0にしてくれるわけでもないので、動き続けてしまうようなことが起こっています。「このOnMoveは不完全だから、食べられないよ」という話になるので、actionsのOnMoveStopはちょっと手を加える必要があります。

(画面を示して)これはCanceledで、Moveを離した瞬間に呼ばれるコールバックです。このタイミングでスタートを入れて、Performedでキーの入力の値をキャラクターに反映させて、キーを離した瞬間にCanceledを呼んで、移動方向を0にしてもらう。そういうふうにキャンセル処理をやる必要があります。

Input Systemの呼び出しのタイミングって、基本的には値が変化したStarted。もしくは、押した後に値が変化したタイミングでPerformed。あとはそれを離して、通常の状態に戻った時に呼ばれるCanceled。この3つを使っていろいろやります。

特にPerformedは値が変化しない限りは呼ばれないので、例えばキャラクターの移動をOnMoveのタイミングでやるのではなく、あくまでも「このタイミングではどういう方向にキーを打ったのか」「どういうふうにボタンを押したのか」。そういう情報だけをキャッシュしておいて、アップデートやエントリーポイントのタイミングでキャラクターを動かす必要があります。

もしくは、Updateのタイミングで_input.actionsから直接ReadValueで取得もできます。

(画面を示して)今までのInput Managerはこのスタイルだったので、なじみがあって使いやすい場合もあると思います。そういう場合は今までのスタイルを使ってもらっても大丈夫です。特にマウスの位置とかはイベントのタイミングを取る時けっこう面倒くさいので、この方法でUpdateを取ってもいいとは思います。

Dashを作りダッシュさせる

移動ができたらダッシュもしましょう。とりあえず、アクションに何かデバイスを登録して、そのデバイスに登録したアクションに対してコードを登録して、イベントを登録して、そこから値を取り出すのがInput Systemの基本的な第1形態の使い方です。実はここからだんだん面倒くさい話になっていきます。

それはともかく、次はダッシュを作りましょう。これも同じく、ActionsにRight TriggerでDashを登録します。とりあえず、ここまで使えればだいたいのものはその気になればなんとかなるというか、ここからはコードでがんばればなんとかなる話ではあります。

とりあえず、Dashのイベントに登録しました。今までと同じようにDashのイベントをコードにも登録します。これはトグル式ですね。

スタートとキャンセルを両方登録していますが、この中でどちらが押されたかを判定します。これでダッシュを押した時にブーストが入って高速移動できるようになり、キャンセルしたら高速移動もキャンセルするかたちにしました。

シーンが変わった瞬間にダッシュが解除される問題

実はこれ、1個だけ罠があって。実際に動かしてみましょう。まずは問題なく動きます。ダッシュ中はすごいエフェクトが出て、高速移動できるようになります。

何が問題かというと、例えばシーンを切り替えたタイミングではダッシュキーをずっと押しっぱなしなんですけど、シーンが変わった瞬間にダッシュが解除されちゃうんです。もう1回押すとダッシュが起動する。

何が起こっているのかというと、DashのStateが初期化されていないことが問題になっています。どういうことかというと、Initial Stateが有効の時と無効の時では、ボタンの扱いが微妙に違うんです。基本的には見ているとおり、Input Systemのボタンのシステムは値が変化したタイミングで処理が呼ばれます。

Initial State Checkが入っていなかった場合や、このタイミングでゲームを開始した場合、最初から押されていた場合は、値が押された状態から変化していないので、イベントが呼ばれないことが起きます。

なので、例えばダッシュキーを押してシーンが切り替わった場合や、もしくはキャラクターを作り直した場合、Input Systemを1回初期化したなどのことが起きた場合、startのイベントが呼ばれないことになります。特にシーンが変わった瞬間、Input Systemでダッシュキーを押しっぱなしなのにその場で停止してしまうことがあるので、そこは注意する必要があります。

それをなんとかするために作られているのが、Initial State Checkという設定です。これが入っていると、起動したタイミングで初期状態をいったんチェックして、押されているかどうかを判断します。例えばDashみたいなものの場合は、Initial State Checkを付けっぱなしにしてからやらないと、シーンが切り替わったタイミングでDashがキャンセルされることがあります。

今押しっぱなしにしていますが、ちゃんとダッシュできていますね。Initial State Checkは、けっこうわかりにくい設定ですが大事なので、よく覚えておくといいと思います。

Pass Throughの設定について

わかりにくいと評判のもう1個の設定、Pass Throughについてもついでに話していきましょう。Pass ThroughはInitial State Checkの話から出てくる最大の項目の1つです。Actionタイプの中で選択できるPass Throughというものについて、いったい何なのかという話もちょっとしておきます。

何かというと、現在入力している情報をどこから持ってくるのかという話です。例えばMoveでは、今2つのデバイスが接続されていて、情報をどちらから持ってくるのかという選択をする必要があります。それをどちらのパラメーターから持ってくるのかの判断するのがPass Throughの設定です。

基本的にはあまり面倒くさいことを考えなくてもよくて、大雑把に言えば、だいたいのケースにおいては大きいほうを使います。つまりどういうことか。わかりにくいので図にしました。

(スライドを示して)上がValueで、下がPass Throughです。今は左から順に入力していくと仮定していますが、例えばスティックを思いっきり上に持ち上げた場合、Pass ThroughもValueもどっちも1を取ります。

その後に、キーボードやコントローラーで2つ目に小さい値を入力した場合、Valueはどちらかを判断して、とりあえず大きいほうを選択します。そのまま使う値は(0,1)になりますが、Pass Throughは上書きされちゃうんです。

新しい値でどんどんパラメーターを更新していきます。例えば次に(1,0)をもう1回入力した場合、値が大きいのでたぶん(1,0)が優先されて使われます。その後にもしController 1を0にした場合、今一番使われている大きな値はController 2の(-0.2,-0.2)なので、Valueはこの値が使われます。

Pass Throughは、そういうものはガン無視して0にします。基本的に、Pass Throughは後で入力されている値がどんどん使われると思ってください。例えば、ゲームパッドやキーボードが2つ接続されている状態、かつActionsにもデバイスが登録されている状態で大地震が起こった場合。

木造の家でPass Throughでやっていた場合は、そのたびに振動によって作られた新しい入力でガンガン更新されてしまうので、けっこう大変なことになりますが、Valueは特に気にせず(大きい値が)そのまま使われるという感じです。

(上記の理由から)Pass Throughという設定がありますが、基本的にはValueを使っておくと安定します。

人生を懸けてゲームをしているタイミングで大地震が起きてゲームが止められないこともあるかもしれないので、そういう人のためにもちゃんとValueを使って、どっちのボタンを使うのかを選択してもらう必要があります。Bluetoothで接続している可能性もありますしね。Starter AssetsはInput Systemをちゃんと使っていない感じがするので、そんなに気にしなくてもいいと思います。

とりあえずここまでで、UnityのInput Systemの第2形態と言っていいかな。Valueとかそのあたりの基本的な使いどころがわかってきたか思います。

先ほどあった質問にも答えておきましょう。「古いシステムからの移行」は、実はマニュアルに書いてあります。ただ基本的にはワンプッシュでいきなり移行するシステムではなくて、「こういうふうに置き換えればいいよ」という感じの話になっています。ただ、コメントの最初にあったGravityのような、今まであったけど今は使えなくなった機能もやはり存在していて、そういうところはなんとかがんばる必要があると思います。

Migrating from the old input system

(次回につづく)