UnityEditorからスマホに直転送して確認したい

f:id:hirasho0:20191003164720p:plain

こんにちは。技術部平山です。

今回は、スマホでhttpサーバを動かした話 の続編でして、UnityEditorから直接スマホにデータを送って即確認する仕組みのお話です。 コードはgithubに全て置いてありますが、まだ実製品の開発には投入しておりません。 現状はプロトタイプです。

今回作ったものを使う流れ

  1. スマホに今回用意したアプリをインストールして起動しておきます。
  2. 今回作ったエディタ拡張ウィンドウに、スマホ側のIPアドレスとポート番号を入れます。
    • IPアドレスはスマホの画面上に出ています。ポート番号は本サンプルでは8080です。
  3. エディタのプロジェクトビューで、スマホで見たいプレハブを選択します。
    • 図ではKonchというプレハブが選択されていますね。
  4. 「選択してるものを送る」ボタンを押します。
  5. スマホで絵が出ます。MonoBehaviour.Start()で再生開始するアニメーションが入っていれば再生されます。

スマホ上の操作は以下のような感じです。

  • タッチ操作で回転、拡大縮小
  • Playボタンを押すとインスタンス生成からやり直し
  • Stopでインスタンス削除
  • Nextを押すと次のプレハブへ移行(複数送った場合)

選択するのはプレハブでもいいし、プレハブを含んだフォルダでもかまいません。 依存したアセットも一緒に転送されるので、単にプレハブだけを選ぶのが 一番使い勝手が良いかと思います。

おまけ機能

スマホ側に解像度が書かれたボタンがありまして、 これを押すと、解像度の縦横を1.414(sqrt(2))分の1にして、総画素数を半分にします。 「ピクセル処理が重いせいでフレームレートが落ちている」かどうかは、これですぐわかるわけです。

さらに進めて、UnityEngine.Profiling.Profilerを使って、スマホ側でプロファイリングを行い、 データをwifiでエディタに送る、ということも考えていますが、 使うのは主にアーティストさん、あるいは企画さんですから、 「シェーダ重い」「マテリアル多すぎ」のように一行でわかるレポートを出す方が、実用性は高いでしょう。 そのあたりは今後考えます。

注意点

  • UnityEditorのPlatformはつなぐ機械に合わせてください。iPhoneを使うならiOSです。でないと互換性がないデータを送ってしまいます。
  • スマホとPCは直接通信できる必要があります。ネットワークが分かれていて通らない、といったことがあると使えません。

何のために作った?

開発の反復速度(イテレーション)を上げるためです。

もちろん、Unityを使えば、作ったものはUnityEditorの画面上で見られます。 シーンビューなら即座に見えますし、 ゲームの流れの中でプログラムが入った状態での見え方を知りたければ、 再生してゲームビューで見ればいいわけです。 ですので、いちいちスマホに持っていって確認する必要はそんなにはありません。

ですが、スマホ実機で確認した方がいいことや、実機で確認せざるを得ないこともあるのです。

  • 処理負荷
    • 実際どれくらい重いのかは実機で動かさないとわかりません。
    • 今回のサンプルでは処理負荷のメーターも出しておきました。重ければすぐわかります。
  • 解像度の適正さや、サイズによる印象の確認
    • PCのモニタは大きいので、無駄に解像度を上げたくなってしまいがちですが、実機は小さいのでそんなに解像度がなくても気にならないことは多いはずです。
  • ハードウェアの個性の確認
    • たまにハードウェアの個性で違った見え方をすることがあります(バグとも言う)。Zバッファ書き込み制御が効いていないように見える機種を見たことがあります。

実機での確認は手間がかかるので、やらずに済む方がコストはかかりません。 「実機で確認するのがプロの心得」と言う人もいますが、 私は「どこまで実機確認せずに済ませられるか詰めてこそプロ」と思っていますので、 たぶん仲良くはなれないでしょう。 そんな私でも、実機確認が必要な場面があることは認識しており、 どうせ必要ならできるだけコストを安くすべきだ、と思うわけです。

これがなかったらどうなる?

PCからスマホに直接転送する手段がない場合、 代表的な確認の方法は二種類あるのではないかと思います。

アプリ本体にデータを入れてビルドする

原理的にはプログラマなしでもできますが、 アプリ本体のプロジェクトの適切な場所にデータをつっこむ必要があるため、 プログラマの手が必要になるケースも多いでしょう。

受け渡しにgitを使えば、gitの操作も必要になりますし、ビルドにjenkinsを使う場合はその手続きも必要になります。手順が効率化され、ビルドを高速なマシンでやるとしても、待ち時間を数分より短くするのは難しいでしょう。典型的には30分くらいはかかるのではないかと思います。

アセットバンドルをビルドしてサーバに配置する

アセットバンドルのビルドを行って、アプリからアクセスできるサーバに配置します。 ビルドをアーティストのPC上でやるか、一旦git等に上げてからjenkinsでビルドするかは 選択肢がありますが、できたものをサーバに上げる手続きが必要です。

プログラマの手を介する必要はないでしょうし、アプリのビルドほどの時間はかからないでしょうが、 それでも1分以内、というわけには行かないかと思います。

で、これをどう使うか?

今回のサンプルは、あくまでサンプルです。 実製品では アプリ本体に入っているスクリプトがないと動かないケースも多いでしょうし、 どのような機能が必要になるかは製品によります。

例えば、ライティングを朝と夜で切り換えて具合を見たいとか、 いくつかある品質設定を切り換えた時の処理負荷が見たいとか、 他のゲーム要素と一緒に出して見たいとか、 そういったことはいくらでも考えられます。

ですから、今回作ったものをそのまま使うことは想定しておらず、 現実的には、アプリの中に専用シーンを用意するか、 専用アプリを用意するか、といった選択肢になるかと思います。 専用アプリを用意するのは、 おそらく本体とアセットバンドルを別プロジェクトに分けているケースでしょう。 アーティストはアプリ本体でしか使わないスクリプト群を必要とせず、 プログラマの大半はアセットバンドル素材には触れないので、 分けた方が安全かつ効率的です。

アーティスト向けツールなどもアセットバンドル側プロジェクトに入れるのが おそらく良く、今回のツールもその例になるかと思います。 アプリ本体と同じスクリプトが必要なケースでは、 本体からパッケージをエクスポートして入れることになります。

実装

エディタ側は、選択されたものをこっそりアセットバンドル化して、 UnityWebRequestでスマホ側のサーバにPUTします。

スマホアプリ側には、以前紹介したhttpサーバが入っており、 ファイルアップロードのイベントで実行されるコールバックで、 アセットバンドルをロードし、中のプレハブを見つけてInstantiateします。

雑に言えばそれだけです。仕組みとしては難しいことはありません。 ただ、結構面倒くさいところもあります。

切断対策

オフィスの1階から3階に上がると何故かつながらない、 といったことが結構起きます。 違うルータに切り換えるにあたってwifiが一時的に切れたのでしょうが、 切れた後しばらく調子が悪いこともあります。アプリを再起動したり、 wifiを一度offにしたりしないといけないこともありました。

詳しいことはおいおい調べますが、 いずれにせよ、エディタ側、スマホ側の両方に「ネットワークの接続状態」 を表示しておかないと、「なんか動かない」となって使い勝手を 悪くしてしまうことが想像されます。

そこで、エディタ側では5秒ごとにスマホのAPI(pingという何もしないapiを用意してある) を叩いて、接続を確認するようにしています。これはもっといい手があるかもしれませんので、 ネットワークに詳しい方に教えを乞いたいところです。

スマホ側はサーバですので、単純にApplication.internetReachability を見て、定期的に画面表示を更新しています。 つながっていればIPアドレスを表示し、wifiが切れていれば「NO WIFI」と表示します。 この手のツールで4Gの回線を使うのはどうかと思うので、 wifiが切れていれば使えない、としています。 LANのIPアドレスを取るのは以下のようなコードです。

public static string GetLanIpAddress()
{
    string ret = null;
    foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
    {
        if ((ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) || (ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
        {
            foreach (var ip in ni.GetIPProperties().UnicastAddresses)
            {
                if (System.Net.IPAddress.IsLoopback(ip.Address))
                {
                    // 無視
                }
                else if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                {
                    ret = ip.Address.ToString();
                }
            }
        }
    }
    return ret;
}

速度が出ない

前にhttpサーバの記事を書いた時点では、たかだか数KBのファイルしか 送っていなかったので、速度のことは全く考えていませんでした。 しかしメガバイト量のデータを送ると数十秒かかることがわかり、 処理を見直しています。

具体的には、PUTを受け付けてファイルをpersistentDataに保存する処理を、 同期処理から非同期処理にし、CPUが無駄にスピンロックしないようにしました。 メインスレッドが休みなくwhileで回ってしまうと、 別のスレッドで動いているhttpサーバ処理も遅くなってしまうようです。

まだ秒間1MBも出ないので遅いのですが、 とりあえず使えなくもない速度になったかと思います。 より高速化する手法をご存知の方は是非教えてください!

作ったアセットバンドルをPCで見るとピンク

Android用のアセットバンドルには、Androidでしか使えないシェーダが入っています。 Android用のテクスチャはPCでも使えるのに、シェーダはダメなのです。 アセットバンドルにマテリアルが入っていて、 そのマテリアルが使っているシェーダがアプリ本体にしかない場合、 アセットバンドルの中にシェーダのコピーが入ります。 アプリ側のシェーダを変更してもアセットバンドルに入っている方は変わらないので 事故の元になる、というのはまあ別の話ですね。

さて、今回の仕組みをデバグするにあたって、 Android用のアセットバンドルをPCでも 見えるようにしたい、という欲求が出てきます。 ビューアアプリをエディタで再生状態にして、PCのIPアドレスを指定すれば 動くのですが、シェーダだけはダメです。ピンクになったり環境光が当たらなくなったりします。

というわけなので、エディタ実行時にはマテリアルの中のシェーダを PC側のシェーダに差し換えることにしました。

material.shader = Shader.Find(material.shader.name);

これだけです。InstantiateしたGameObjectからRendererを全部得て、 マテリアルを全て取り出し、Shader.FindでPC側のシェーダを見つけて差し換えます。

おまけ:高速化

この処理は、大量のマテリアルがあると、かなりのメモリと処理時間を食います。 また、マテリアルプロパティが数多くある複雑なシェーダだと、 マテリアルプロパティを一つづつコピーする処理が必要らしく、 べらぼうに重くなるのです。

エディタだけなら気にしなくてもいい、とも言えるのですが、 実製品では200MB以上のメモリと4秒以上の時間がかかったりもして、 ちょっと悲しかったので対処しました。 今回のサンプルにはこれが入れてあります。 ImcompatibleShaderReplacerという、英語が不安な名前のクラスでして、 これのReplace()を呼ぶと、エディタ実行時のみ再帰的にマテリアルのシェーダを差し換えてくれます。

その際に、「すでに差し換え処理をしたマテリアルはもう差し換えない」 という処理を足すことで、無駄な差し換え負荷を防いでいます。 素直にHashSetにMaterialを登録していくと無限にHashSetが 肥大化していって、そこに格納されたMaterialがメモリリークになるため、 固定の配列を用いて、一定数以上に増えないようにしてあります。

終わりに

いじっては確認、いじっては確認、というサイクルが速く回ることは 何にも増して重要です。 待ち時間が長いとサイクルを回す回数が減って質が落ち、 待ち時間のためにやる気が失せます。

今回はサイクルを早く回すための方法を一つ試してみました。 今後は特定の製品向けのカスタマイズを行って、実戦投入していく予定です。 そこで得た経験は、今回のサンプルにも反映させていくことになるでしょう。

なお、製品向けカスタマイズをするまでもなく、必要とわかっていることはいくつかあります。

  • ライト制御
    • ライトも当然動かしたい
  • hierarchyとinspectorに相当するものをエディタに表示して閲覧、値の変更をできるようにする
    • 特定のオブジェクトを出したり消したり動かしたりしたいはず
    • コンポーネントのパラメータも見たいしいじりたいはず
  • canvas対応
    • 現状canvasの下にInstantiateされる前提のprefabだと何も出ない。そして2D的に表示する機能もない。

このあたりができた時点で、また記事にするかもしれません。また、 プレハブだけでなく、シーン丸ごと、メッシュ単体、テクスチャ単体、 といったものも見られると良いのかもしれません。

ところで、UniteでUSBで実機と通信して似たようなことをする話 を聞きまして(106ページ目あたりから)、それもアリだなと思いました。 ネットワークに起因する面倒は結構ありますし、 IPを打ち込む手間もあります。 USBでつながっていれば、それを第一選択とするように拡張するのは良さそうです。

Unityで画素密度を固定する解像度設定(FixedDPI)

f:id:hirasho0:20191001224142p:plain

こんにちは。技術部平山です。

今回は小ネタで、サンプルもありません。ビルドの解像度設定についてです。

スクリーンショットにあるように、PlayerSettingsのResolution and Presentation の項に、Resolution Scalingというものがあり、 Resolution Scaling ModeをデフォルトのDisabledからFixed DPIに 設定することができます( AndroidマニュアルiOSマニュアル )。

これは、画素(ピクセル)の密度が同じになるように、機種によって解像度を自動で変える、という設定です。 上の画像では326dpi(dot per inch。1インチあたりの画素数)になっており、 これはiPhone4あたりからの伝統的な値です。 iPhone6Plusのような上位機種を除けば、だいたいこの密度になっています。

実際どうなるのか?

この設定にしておくと、ディスプレイの画素密度が326dpiの機械では、 元の解像度で描画されます。例えばiPhone8なら1334x750です。

ところが、画面の大きさの割に解像度が高い端末、 例えばiPhone8Plusであれば、元よりも低い解像度になります。 元の解像度は1920x1080ですが、これが1560x878になります。 元の画素密度は406dpiもありこれを326dpiに落とすと、 解像度が縦横それぞれ(326/406)倍されるわけです。

何のために使うの?

「無駄に高い解像度で描画して、フレームレートや電力消費を悪化させるのを防ぐため」です。

高解像度で描画する方が綺麗なのは確かですが、タダではありません。 GPUの処理負荷はかなりの部分が画素数に比例しますから、 1920x1080で描画する負荷は、1560x878で描画する負荷に比べて、 およそ1.5倍になります。 CPU側に余裕があって、フレームレートが画素数で決まっているようなケースでは、 この設定を有効にすることでフレームレートが1.5倍になる可能性がある、ということです。

また、元々60fpsで動いているような場合は、電力消費が改善します。 微々たる差しか感じられないことに無駄に電気を食うよりは、 少し解像度を下げて余力を残した方が良いかもしれません。 スマホの場合、あまり電力消費が大きいと熱のために処理能力が低下し、 フレームレートが落ちてくることもよくあります。 前もって解像度を下げておくことは、これを防ぐ効果もあるのです。

実際の例

実際の例としては、iPhone6Plus(1920x1080)で40FPSしか出なかったアプリが、 この設定を有効にするだけで60FPSに改善した例があります。

また、一部Android機のように、 GPU性能の割に解像度が高い(例えば京セラS2で1280x720、S4で1920x1080) 端末では、 元々フル解像度でゲームを動かすことには無理があり、 解像度を下げることで大きくフレームレートが改善します。 S2の場合、対角5インチのサイズで1280x720で、 1280画素ある長辺の長さは4.345インチです。 1280をこれで割ると、294dpiとなります。

iPhoneの326dpiよりも低いですが、 「そもそもこのゲームって、そんなに解像度高くてうれしいの?」 ということをよく考えてみれば、 製品によっては「もう少し低くてもいいな」という判断にもなるでしょう。

お客さんが選べるように選択肢を用意するのも一つの考えかと思いますが、 ほとんどのお客さんにとって適切な設定をデフォルトにするのもまた重要なことです。 画質とフレームレート、電力消費のバランスを鑑みて、 画素密度(dpi)で設定するのは悪くない選択でしょう。 設定一つで済むので実装コストはほぼゼロです。

製品の性質にもよりますが、200から300くらいを設定するのが良いのかな、 と個人的には思います。Nintendo Switchが235dpiであり(Switch Liteは268dpi) それだけあれば十分な気もします。 もし不安があるのであれば、たくさん売れたiPhoneを基準にして上の例のように326 に設定するのも良いでしょう。それでもPlus系での処理落ちを防げます。 その場合も、Androidでは低スペック機に配慮して下げる方が良い気がします (dpi設定はiOSとAndroidを別に持てないので、ビルド前にスクリプトで変更するのが良いでしょう)。

ダイナミックなフォントは解像度が高いほど綺麗になり、 3Dのポリゴンも解像度が高いほど輪郭が綺麗になりますが、 前もって用意したテクスチャは元解像度以上にはなりません。 解像度が高すぎてもボケて見えるだけで、 それは解像度を落として描くのとほとんど変わらないのです。 しかしフレームレートと電力消費は確実に悪化します。

別の方法

お客さんに解像度設定を委ねる場合には、これを使っての設定はできません。 一律になってしまうからです。 設定を見て、 Screen.SetResolution を起動後に呼ぶことになります。

Screenクラスからはdpiも取れますので、FixedDPIを使うのと同じこともできますが、 解像度変更はその場では終わらず次のフレームまでかかりますし、 解像度に依存した処理があると、解像度が変わった時におかしくなる危険もありますから、 注意しましょう。起動直後のみの反映が安全ですが、すぐに確認できないので利便性は落ちます。 設定画面を作るコストの問題もありますから、 製品の性質やお客さんにとっての価値を考えた上で、 良い選択をしてください。

なお、お客さんに設定して頂けるのであれば、 かなり攻めた設定(768x432まで解像度を下げるなど)ができ、 電気が気になる方(例えば私)や、低性能の機種をお使いの方(例えば私)に とってはうれしいのではないかと思います。 過去参加した製品 では、768x432で20fpsという省エネ設定ができるようにしましたが、 完全に私のためでした。

代表的な機械の画素密度

以下に代表的な機械のDPIを表にしておきます。 「この機械はドット粗いなあ」と思う機械よりは上げたいですが、 「これ以上細かくても差がわからないよ」「そもそもそんな高解像度で素材作ってないよ」 という場合には下げて良いかと思います。 素材の解像度が上がれば容量も増えて、通信料金やスマホのストレージにも 悪影響を与えますから、何事も程々のバランスが良いでしょう。

機種 対角インチ 解像度 DPI
iPhone 5S,SE 4 1136x640 326
iPhone 6, 6S, 7, 8 4.7 1334x750 326
iPhone 6Plus, 6S Plus, 7Plus, 8Plus 5.5 1920x1080 406
iPhoneX, XS, 11Pro 5.85 2436x1125 458
iPhoneXS Max, 11ProMax 6.46 2688x1242 458
iPhoneXR, 11 6.06 1792x828 326
古めのiPadの多く 9.7 2048x1536 264
今のiPad 10.5 2224x1668 264
対角5インチの16:9機 5 960x540 221
対角5インチの16:9機 5 1280x720 295
対角5インチの16:9機 5 1920x1080 442
対角24インチのFullHDモニタ 24 1920x1080 92
Retina MacBookPro 13インチ 13 2560x1600 232
Nintendo Switch 6.2 1280x720 238
Nintendo Switch Lite 5.5 1280x720 268

QualitySettingsでの補正

この項目は試していないのですが、たまたま調べたら出てきたので紹介しておきます。

dpi値はQualitySettingsで修正することができるようです。 公式マニュアル にあるように、Resolution Scaling Fixed DPI Factorを設定すれば、 lowやmediumのような設定ごとに、先程設定したdpi値を補正することができるとあります。 0.5を書けば半分になり、例えば326dpiであれば163dpiになって、 解像度が半分になるのでしょう。

機械の性能を見てQualitySettings.SetQualityLevel などを呼んでいる場合には、動的に解像度も変わるのでしょうか? 正常に動くのか、どれくらい処理が止まるのか、といったことはわかりません。

終わりに

今回は小ネタでしたが、下手に最適化で苦労するよりもよほど簡単に効果が上がるので、強くオススメしておきます。

個人的には、多くのゲームのように画面に動きがあるアプリケーションで 300dpiを超えた描画するのは、電池が惜しいと感じます。 もし余力があるのであれば、解像度を上げるよりも、 ライティングなどのピクセルあたりの処理を豪華にする方が、 おそらくは品質も上がるでしょう。