ゲームの起動時にベンチマークを走らせることにした [Unityゲームグラフィックス実践]

![clip_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/08395242-cd4d-1a40-28a1-5deb66fee685.png)

## 長い、3行で

ゲームのデフォルト画質設定やKPI分析の為、モバイル端末のスペックを類推したい。
ので、アプリ初回起動時に簡易ベンチマークを走らせることでそれを実現した。
更にこのスコアをサーバにまとめデータベース化することでベンチ自体ほぼ不要となった。

## はじめに

スマホやタブレットのようなモバイル環境で動くゲームアプリケーションは、最新の高性能端末から骨董品のような低性能端末まで、幅広いスペックへの対応を求められます。
「最新のiPhoneでないとまともに動きません！」みたいなゲームを作る状況もあるにはあるのでしょうがそれはあくまでレアケースで、なるべくであれば多種多様な端末で安定動作することが望ましいのは言うまでもありません。

そしてこの目的を達成する為、例えばオプション画面に「グラフィックス設定」といった項目を用意することで、端末の性能や期待するバッテリーライフに合わせた低画質～高画質な設定をユーザーの好きなように調整出来るように出来る、のが一般的かと思います。

グラフィックス設定を手動で調整させること自体は問題ではないのですが、モバイルゲームユーザーのメイン層はハードウェアに精通したゴリゴリのPCゲーマー層とは対極的で、カジュアル寄りのスタンスであると私は認識しています。わざわざメニューの奥の方にあるオプション画面を弄る、なんて面倒くさいことはしない人の率はそこそこ高そうです。「初期状態でのデフォルト画質設定」で遊ばれ続け、出ている絵がそのゲームのグラフィックスの全てであると判断されることでしょう。

故に、**「初期状態でのデフォルト画質設定」の割り当ては重要である**と私は考えます。これが端末スペックに比して低画質だとグラしょぼゲーと見做され、逆に分不相応に高画質であれば重たすぎてまともに遊べず一瞬でアンインストールされてしまうことでしょう。

## 端末スペックの推定

「初期状態でのデフォルト画質設定」を適切に割り当てるには、実際に動作している端末のスペックを推定する必要があります。その手法にはいくつかの選択肢が考えられます。

1. **初回起動時、ユーザーにいくつかの画質プリセットから手動で選択させる**
このゲームがどの程度重たいのかユーザーが知る術はないし、自身の端末スペックにも詳しくない可能性がある。→ 厳しい。

2. **端末から拾える情報(SystemInfo等)などからスペックを類推する**
残念ながら端末から拾える情報量は少なく、まともなスペック推定は難しい。CPUコア数やクロック数は拾えるが、その数値に嘘をついてくる端末すら存在するのでだいぶアテにならない。→ 厳しい。

3. **端末名称とスペックの組み合わせリストなデータベースを事前に構築しておく**
実現出来るならとても良いけど、どうやってこのデータベースを作る？ネット上のAntutuスコアなどを拾ってくるにしても大変だし情報の信頼性も低い。新しい端末がリリースされたら手動で追加する？そのメンテ工数はどこから？iOS端末くらいの物量ならともかく、Androidの無限に存在する機種数をカバーするのは非現実的じゃない？→ 厳しい。

4. **初回起動時に簡易的なベンチマークを走らせて、その場でスペックを推定する**
メリット/デメリットのバランス上これがモアベターだと判断。実際にリリースされているゲームアプリに組み込み、一定の動作実績を得ることにも成功。今のところ想定通り上手く動いてくれている。
 
というわけでこのエントリは、この簡易ベンチマーク実装について話を進めていきます。

## ベンチマークシステム

初回起動時（及び同じアカウントで端末の切り替えが発生したとき）限定で、端末のスペックを類推する為のベンチマークテストを走らせます。

あくまで大凡の端末スペックが分かればそれで良いので、ある程度の正確さは必要ですが厳密なスペックを計測する必要はありません。ユーザーを待たせてしまうことの方が問題なので、ベンチマーク自体は可能な限り短く、数秒程度で完了する簡易的なものを用意します。
この結果のスコアを元に端末スペックTierを決め、デフォルト画質設定の割り当てを行います。
 
なお、（ある程度正確な）端末スペックが拾えるようになると、例えば**KPI分析に「端末スペックTier毎の離脱率」といった軸を追加する**ことが出来ます。想定した端末シェアへ正しくアプローチ出来ているかの判断基準が用意出来るのは結構美味しいポイントではないでしょうか。

### 実装概説

今回私が用意したベンチマークでは、ピクセルの塗りつぶし能力（フィルレート）を計測しています。CPUベンチマークはマルチコアの扱いによってスコア差が大きく出てしまいますし、そもそも大抵の状況で描画ボトルネックの原因になるのがフィルなのでここを計測するのが自然だと判断しました。

一定時間の間にどの程度の塗りつぶしが可能か、を直接計測したいのですが、何か明快なカウンタがハードウェアやシステムで用意されているわけではありません。特定の端末限定であればそのような厳密な値が取得可能な状況もありますが、アプリが動作する全ての環境で拾えなければ意味がありません。ですので、それっぽい仕組みを自作します。しました。

まずフレームレートを60fps動作にします。
適当なサイズのRenderTextureを1枚用意して、それを適当に用意したシェーダで塗りつぶしするような仕組みを用意します。この塗りつぶし面積は自由に調整できるようにしておきます。（なおシェーダはタイルベースアーキテクチャで最適化されないようにTransparent指定しておく等、特定のハードウェアに依存しないよう細心の配慮を行っています）

時間経過によって塗り面積を少しずつ増やして描画負荷を上げていくと、次第に60fps動作に間に合わなくなりフレームレートが落ち込んでいきます。30fpsを切った時点で、今度は塗り面積を少しずつ減らしていきます。そして30fpsを上回ったらまた面積を増やします。これを何度か繰り返し、30fpsで安定した時点での塗りつぶし面積、これを端末の描画性能、ベンチマークスコアと判断します。

この動作をグラフにしたものが↓になります。赤線はフレームレート、青線は塗りつぶし面積です。実際のAndroid実機上での計測データです。

![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/fe3f2887-0583-a43c-ae47-5ca640d0d2a4.png)


塗りつぶし面積が小さい序盤は60fpsに張り付いていますが、面積が一定以上に増えるとfpsが急激に落ちていきます。
30fpsを割ったら面積を減らしていき、30fpsを超えたら面積を増やし、を繰り返してバランスを取っていき、最終的に30fpsに落ち着いた時点での面積を描画性能スコアとしています。

スコアが求められたら、あとは適当に「この数値からこの数値が標準スペック、この数値以上はハイエンドスペック」のように区切るだけです。簡単ですね。

### 計測の安定化

ベンチマーク開始直後は端末の動作が安定していない可能性が高いので、最初の数フレーム走らせた後で数値をリセットして、そこから改めて計測を再スタートさせています。また、スパイクと判断したフレームは計測に含めず無視します。これにより安定した計測を実現しています。

ベンチマーク中に限らず、実機端末のフレームレートは1フレームずつで確認すると微妙に不安定でフラフラしています。これをそのまま使ってしまうとベンチマーク結果も不安定になってしまうので、現在は以下のような実装で安定化を図っています。

1. 過去5フレーム分のフレームレートを保持しておく。
2. フレームレートの値でソートを行う。
3. 一番大きな値/小さい値を捨てる（中央の安定していそうな値だけを使う）
4. 残り3フレーム分の数値の平均を、実際のフレームレートとして利用する。

いくつかのパターン（5フレーム全ての平均値を用いる、単純な中央値(ソートした3番目の値)を使う、など）でテストしてみましたが、上記のやり方が一番安定した結果となりました。

またこの5というマジックナンバーをもっと大きな値にすることで更に安定したスコアを得ることが出来ますが、その分ベンチマークに必要な時間が増えてしまいます。今回のベンチマークは数秒以内にとっとと終わらせないといけないので、ギリギリ安定した結果が手に入る数値を採用しています。

### 想定外な挙動

ベンチマーク作成当初、なるべく実際のゲームの挙動に近づける為、塗りつぶしを行うシェーダに「PBRなキャラや背景用を想定しためちゃ重たいシェーダ」「ポストプロセスを想定したテクスチャサンプルを多用したシェーダ」「UI/エフェクト用を想定した軽いシェーダ」といった複数のバリエーションを用意し、面積比率はそれぞれ調整しつつも同時に塗りつぶしを走らせていました。ですが、この実装ではなぜかハイスペック端末とミドルスペック端末でスコアが逆転したりといった不安定な描画スコアが出てしまいました。

推測する以外に選択肢が無いのでおそらくの話にはなってしまうのですが、SoCによって得意/不得意なシェーダ演算やメモリアクセス手段がバラバラに存在し、ボトルネックが機種ごとに違うせいで描画スコアが不安定になってしまった、のだろうと想定しています。

最終的には、ややこしいことは全部捨ててシンプルなシェーダを1つだけ使って塗りつぶす方式に切り替えたところ、おおよそ端末のイメージ通りの描画スコアが出るようになりました。良かれと思って手間を掛けたことが裏目になる。よくあることですね。

## ベンチマークによるスペック推定の問題点

さて、こんな感じで実際にアプリに組み込まれ動き始めたベンチマークシステム。当初から既知であったり運用してみて初めて気付いたりと状況は様々でありますが、いくつかの問題点が出てきました。

### 問題点1：ベンチマーク中の数秒間、無駄にユーザーを待たせてしまう

アプリの初回起動直後、お客さんを一番逃がしたくない大事なタイミングで、（ロード中のアニメーションこそ出してはいますが）画面が暗転したまま数秒間ユーザーを待たせることになってしまいます。

もちろん当初から想定内の挙動ではありますので、ベンチマークは多少精度を甘くしてでもすぐ完了するようにしていますが、お客さんからしたら此方側の都合なんか知ったことではないので、ストレスはストレスですよね。なるべくなら1フレームでも待たせたくはありません。
~~昔とあるゲーム開発者がブログで「1int(1フレーム)でもインタラクションを受け付けない時間があるゲームはクソ」とか書いていて感銘を受けたことがあります。~~

### 問題点2：ベンチマーク中に高負荷な割り込みが入るとスコアが不安定になる

初期のベンチマーク実装では、ベンチマーク計測中に端末をスリープさせてしまうと「描画に時間が掛かっている？ってことはこの端末は低スペックだな」といった誤判定が起きていました。

この手の穴は最新版ではほぼ潰してありますが、例えばAndroidの常駐アプリで動画録画を行っていたり、アプリ起動直前まで高負荷な処理を行っていて端末がサーマルスロットリングに引っかかっている状態だったり...といった回避不可能な「ベンチマークスコアが不安定になる何か」はどうしても存在します。

スコアが本来よりも多少低くなったところで、初期状態の画質が低下する程度で遊べなくなるわけではないので致命傷ではありませんが、出来れば安定した正しいスコアが入手出来た方が良いですよね。

### 解決策

上記の問題点を抱えたまま、実際にベンチマークがアプリに組み込まれ数ヶ月が経ちました。サーバ上には”端末モデル名”と”ベンチマーク計測結果のスコア値”のログが大量に蓄積されています。

ふと、「**このログを使えばわざわざベンチマークを走らせなくても良くない？**」ということに気付きました。

というわけで、このログをごっそりと拝借して整理を行い、"端末名称とスコアの組み合わせリスト"を構築しました。このリストとのマッチングをゲームの初回起動時に行うことで、

- リストに適合する、過去に誰かが同じ端末でベンチマークを走らせている端末：
    → ベンチマークは走らせず、リストに保存してあるスコアをそのまま使う
- リストから漏れている、新しい端末やあまり使われないマイナーな端末：
    → 今まで通りベンチマークを走らせる。その結果のスコアはサーバにログとして送信され、リストの更新時に利用される

という分岐を用意することにしました。

リストに適合していればベンチマーク完了までユーザーを待たせることもありませんし、明らかに不安定なスコアはリスト構築時に弾いてしまえば良いので安定したスコアのみを利用することが出来ます。これにより、先程の問題点がほぼ解決することが出来ました。

このエントリの序盤で列挙した端末スペックを推定する方法のうち3番目の、**「端末名称とスペックの組み合わせリストなデータベースを事前に構築しておく」がいつの間にか実現可能**となっていたわけですね。

もちろん、新しくリリースされた端末などリストから漏れたものに関しては今まで通りベンチマークが走ってしまいます。とはいえ、サーバ上のログが溜まり次第リストは随時更新することが可能なので、手作業を挟むことなく＝工数ゼロで新しい端末にも自動的に対応が入っていくわけです。

この改修によって、やっとベンチマークシステムが一定程度に完成形となった、と胸を撫で下ろしました。

### ログの整理方法

さて少々余談になります。

サーバに溜まった膨大なログを整理して、”端末モデル名”と”ベンチマーク計測結果のスコア値”が一対になったシンプルなリストを構築する必要が出てきました。

一つの機種ごとに、下手すると数千数万といったレコードが保存されています。ここからどうやって一つの「代表的なスコア」を導き出しましょう？という問題が待っていました。

シンプルに平均を求める、というのが真っ先に思いつくかと思いますが、これは大抵のパターンで間違いであることが多いです。

例えば同じ機種でのスコアが5つ見つかったとします。このうち4つのスコアが100で、1つだけが10000だったとします。
この場合、その機種の推定スコアは100が妥当であると考えるのが自明ですが、算術平均を求めると (100*4+10000)/5 = 2080 になってしまいます。

このような、データ群の中で極端にばらつきのある値が含まれているものを外れ値と呼びます。データに外れ値が混ざってしまうと、その分析結果の信用は大きく揺らぎます。先ほど書いたようにベンチマークスコアのログの中には不安定な結果、つまり外れ値が結構な率で混ざってしまっており、この対策が必須となります。

まず、サンプル数が一定数より足りていない機種はリストアップから外しました。サンプル数が少ないと、代表値とするには適さないような外れ値が大勢を占めてしまうリスクがあるからです。

で本筋の代表値の求め方ですが、今回は、先人の知恵によりロバスト統計を持ち出して、「ホッジス・レーマン推定量」を使ってみることにしました。
>Wikipedia
>単純な事例では、ホッジス・レーマン推定量は1変量の集団の位置母数を推定する。
>まずn個のデータセットに対して、直積集合（n(n + 1)/2個ある）をとる。ここで直積集合には同じ標本を2つ取ったものも含む。次にそうして得られた集合の要素それぞれに対して平均を取る。最後にn(n + 1)/2個の平均値の集合から中間値を取ったものがホッジス・レーマン推定量である。
>ホッジス・レーマン統計量は2つの集合の差も推定できる。観測値がmとnである2つの集合に対してm × n対の直積集合をとり、直積集合の各対に対して差を取る。m × n個の差の中間値である[3]。 

これにより、外れ値に対してロバスト性の高い、安定した代表値を取り出すことが出来ました。

## さいごに

というわけで、ベンチマークを用いて端末のスペックを推定しゲームのデフォルト画質設定を自動割り当てする仕組み、について解説しました。

他社の状況について詳しくないので、他社タイトルはこの辺りどのように対応なさっているのかが気になります。

また、立場上モバイルゲームに限定した話をしましたが、SteamのようなPCゲーム環境でもスペックの推定にベンチマークを使うという仕組みは悪くない気がします。前述の通りPCゲーマーは自分でカスタマイズしたがる人が多いでしょうけれども、ハードウェアに詳しくない・面倒だという人もいるでしょうから、自動的にPCスペックに適合する画質設定を割り当ててくれる、みたいなのは悪くない気がします。既にどこかのゲームでやってそうな気がしますがどうなんでしょうね。

ちなみに、このベンチマークシステムは特定のゲームタイトルとは切り離したライブラリとして作っていますので、何ならオープンソースとして公開することも不可能ではありません。
もう少しきちんと作り込んで、汎用的に端末スペックを推定する仕組みとして様々なアプリに共有出来たら良いな、と思うのですが、どこか音頭を取ってくれる会社とかありませんかね...？お声がけ待っています。（残念ながら私の現在在籍している会社ではそんな余裕はありません）

理想はUnity社のような元締めが用意してくれることなんですけど、それはまあシガラミの関係上難しいでしょうね...

![qrFC33g.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/93fa02f5-46f7-e57b-1f7b-e0ecf46492b2.gif)