稲枝の押入れ

いなえが適当なことを書いては、しまっておく場所

Unityでゲームパッドから多人数入力を受け取る方法まとめ

目次


初めに

前置き

どうも、いなえのまきです。

僕については

inaenomaki.hatenablog.com

この辺を見てもらえると良いと思いますので、自己紹介はこのくらいにしておきます。

この記事はUnityについてのものですが、僕の環境がUnity 5.4.0f3なので、それを想定して書いています。

また、初心者ゆえに間違いがある部分があるかと思いますが、その点については指摘していただけると助かります。

CCS Advent Calender

この記事はCCS Advent Calendar 2016の21日目として書かれた記事です。

前の記事 by こまつな

次の人→まっそうめん


動機

皆さん、ゲームを作るのは楽しいですよね。でも、それと同じかそれ以上に難しいとも思います。面白いゲームを作ることの難しさというのを僕はこの何年かでとても感じました。面白いゲームを作るには様々な要素について様々な視点から考え、その上で作りたいゲームのコンセプトに合致した決定をしていかないといけません。正直、大変な作業です。
これを楽にする方法の1つに、多人数プレイにしてしまうという方法があります*1。シンプルなゲームルールでも、多人数対戦等にすると良ゲーと化したりします。みんなで盛り上がりながらゲームをする楽しさというのは、単純なそのゲームの楽しさ以上のものがあると思います。
そして多人数ゲームにしてしまうと、AIの実装など、一部の実装について必要がなくなる可能性があります。開発が楽に越した事はないですよね*2
しかし、多人数プレイのゲームを作るとなると、2人ならいいですが、人数が増えるにつれてキーボードやマウスのみの操作では辛くなってきます。例えば4人で一つのキーボードを押してプレイするなんて狭くてたまったものじゃありません(遠い目)

複数人対戦型おでんゲー「ODEN FIGHT!!(仮)」*3。誰か良いタイトルください。 f:id:makiofinae:20161216205310p:plain f:id:makiofinae:20161216205306p:plain

そうなるとゲームパッド(よくあるゲームのコントローラー)で操作できるようにするのがいいと思うのですが、その多人数対戦ゲームをUnityで作ろうとすると色々と調べることになると思うのですが、少し情報が分散して手間がかかります。もしかすると人によっては諦めてしまうかもしれません。この記事は、そういった人に多人数ゲームを作ってもらえたらなという思いから、入力関係についてまとめた記事です。

その為大して新規性はありません。ただ、この記事を見れば多人数対戦ゲームが作れるように纏めたつもりです。

InputManager

InputManager

Unityでは入力関係を扱うクラスとして、Inputクラスというものが用意されています。Unityを使ったことのある方なら一度は使うであろうこのInputクラスの管理をする為に、UnityではInputManagerというものが用意されています。このInputManagerの設定等を受け、Inputクラスは動きます。ではこのInputManagerでは何が出来るのでしょうか?

仮想入力

InputManagerでは、仮想の入力を扱います(仮想の入力について、以降『仮想入力』と書きますが、これは説明がしやすいように僕が便宜的に読んでいるだけで日本語では正式にどう呼ばれているのかは僕は知りません)。急に言われてもよくわからないですね。仮想入力とは、実際の入力とゲームの入力受け付けとを結びつける概念のようなものです。どういうことかというと、例えば2DアクションゲームでZキーを押したらジャンプする様な処理をする場合、

  • Zキーを押したか→押していたらジャンプ

という風にコード上で書くのが通常かと思うのですが、これをUnityでする場合、

  • "Jump"の仮想入力があったか→あったらジャンプ

という風に書いた上で、"Jump"の仮想入力とZキーの入力とを紐付けることによって実現します。

f:id:makiofinae:20161220214729p:plain

仮想入力を使うメリット

変更が楽

これによるメリットは何か、というと、まず操作方法が変わった場合に楽に変更できる、という所にあります。 上の例だと分かりにくいので設定を増やしましょう。タイトル画面、ゲーム中のポーズ画面、ゲームオーバー時のリトライ画面でも、Zキーを押した時に決定をしたことにする、とします。 操作に使うキーやボタンというのはそのゲームに合わせて必要最小限にとどめるべきであり、また統一したゲーム操作感、プレイヤーが期待するであろうボタンを押した時の動作等を保証するためにもこの様に1つのボタンで様々なところでの操作を行う場面はあるかと思います。

さて、この様な場合にZキーでしていた動作をやっぱりXキーでするようにしよう、となったとします。もし「Zキーを押したか→押していたら処理」という方式を取っていた場合、ジャンプの処理や上記決定の処理の部分の「Zキーを押したか」の部分を「Xキーを押したか」に書き換える必要があります。こうすると書き換え漏れが起きたりといった問題が起きるかもしれません。

『"Jump"の仮想入力があったか→あったら処理』という風に書いた上で、"Jump"の仮想入力と実際のキーの入力とを紐付ける」という方式をとっていた場合、"Jump"の仮想入力に紐つけるキーをZからXに紐つけるだけで済みます。 また、仮想入力には例えばここでは"Jump"等の名前がつけられるので、その仮想入力がどういった場面で使われるものなのかがわかりやすくなります。(この点から考えると上記決定の処理については"Jump"では無い新しい仮想入力を作ってそれを割り当てるべきかもしれない。Mainとか?)

f:id:makiofinae:20161220214801p:plain

キーコンフィグが使える

そしてもう一つ、大きなメリットとして、仮想入力に紐つける実際の入力については、プログラムの起動時に出るウィンドウで設定することが出来るということです。これによって、特に用意しなくてもキーコンフィグが実現されます。実際の入力を好きな仮想入力に紐付けられるので、本来はキーボード操作を想定しているゲームであっても、ゲームパッドの入力を仮想入力に紐つけることでゲームパッドでプレイすることも可能になります。これは個人的には非常に大きなメリットかなと思います。

f:id:makiofinae:20161216175323p:plain

こんな感じのウィンドウが起動時に出て、ここで設定が可能です

実際の設定の方法

InputManager概観

仮想入力についてそれとなく話したところで、InputManagerを実際に開いてみましょう。上のバーからEdit->Project Settings->Inputをクリックします。 f:id:makiofinae:20161216174755p:plain

そうするとInspecterにInputManagerの中身が表示されると思いますAxesを開くと、中にはSizeという項目と、そこから下にHorizontal等の項目があると思います。このSizeという項目の値が、合計で何個仮想入力を作成してあるかの値です。そしてそれ以外の、Horizontal等の項目の一つ一つが、仮想入力の設定になっています。 f:id:makiofinae:20161216174757p:plain f:id:makiofinae:20161216205714p:plain

仮想入力の作り方

Sizeの値が、デフォルトで18とかになってると思うので、仮想入力を新しく1つ作りたい時は19と書き換えれば仮想入力が1つ増えます。  f:id:makiofinae:20161216181427p:plain f:id:makiofinae:20161216181431p:plain

下に一つ増えた!!

また、既存の仮想入力を右クリックする事で複製して増やしたり、逆に削除して減らすことも出来ます。右クリック->Duplicate(複製)もしくは右クリック->Delete(削除)と操作すると可能です。
f:id:makiofinae:20161216174745p:plain

ソースコード側での話

Inputクラス

さて、仮想入力の作り方が分かったところで、それをソースコードからどう使うのかについて説明したいと思います。ソースコードからはInputクラスを通じて仮想入力を取得します。Inputクラスはその名の通り入力一般を扱うクラスです。そのメンバ関数として仮想入力を取得する関数を持っているので、それを使います。入力を取得する関数には、大別して主なもののみあげると、仮想軸入力を扱う関数、仮想ボタン入力を扱う関数、キー入力を扱う関数、マウスボタン入力を扱う関数の4つがあります。正確にはまだあるのですが、本題からはそれるので気になる人は調べてみてください。さて、この中で今回は仮想軸入力仮想ボタン入力、つまり仮想入力を扱う関数について見ていきたいと思います。

仮想入力を扱う関数

仮想ボタン入力

それでは実際に見てみましょう。まず、仮想ボタン入力について考えてみましょう。そもそもボタン入力とは何か、というと押している/押していないの二つの情報をもつ入力です。つまり仮想ボタン入力とは仮想ボタンが押されているかどうかの情報をもつ入力なのですが、Unityではその仮想ボタン入力について、今押されたところであるか/押されている最中であるか/今離されたところであるか、の3つを取得することができます。それぞれ対応する関数はGetButtonDown/GetButton/GetButtonUpという名前になっています。引数にInputManagerで作った仮想ボタン入力の名前の文字列を渡してやることで、その入力がそれぞれの関数で判定される入力の状態を真理値で得ることができます。つまり

        if (Input.GetButtonDown("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押し下げられた時にする処理
        }

        if (Input.GetButton("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押されている間ずっとする処理
        }

        if (Input.GetButtonUp("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが離された時にする処理
        }

と書いて使います(GamePad1_Jumpは仮想ボタン入力名)*4

以下にそれぞれの関数がどういう動きをするかを表にしておいたので参考にしてください。

f:id:makiofinae:20161220220422p:plain

仮想軸入力

では次は仮想軸入力について見てみましょう。そもそも軸入力とは何か、というと正負どちらの方向にどの程度の入力があるかの情報を持つ入力です。単に入力があるかないかについて見るのみでなく、入力がある際にその入力の大きさ等についても見るところがボタン入力とは違います。入力軸一つごとに入力に対する数直線があると考えるとわかりやすいかもしれません。この入力軸の情報を取得するためにはGetAxis関数もしくはGetAxisRaw関数を使います。

GetAxisRaw関数は引数にInputManagerで作った仮想軸入力の名前の文字列を渡してやることで、その仮想軸入力の値を符号付き少数型で得ることができます。返ってきた値が正の値なら軸の正の方向に、負の値なら負の方向に入力があったことを意味します(一部例外あり)。これは例えばゲームパッドのアナログスティックの入力などを取得する時に使います。つまり

        if (Input.GetAxisRaw("GamePad1_X") > 0)
        {
            //ゲームパッドの移動入力のx軸の正の方向(右方向)の入力がある時にする処理
        }
        if (Input.GetAxisRaw("GamePad1_X") < 0)
        {
            //ゲームパッドの移動入力のx軸の負の方向(左方向)の入力がある時にする処理
        }


        if (Input.GetAxisRaw("GamePad1_Y") > 0)
        {
            //ゲームパッドの移動入力のy軸の正の方向(上方向)にある時にする処理
        }
        if (Input.GetAxisRaw("GamePad1_Y") < 0)
        {
            //ゲームパッドの移動入力のy軸の負の方向(下方向)にある時にする処理
        }

と書いて使ったりします (GamePad1_XとGamePad1_Yは仮想軸入力名) 。

GetAxis関数は使い方はGetAxisRaw関数と同じなのですが、返ってくる値に一部補正が入っています。例えばGetAxisRawでは、仮想入力にキーボードのキーが割り当てられていてそれらが押された場合、1・-1のいずれかの値を返しますが、GetAxis関数の場合は、スムーズに入力の値が増加/減少が行われる様に補正がかかります。これらの補正は以下の一部プロパティに影響されますが、ここでは本旨ではないので触れません。*5

その他の入力を扱う関数についても使い方が多少違いますが、これくらい簡単に使えるので公式リファレンスを確認するなどしてみてください(仮想入力を使うようになったら使用頻度は下がると思いますが…)

ちょっと楽に

慣れている人にはいらないかもしれませんが、そうでない人のために少し簡単にコードを書くヒントを。仮想入力について取得するここまでの関数は引数に文字列を渡してやります。その為、複数人の仮想入力について同じような処理をする時は

        for (int GamePadNum = 1; GamePadNum <= 4; GamePadNum++)
        {
            var ButtonName = "GamePad" + GamePadNum;
            if (Input.GetButtonDown("GamePad" + GamePadNum + "_Jump") == true)
            {
                //それぞれのゲームパッドで特定のボタンが押された時にする処理
            }
        }

例えばこんな感じにしてやるとゲームパッド1~4までについて同じ処理をすることが出来て楽かもしれないです。

(もちろんこのコードはアイデアを示すためのものなのでちゃんと自分で考えて書いてください。例えば…マジックナンバーダメゼッタイ。)

仮想入力の設定

仮想入力のInputManager上の設定

上記の方法でソースコードを書いたとしても、うまく動かないと思います。というのもまだInputManagerの方で設定をしていないからですね。仮想入力の設定の仕方を見ていきましょう。

主要な入力情報の設定

InputManagerのプロパティの説明については以下を参照してください(公式マニュアルより)

プロパティー 説明
Axes 現在のプロジェクトに定義されたすべての入力軸を含みます。「 Size 」は、このプロジェクトでの各入力軸の数で、「 Element 0、1、… 」は、修正する特定の軸になります。
Name ゲームランチャーやスクリプトを通じて軸を参照する文字列。
Descriptive Name ゲームランチャーに表示される「 Positive Button 」関数の詳細な定義。                                                                                  
Descriptive Negative Name ゲームランチャーに表示される「 Negative Button 」関数の詳細な定義。                                                                                  
Negative Button 軸に負の値を送るボタン。
Positive Button 軸に正の値を送るボタン。
Alt Negative Button 軸に負の値を送るための、代わりのボタン。
Alt Positive Button 軸に正の値を送るための、代わりのボタン。
Gravity 入力が再度中心になる速度。「 Type 」が「 key / mouse button 」の場合にのみ使用されます。
Dead 指定した数値未満の正や負の値は 0 として認識されます。ジョイスティックに便利です。
Sensitivity キーボード入力の場合、この値を大きくすると、応答時間が速くなります。値が低いと、より滑らかになります。マウス移動の場合、この値によって実際のマウスの移動差分が縮小拡大されます。
Snap 有効にすると、反対の入力を受け取った直後に、軸の値が 0 にリセットされます。「 Type 」が「 key / mouse button 」の場合にのみ使用されます。
Invert 有効にすると、正のボタンと負のボタンが入れ替わります。(正のボタンが負の値を軸に与え、負のボタンが正の値を軸に与えます。)
Type なんらかのボタンに「 Key / Mouse Button 」、マウスの移動差分やスクロールホイールに「 Mouse Movement 」、アナログジョイスティックに「 Joystick Axis 」やユーザーがウィンドウを揺すった場合に「 Window Movement 」をそれぞれ使用します。
Axis バイスからの入力の軸(ジョイスティック、マウス、ゲームパッドなど)。
Joy Num 使用するジョイスティックの選択。デフォルトでは、すべてのジョイスティックからの入力を回収します。これは、入力軸のみに使用され、ボタンには使用されません。

大体の設定については上の説明で充分かと思います。

キーの名前については以下のようなルールに乗っ取ります(公式マニュアルより)。

  • 通常キー: “a”、“b”、“c” …
  • 数字キー: “1”、“2”、“3”、…
  • 矢印キー: “up”、“down”、“left”、“right”
  • キーパッドキー: “[1]”、“[2]”、“[3]”、“[+]”、“[equals]”
  • 修飾キー: “right shift”、“left shift”、“right ctrl”、“left ctrl”、“right alt”、“left alt”、“right cmd”、“left cmd”
  • マウスボタン: “mouse 0”、“mouse 1”、“mouse 2”、…
  • (任意の)ジョイスティックボタン: “joystick button 0”、“joystick button 1”、“joystick button 2”、…
  • (特定の)ジョイスティックボタン: “joystick 1 button 0”、“joystick 1 button 1”、“joystick 2 button 0”、…
  • 特殊キー: “backspace”、“tab”、“return”、“escape”、“space”、“delete”、“enter”、“insert”、“home”、“end”、“page up”、“page down”
  • Function キー: “f1”、“f2”、“f3”、…

1つ注意が必要なのは、joystickは1からでbuttonは0からということです。

キーボードから仮想軸入力を実現したいのであれば、Positive ButtonもしくはNegative Buttonプロパティに上記のルールに則ってキーを書く事で可能です。

また、

複数の軸を同じ名前で作成することができます。入力軸を取得するとき、もっとも絶対値が大きい軸が返されます。これによりひとつ以上の入力デバイスを軸の名前に割り当てることができます。例えば、ひとつの軸をキーボード入力として作成して、ひとつの軸をジョイスティック入力として同じ名前で作成します。もしユーザーがジョイスティックを使用していれば、入力はジョイスティックから来て、そうでなければキーボードから来ます。これによってどの入力から来ているのかスクリプトを書くときに考慮する必要がありません。

とのこと(公式マニュアル)なので、「ユーザーに環境を強制したくない!」かつ「キーコンフィグでいちいち設定させずとも初期設定でできるようにしておきたい!」という人は、これを使ってゲームパッドとキーボードどちらからでも入力を受け付けるようにしてみるといいかとおもいます。

これで仮想入力の扱い自体は出来ると思います。後は複数の入力を受け付けることが出来ればもう複数人でのゲームを作ることが出来ますね。

参考までにどんな感じの設定にすればいいかの例を載せておきます

  • X軸の仮想入力 f:id:makiofinae:20161216180408p:plain

  • Y軸の仮想入力:Invertにチェックを入れないと入力が反転してしまう事に注意 f:id:makiofinae:20161216180512p:plain

  • ボタンの仮想入力:Positive Buttonにjoystick 【ゲームパッドの番号】 button 【ボタンの番号】という書式で記述することに注意。後述する様にTypeも軸入力とは違う。この場合Axisプロパティは特に意味はない。 f:id:makiofinae:20161216180656p:plain

複数入力

仮想入力を使って複数のゲームパッドからの入力を受け付けるのは、Joy Numプロパティを設定して行います。例えば一つ目のゲームパッドであるゲームパッド1での入力(通常はプレイヤー1の入力として扱う)の入力を見るには、Joy NumをJoyStick 1にします。後は参加を考えているプレイヤーの数だけこの仮想入力を作ってやれば終了です。ここで楽に操作するために、仮想入力は、

  • プレイヤー1の仮想入力
    • X
    • Y
    • Jump
  • プレイヤー2の仮想入力
    • X
    • Y
    • Jump ...

としていくよりも f:id:makiofinae:20161216182013p:plain

  • X
    • プレイヤー1
    • プレイヤー2 ...
  • Y
    • プレイヤー1
    • プレイヤー2 ...
  • Jump
    • プレイヤー1
    • プレイヤー2 ...

f:id:makiofinae:20161216182025p:plain

としていく方が良いかもしれません(伝わって)。

というのも、InputManagerは仮想入力を増やす方法として新規に仮想入力を1から作る複製するかの方法がありますが、1から作るとなるとプロパティについてすべて設定しなければなりません。複製すると、その点プロパティについてはコピーされるので設定の必要がなく、これによってプレイヤー毎に入力の感度等が異なるといった事態を防げます。すると複製をしたいわけですが、複製して作られた仮想入力は、その複製元の仮想入力の下に作られます。そうなると、プレイヤーごとにまとめるよりも、複製がしやすいように入力の種類ごとにまとめたほうが楽です。そうすると、プレイヤーの増加に合わせて仮想入力を増やす時にも他のプレイヤーの仮想入力をDuplicateしてからNameとJoy Numを書き換えるのみで増やすことができます。

もし5人プレイを出来るように仮想入力を増やすことになったら、Duplicateして… f:id:makiofinae:20161216181140p:plain

Nameを変えて… f:id:makiofinae:20161216181225p:plain f:id:makiofinae:20161216181234p:plain

Joy Numも変える!! f:id:makiofinae:20161216181255p:plain f:id:makiofinae:20161216181302p:plain

注意事項

関数を使うにあたって

上のGetButtonDown/GetButton/GetButtonUp関数やGetAxis関数は受け取る仮想入力のTypeが合っていないと正常に動作しません
つまり、GetButtonDown/GetButton/GetButtonUp関数を使う時はそれに渡す仮想入力のTypeプロパティを"Key or Mouse Button"に、GetAxis関数を使う時はそれに渡す仮想入力のTypeプロパティを"Joystick Axis"にしておく必要があります。

入力の検知のタイミング

UnityのUpdate系関数には Update/LateUpdate/FixedUpdateの三種類があります。簡単に説明すると、

  • Updateは御存知の通り毎フレーム呼ばれる関数で、
  • LateUpdateは毎フレームのUpdateが呼ばれた後に呼ばれる関数で、
  • FixedUpdateは固定のインターバル(デフォだと0.02秒)毎に呼ばれる関数です。

それがどうかしたの?って感じかもしれませんがこれについても軽く注意をしておかないと初心者はずっこけます。
まず前提として、仮想入力の管理については、上記関数が呼び出された時に実際に仮想入力があるか等の確認をするのではなく、一定の間隔毎に仮想入力があるか等の確認をして、その情報を保持しておき、それについて上記関数が呼び出されると見にいくような作りになっています。つまり、上記関数は、仮想入力についての情報を取得しているのですが、その情報というのは関数を呼び出したその時の情報ではなく、最後に仮想入力について更新された時の情報について見ているのです。 ではその更新とはいつされるのか、というとUpdate関数が呼ばれる時です。 ある意味メモ化とも言えるのでしょうか?毎フレーム呼んでいたら少し違う気もしますが…

f:id:makiofinae:20161220215755p:plain

つまりは、例え実際に入力がなされたとしても、Update関数が呼ばれるまでは上記関数でその入力については検知できないのです。つまり - 実際の入力中に更新(Update関数)→仮想入力関数の使用 とやれば問題はないのですが - 実際の入力→仮想関数の使用→更新(Update関数) となると、仮想関数は実際の入力について取得することは出来ないのです。 でもそんな場合あるの?毎フレームUpdate関数は呼ばれるし関係なくない?と思うかもしれません。そのUpdate関数が「毎フレーム」呼ばれるというのがミソなのです。毎フレームとはつまりは画面の描画が変更される毎、ということですが、この画面の描画の変更は何も一定で動いているわけではありません。所謂FPSといわれる描画間隔は、処理の重さ等々の事情により変動します。となると、極端な話、1フレーム更新するのに10秒かかることもありえるわけです。そうなると、その10秒の間にボタンを押してから一瞬で離した場合、更新時にボタンは押されていないので入力は検知されません。これについては仮想入力の仕様上避けられないと思うのでそもそも1フレーム10秒もかかるような処理を書かないようにする他ないのですが、これ以外で問題となる場合が出てきます。

それが最初に書いたFixedUpdate関数を使うときなどです。FixedUpdate関数は呼ばれる間隔が一定です。これはどういうことかというと、FixedUpdate関数が1回目呼ばれてから2回目呼ばれるまでの間に、Update関数が2回以上呼ばれることも有り得れば、1回も呼ばれないということも有り得るということです。

f:id:makiofinae:20161220220823p:plain

ここからは言葉で言うよりコードを見たほうが早いかもしれません。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {//画面描画が更新される度に呼ばれる
     //このタイミングで実際には仮想入力情報の更新が行われている
    }

    void FixedUpdate()
    {//固定の時間間隔毎に呼ばれる
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

Numは値を保持するint型の変数です。このように、ボタンが押される度に値をインクリメントする(つもりの)処理をFixedUpdate関数内に書くとします。

ここでGamePad1_Add入力がされ、Update関数が呼ばれて、まさに今 GamePad1_Add入力がなされた、と検知されたとしましょう。そしてその後処理が重くなってFixedUpdate_1FixedUpdate_2FixedUpdate_3Updateという風に呼ばれたとしましょう( FixedUpdate の後ろについている_1等はそれぞれを区別するために便宜的につけたものです)。     そうすると、まず今まさに入力がなされた、という情報が保存されているのでFixedUpdate_1ではNum++;が実行されます。その次の FixedUpdate_2においても、保存された情報の更新がないので今まさに入力がなされたものとしてNum++;が実行されます。 その次の FixedUpdate_3においても同じです。その後Updateにはいって、実際の入力をチェックして、「今まさに入力がなされた」という評価から、「入力が押し下げ状態である」若しくは「入力がなくなった」という評価に、更新されます。

さて、そうするとどうなるか。実際には GamePad1_Add入力は1度しかされていないにも関わらず、Numは3増加してしまいました。これは意図しない動作ですね。

この例とは逆に FixedUpdate関数が1回目呼ばれてから2回目呼ばれるまでの間に、Update関数が複数回呼ばれることもあります。この場合「今まさに入力がなされた」という評価がなされた状態でFixedUpdate関数に至らず、Numが増加しません。これも意図しない動作ですね。

つまり、FixedUpdate関数内に入力判定の処理を書くと意図しない動作をすることがある*6ということがわかります。

f:id:makiofinae:20161220232952p:plain 表にしたほうがわかりにくいのではという良い例(悪い)

…ここまでだと「いや、言われなくてもしねーよ」と思うかもしれません。どこでしてしまう可能性があるかというと、当たり判定を見た上で入力を見るような場合です。

Collision系の物理演算はFixedUpdateと同タイミングで呼び出されて更新されます。つまり仮想入力の更新のタイミング(毎フレーム)と物理演算の更新のタイミング(固定インターバル毎)が違うというわけですね。そうすると、さっきの場合とは反対に物理演算については更新がFixedupdateでされる為、物理演算に関する処理についてはUpdate関数に書くのではなく、FixedUpdate関数内に書くべきであるということです。これはさっきの仮想入力と同じ理屈ですね。呼び出し間隔の異なる2つの関数内のどちらかで更新される情報を取得する関数について、もう一方の関数内で扱うというような使い方をしていると意図した動作をしない可能性があるということです。

そうなると困ったことが出てきます。ここまでの話をまとめると、Input(仮想入力)に関する処理はUpdate関数内に書くべきで、物理演算に関する処理はFixedUpdate関数内に書くべきということなのですが、もし「入力と物理演算について同時に見たい」となったらどうしたら良いのでしょうか?例えばオブジェクト同士が重なっている時に決定キーを押すとスタート、と言った処理や、ボタンを押されている間に接触したオブジェクトを見てそれを破壊する、といった場合などです。これはもう少し実際の製作に近いところから言うと、物理挙動イベント関数(OnCollisionEnter、OnCollisionStay、OnCollisionExit、OnTriggerEnter、OnTriggerStay、OnTriggerExitなど)内で入力を見る処理を書くと、物理挙動イベント関数がFixedUpdate関数で更新されるので上記問題が発生し、意図した動作をしない可能性があるということです。

これが正しい解決策かはわからないのですが、僕は一つの方法としてbool型の変数を作ってそこを経由するという方法を使っています。 まず、上でさっき問題があると言ったコードをもう一度見てみましょう。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        //このタイミングで実際には仮想入力情報の更新が行われている
    }

    void FixedUpdate()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

このコードは入力についてFixedUpdate内で呼び出しているのが問題でした。というわけで入力周りの処理をUpdate関数に移しましょう

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

    void FixedUpdate()
    {

    }

では、何かと重なった状態の時にボタンが押されるたび値を増加させるとして、 OnTriggerStay関数を追加してみましょう。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {

    }

さて、追加しましたが、この OnTriggerStay関数はFixedUpdateと同じく等間隔で呼び出されるので、この中に直接仮想入力についての処理を入れるわけにはいきません。そこでbool型の変数を使って状態だけ保持してみます。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    bool IsOnStay = false;
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true && IsOnStay == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
            IsOnStay = false;
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {
        IsOnStay = true;//状態を更新
    }




この例だとbool型の変数で保持するのは当たり判定の方ですが、別に仮想入力の方を保持してもいいと思います。つまり、

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    bool IsButtonDown = false;
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            IsButtonDown = true;
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {
        if (IsButtonDown == true)
        {
            Num++; //ボタンが押されるたび値を1増加

            IsButtonDown = false;
        }
    }

とするということです。ただし、この2つは同じ意味ではないことに注意しないといけません。実際に値を増加するタイミングが、フレーム依存の変化するものか、固定のインターバルによるものかが変わってくるからです。

ただし、この回避法は僕がとりあえず出来るようにと即席で使った方法にすぎないので、もっと良い書き方があると思います。もしかするとこれでも意図しない動作が起きるかもしれません。そのため、利用する前にもっといい方法が無いか考えて、もし思いついたら教えて欲しいです(切実)

Thanks for reading!!

最後に他人任せのようなことを書いて申し訳ないですが、こんな感じで僕の説明は終了です。駄文ゆえに苦労をして読んだ人も多いと思いますがここまで読んでくださったことに感謝します。

最後に後からこれをもう一度見るのも面倒だろうと思うので、まとめをしておきます。忘れた時に見返すのにでも使ってもらえると幸いです。

まとめ

  • 特定のキーそのもので入力を検知するのではなく、仮想入力を使おう
    • 仮想入力には仮想軸入力と仮想ボタン入力がある
  • InputManagerは「 Edit->Project Settings->Input 」で開ける
    • 仮想入力の値の設定、新規追加、複製、削除ができる
    • JoyNumと名前の異なる仮想入力を作ることで複数の入力に対応できる
  • 仮想入力について取得する関数は GetButtonDown/GetButton/GetButtonUp関数やGetAxis関数がある
    • InputManagerで仮想入力のプロパティのTypeがそれぞれの関数に対応するものになっていないと使えない
  • Inputが更新されるタイミングに気をつけよう
    • 入力はUpdateと同じタイミングで更新される
    • FixedUpdateに処理を書くのはやめよう
    • 物理挙動イベント関数についてはFixedUpdateと同じく等間隔に更新されることに気をつけよう
      - 物理挙動イベントとInputを同時に使いたいのであれば、bool型の変数を使うと良いかも?
      

おまけ

応用的なもの

初心者にはちょっと難しいかな?と思って触れなかったものについてリンクだけ張っておきます。興味があったらどうぞ!

InputManagerにスクリプトからアクセス

まあこれはしたいと思うので…参考までに

InputManagerを4人分手作業で設定していくのはまずありえないと思ったので

わかる(白目)

参考リンク

Unity公式ページ

一般的なゲーム入力

Input

InputManager

Qiita

UnityのInputで入力を扱う

個人ブログ

Unityで複数コントローラー

【理解した!】スマホ向けUnity スクリプト実行順の謎

Unity 〜UpdateとFixedUpdateの違い〜

Update()とFixedUpdateの使い分け [Unity]

他にもあった気がするけどメモし忘れた…

*1:もちろん多人数プレイのゲームにすれば必ずしも簡単に面白いゲームを作れるというわけではないですが…

*2:本当に楽になるかはさておき

*3:唐突な宣伝

*4:if内の==trueは初心者にわかりやすいように書いただけのものなので特に気にしないでください

*5:気になる人はGravityやSensitivityの値をいじってみると良いかも

*6:実はもっと広くFixedUpdate関数内に物理判定に関係する処理以外の処理を書くと意図しない動作をすることがあるといえます