アドベンチャーゲーム『ALTDEUS: Beyond Chronos』の開発メンバー

中地:「Unityでチャットに使えるモバイルキーボードの実現」と題して発表を始めたいと思います。よろしくお願いいたします。

メチャクチャ緊張しているので、すごく噛んだら申し訳ないです。最初に自己紹介をします。UnityとかVRのエンジニアをやっている、なかじと申します。Twitter(@nkjzm)をやっているので、よかったらフォローしてください。「日本VR学会認定 上級VR技術者」で、専門学校でVRを教えたり、VRのPodcastをやっていたり、VR関係のことをけっこういっぱいやっている人です。

略歴に書いたんですが、僕は実は新卒で入った会社がサイバーエージェントで、久しぶりにこういう機会に関われてうれしいなと思います。

そのそのつながりで今やっていることを紹介したいんですが、『ALTDEUS: Beyond Chronos』というタイトルの開発をしています。このゲームは、VRを使ったアドベンチャーゲームで、物語にすごく入り込めるところが推しのゲームなんですが、マシンアクションパートがあったり、かわいい女の子がVRライブをしてくれたり、盛りだくさんのゲームです。

こちらが「Oculus Quest」と「Oculus Rift」向けに好評発売中で、なんと本日(※取材当時)「Steam」版が発売しました。発売してすぐに、Steamの全カテゴリでトップ2位や、VRカテゴリでトップワンを取っていて、すごく注目されているゲームなので、ぜひ遊んでみてください。PlayStation VR版も今度出ます。

キーボードの高さにチャット欄を調整する方法

ここから本日の内容に入るのですが、本日はモバイルキーボードを気持ちよく実装する方法について話していこうと思います。左の「よくある実装」と書いてあるGIF画像を見てください。Unityで普通にチャットを実現すると、こんな感じになると思っています。

下のほうにテキストエディタが入っていて、ネイティブの入力フィールドが持ち上がって、Sendボタンを押すと送信される流れだと思うのですが、今回紹介する右の例を見ると、きちんと右のほうにチャット欄が上がって、Unity側のテキストのInputFieldを使って送信ができています。やってみて大変だったので、今回紹介してみようと思いました。

左下に書いてある『ワンナイト人狼オンライン』というゲームは、業務委託で関わったもので、最近リリースされたんですが、このアプリで使用したものを今回紹介します。

アジェンダはこんな感じです。最初に、キーボードの高さにチャット欄を調整するところを紹介していきます。後半はけっこう細かい話が続くんですが、これは1人でやるとけっこう大変なので、ぜひ参考にしてもらいたいと思っています。

キーボードの高さは、iOSに関してはけっこうそのまま取得できます。UnityEngineの中に、TouchScreenKeyboardというクラスがあるので、その中のareaというメソッドを取得すると、Unityの矩形を表すクラスのRect型で矩形を取得できます。

ただ、このメソッドのドキュメントに、すごく悲しいことに「AndroidではRectは0です」と、Androidには対応していないとリファレンスに書いてあります。なのでAndroidに関しては、ここはbaba-s(@baba_s_)さんのスクリプトで対応しています。

一部抜粋したんですが、こんな感じになっています。見てもらうとよくわからない、けっこう難しい処理が書いてあると思うんですが、黄色で書いてあるAndroidJavaObjectというクラスを使うと、Androidのメソッドやインスタンスを呼び出せるので、それを使ってがんばってネイティブ側の情報を取得しています。

なので、Androidに関してはけっこう大変だったんですが、baba-sさんが簡単にできるライブラリを公開してくれていたので、それを使って比較的簡単にできました。

キーボードをチャット欄に反映させるために考慮すべきポイント

次に、キーボードをチャット欄に反映させるというところを紹介していきます。これは3段階あります。普通に考えたらuGUIに反映させたらいいという話です。GIF画像があるんですが、キーボードが出てきたら全体の相対位置を変えて、チャット欄を良い感じに上にずらせばいいだけです。

だけど、その仕組みをやったことがない人だとちょっと難しいです。右の図はStretchにしているので、全体に引き伸ばしているうちのBottomだけを上げればいいんですが、Rect Transformの場合、ここに書いてある値がそのままスクリプトや内部で保持されているわけではありません。

下に「Debug表示で実際の値が見られる」と書いたんですが、Debug表示に切り替えるとわかると思いますが、実際はけっこう複雑で、いろいろな値によって右のようなViewが構成されているので、読み解くのがもしかしたら大変かもしれません。なので今回は、chatContainerの全体の大きなuGUIの枠の中の.sizeDeltaに対して値を入れることで、今GIF画像のマウスでやっているような動きを実現しています。

ほかにも気をつけるところがあって、解像度の調整をする必要があります。これは何かというと、先ほど取得したキーボードの値です。キーボードの値は、端末の解像度の単位で返ってくるんですが、uGUIの場合、Canvasの解像度で制御されていることが多いと思います。Canvasの右下に書いてあるCanvas Scalerを使っている人がけっこういると思います。

これはReferenceResolutionといって、その解像度を基準にしていい感じに縦・横を伸ばすことで、どの端末で見てもだいたい同じぐらいの大きさでUIが表示されるというものです。ただ、これを使うと、端末の解像度とuGUI上の解像度の単位が変わるので、それらを合わせる必要があることを考慮する必要があります。

技術的なコードが左に書いてあるんですが、まずresolutionHeightですね。Canvasの解像度を取得して、rateで端末の解像度とCanvasの解像度の比を求めて、それをきちんとキーボードのHeightと掛け合わせることで、uGUI上で扱える値にする処理を書く必要があります。

これをチャット内に反映させるところにもSafeAreaの罠があります。さっき紹介したキーボードの高さは、SafeAreaの領域を含むんですね。下の図の、緑の枠で囲まれた部分がSafeAreaなんですが、その下の部分の領域まで含んだ高さになっています。ただ、SafeAreaの部分は入力ができない部分なので、そこのUI要素を上にずらすことが多いんですね。

なので下に隙間があるんですが、そこにさらにキーボードの高さ分空けてしまうとSafeAreaが2回分含まれてしまうことになり、その分ズレるという問題が発生します。そこを考慮する必要があるのに意外とハマりました。

そんな感じでキーボードの高さは実現できたので、続いてネイティブの入力フィールドを非表示にする処理に移ります。

これは変数が用意されていて、さっきのTouchScreenKeyboardに.hideInputというものがあります。これも引っかかりやすいポイントがあって、InputFieldを使うのがけっこう多いと思うんですが、InputFieldの場合、HideMobileInputというプロパティが用意されていて、これで毎回上書きされちゃうので、上で設定しても反映されないことがよくあります。

今回はネイティブの入力フィールドを隠す方法をお話ししたんですが、下の注意事項にあるとおり、ネイティブのコピー&ペースト機能が使えなくなったり、Androidだと疑似的な実装なので、動作が不安定になったりするなど、デメリットがあります。なので、別にこれはそのまま使う実装でもぜんぜん良いとは思いますが、これを隠してUnityのInputFieldを表示したほうが、フォントなどをゲームの世界観に合わせやすいと思います。

続いて、ネイティブボタンのOKボタンを取得するところです。何で必要かというと、これはAndroidのスクリーンショットなんですが、キーボード外の入力は拾えないという問題があります。画面の中央部分の赤いSendというボタンを押しても、そのSendボタンの判定が取れないという問題があります。

ポジションなどを取って、離すタイミングが取れるんじゃないかいろいろやったんですが、無理でした。なので、右下の青いネイティブのOKボタンを使って、送信するようにしました。これもけっこうやり方があったんですが、ソフトモバイルキーボードのクラスの中の.statusという値を監視することで、実現できました。

使っている値は大きく2つで、DoneボタンになったらOKを押したタイミングで、そのイベントが飛んできて、画面外をタップしてキーボードを閉じた段階でLostFocusが入ってくるかたちです。

送信ボタンを押しかけて押さないケースを想定

最後にキーボードのハンドリングです。キーボードは1回入力を終えたり、Unity領域をタップしたり、OKボタンを押したりすると閉じちゃうんですが、チャットの場合は開きっぱなしにしたいですよね。右のGIFを見てもらうと、打ったあとにきちんとキーボードが開きっぱなしになっていると思います。それを実現するための知見です。

やっていることは、いろいろなタイミングでのキーボード再表示と再フォーカスです。チャットを送信した瞬間にキーボードを1回出しているんですが、それだけじゃ不十分です。UnityのSendボタンが用意されているんですが、あれはタップした瞬間の判定が取れなくて、離した瞬間にボタンが押されたという判定を取っているので、タップした瞬間にキーボード自体を閉じ始めちゃうんですね。なのでそこのタイミングでもう1回キーボードを出しました。

ほかにも複雑なのが、Sendボタンを押したあとに横に指をずらして離す場合ですね。ユーザーの求めていることは、キーボードを閉じずに、テキストはまだ編集できるという状態だと思うので、そういうボタンを押しかけて押さなかったときも考慮する必要があって、けっこう大変でした。

処理も、キーボードを開く処理と、フォーカスをきちんとテキストのInputFieldに戻す処理の2つがあるので、けっこう考慮するところがいっぱいあって大変だと思いました。

今回、考慮するところがけっこうあったし、ネイティブの挙動なので、毎回試して実装を変えて、ビルドして、確認するのがすごく大変でした。今回紹介したこのサンプルプロジェクトをGitHubのUnlicenseで公開しているので、モバイルのキーボードで悩む人がいたら、これを参考にしてもらえるとうれしいです。

ご清聴いただきありがとうございました。

司会者:ありがとうございました。

コメントは私が読み上げますね。「Rect Transformは魔界」など、みなさん苦労しているようなコメントが多くありました。

中地:そうですね。今回は省いたんですが、uGUI上では、LeftやAnchorsやPivotなどとわかりやすい概念になるんですが、それをスクリプトで扱おうとすると、それらがどういう関係で構築されているのかを理解する必要があるので、やったことがない人だと、ここがすごく苦労すると思います。

司会者:なるほど。ありがとうございます。SafeAreaの対応についてもコメントでいくつかいただいていましたね。

中地:ちょっと補足です。SafeAreaのこのスクリーンショットなんですが、UnityのDevice Simulatorというものを使っていて、エディタ上でSafeAreaのエミュレーションができるので、これを使うと対応がずいぶん楽になると思います。

司会者:ありがとうございます。ではお時間になりましたので、これでLTを締めたいと思います。なかじさんありがとうございました。

中地:ありがとうございました。