1. はじめに
この解説は、そらまめゲームスが開発中の3D謎解きゲーム「Labyrinth(仮題)」の実装方法を解説したものです。
Labyrinthではゲームのシステム部分のUE4プロジェクトを公開しております!私たちの方針はノウハウの全公開ですので、少しずつですが実装方法をご紹介していこうと思います。
あくまでLabyrinthでの実装事例を報告したもので、推奨される実装方法とは限りません。
難しい実装は何一つしていませんのであまり期待をしないでくださいね!
実装を参考にするのは自己責任でお願いします。
もっと良い実装方法があれば是非教えて下さい!質が上がれば皆が幸せになります!(主に私が)
2. 要件定義
Labyrinthでは、以下の操作を使用します。
- 1本指でのタッチ、タップ、スワイプ、ロングタッチ
- 2本指でのタッチ、スワイプ、ピンチ
また、開発用にマウスでの以下の操作も実装します。
- 左クリックと右クリックがそれぞれ別の指として認識すること
- 左クリックと右クリック同時押しで2本指操作が出来ること
- ピンチ操作が出来ないのでマウスホイールで対応すること
私は普段iPhoneで実機動作確認をしていますが、エディタ上で正しく動いていても、実機上だと細かいところで動作が違ったり、受け取れる値が変わったりします。その点も、私が把握している範囲で補足しています。
3. 解説
3.1. 全体像
スマホのタッチを検知する大元はPlayerControllerブループリントです。
ゲーム側で使いやすい形になるように、PlayerControllerの中で良い感じに入力を処理してあげます。
ゲーム側は入力に関する難しいことには関与せずに、タップしたりスワイプしたときだけ教えてもらうのが優しい実装かと思います。
3.2. 入力
タッチ入力のノードはInput Touchを使用するのが良いと思います。
押し始め(Press)、押しながら動かした時(Move)、離した時(Release)にこのノードが呼ばれ、タッチした座標とタッチIDを取得することができます。
それぞれのイベントごとに内部でいい感じの判定をしてやることで、タップやスワイプなど結果だけをイベントディスパッチャーを使ってゲーム側に通知してあげます。
3.3. 押し始めの処理
入力情報をいい感じに処理するために、タッチ情報を構造体に保管しておきます。同時タッチ数分、用意する必要がありますのでここでは10点タッチまでを想定して配列化したものを使用します。押し始め時の座標や時刻を保管しておくことで、後々のタップ判定で使う事ができます。
また、いま登録した構造体の配列を調べて、同時タッチ数を更新しておきます。
なお、タップの判定は押し始めではなく、離した時に判定します。
ここでは、「押したよ!」というだけのイベント「Press」を呼び出します。
3.4. 押しながら動かした時の処理
押しながら動かした時にはMoveが呼ばれます。逆に言うと、押しながら動かさない限りはMoveが呼ばれず、Press入力のまま、何も呼ばれない状態が続きます。
先ほどの構造体配列を、Moveの情報で更新します。
過去の座標との差分から1フレームの移動量を取得することができます。また、押し始め時の座標を保管しておいたので、押し始め時の座標からの移動量も取得することができます。
iPhone実機で動作検証した際に、後述のRelease入力(離した)の後にMove入力が呼ばれることがありました。そのままタッチ情報を上書きすると、離しているはずなのにMoveの状態になってしまいバグの原因となります。
Moveの情報で更新する際は、タッチ状態がReleaseになっていない時だけ更新するようにします。
現在の指の本数によって、1本ならば「Swipe」、2本ならば「TwoFingerSwipe」のイベントを呼び出します。
3.5. 離したときの処理
離した時には単純に「Release」イベントを呼び出すことと、タップのための判定をしています。
タップの成立条件として、「押していた時間が◯秒未満だった」「押している間に大きく座標が変化しなかった」ことを条件としました。
これが正解ということではなく、Labyrinthではこれでいい感じになったので良しとしています。
Releaseの入力が呼び出された際に、離した時の座標情報が(0,0,0)で入ってくることがあり、これを使用するとタップの判定がうまくいきません。
Release時の座標は使わず、タッチ情報の構造体配列に保管してある過去フレームの座標をそのまま使用しています。
3.6. ピンチイン/ピンチアウトはどう判定するか
Move入力でタッチ数が2の時に、ピンチイン/ピンチアウトの判定も行っています。(先ほどのMove入力の画像参照)
ただしこの実装だと、同じフレーム中に指AのMove入力と指BのMove入力の2回呼び出される可能性があります。
対象法として、Move入力のタッチ数が2の処理をした時のTimeを保存しておき、次にタッチ数が2のMove入力が来た時に、同じTimeであれば処理しないようにしています。
たぶんこれで大丈夫だと思うんですがどうでしょう!?
3.7. ロングタッチはどう判定するか
◯秒間押し続けるというロングタッチの処理は、ゲーム側で判定してもらうこととしました。
その理由は、前述の通り、押している際に指を動かさないとMove入力が呼ばれないので、押している間ずっと判定するためにはPlayerController内で毎フレーム処理(Tick)にて判定する必要があるためです。(でもなんかPlayerConterollerってもともとTick有効前提っぽいです…?)
また、何秒間押し続けるか、どの座標範囲を押し続けるかといった判定もゲーム側都合なので、ゲーム側でよしなに判定してもらうことにしました。
3.8. ダブルタップはどう判定するか
Labyrinthでは今のところ使用しないので実装していませんが、ダブルタップを判定するには「前回のタップ成功時刻」を保管しておいて、次にタップが成立した時に経過時間と距離を判定してやります。
ただしこれだけだと、
- 連打し続けるとダブルタップが呼ばれ続ける
- ダブルタップの判定がしたくても1回目のタップイベントが呼ばれてしまう
という問題が起きます。
この辺りはいい感じに実装して下さい!
3.9. フリックはどう判定するか
指で弾くような操作を判定するには、指を離す直前の移動量を見て、移動量が多ければ弾いたとみなします。
もう少し正確に判定するには、直前1フレームの座標からの移動量だけでは情報が足りないかもしれません。
タッチ情報構造体配列に過去数フレームの移動量を保管して、平均値を使ったりします。
しかしその時に問題となるのが、指を動かさなかった時にはMove入力が呼ばれないため、タッチ情報構造体配列に保管されている離す直前数フレームが、連続ではないということです。
う〜ん、めんどい!
4. おまけ
4.1. 入力情報を視覚化する
入力情報は目に見えないため、パラメータがどうなっているか気付きにくいです。このため、デバッグ用に視覚化しておくと良いです。
後述の配布UE4プロジェクトでは、WB_Debugというウィジェットブループリントにてタッチ情報を表示しています。
ゲームを実行して画面左上のDebugボタンから、デバッグ情報を見ることができます。
5. 配布UE4プロジェクト
以下のリンク先からUE4プロジェクトを取得すると、今回の実装をすべて確認することができます!自己責任でお好きにご利用くださいませ!