ひらしょー

平山尚が技術のことを書く場所。Unityが多そう。

高速化なあ...

技術ブログらしく、ちゃんと構成を考えて、

いらんことを書かずに情報価値の高いものを書こう、

と思っていたのだが、その結果、一度も書かないまま

数ヶ月が過ぎるという状態になった。

 

本を書いてすら脱線と雑談まみれのものになるのだから、

本ほどの労力をかけられるはずもないブログが

まともなものになるわけがない。

 

あきらめた。それよりはメモでも何でも書いた方がいい。

そもそも、情報それ自体で勝負するスタイルでは私は不利なのだ。

 

さて、最近は高速化をやっている。もちろんUnityでの話だ。

 

過去、高速化仕事をせずに終われたプロジェクトはなかった。

しかしUnityならば違った展開もあるだろう、と期待していた。

エンジンの思想に沿った範囲で無理なく作り、

それで浮いたコストをより価値のある、お客の喜ぶところに

つっこむ、というのが皆が幸せになる道であるはずだ。

スマホの処理速度はおおむねPS VITAを越えており、

あまり無理をして高速化せずとも十分綺麗なものができる

公算は高い。

 

でもまあ、やっぱりそうは行かないよなあ。

 

速いマシンはより遅いコードを書くために使われる。

UnityのオーバーヘッドとC#のオーバーヘッド、

それに数々の新しい書き方のオーバーヘッドが

重なると、とてもVITAより速い機械とは思えない状況になる。

 

では、Unityを2Dメインで使っている場合の負荷はどこに来るか。

 

- シーンの初期化スパイク

- 動的Instantiateによるスパイク

- ガベコレによるスパイク

- DrawCall関連負荷

- 通信関連負荷

- 2Dゲーム特有の重ね塗りの激しさ

- Canvasの頂点詰め直し関連負荷

 

このあたりだろう。

 

シーンの初期化は悩ましい問題だ。

そんなの別スレッドで初期化まで済ませてくれるんでしょ?

くらいに思っていたが、ユーザスクリプトは基本メインスレッド

でしか動かないわけで、AwakeやらOnEnableが

大量についているとメインスレッドが遅くならざるを得ない。

昔ながらのやり方で、画面が止まってる時にガッツリ

初期化してしまうのがたぶん一番いいのだろうし、

Unityはそういう前提で作られているように見える。

 

しかし、それでは待ち時間とメモリ消費が増える。

多少ゲームがガタついても、必要になるまで初期化を遅らせれば、

最初に画面が出るまでの待ち時間は減るし、

そもそも使わずに済むものを無駄に作ることもなくなるから、

合計の待ち時間は減るのだ。

スパイクを削るために、エフェクト全種類を最大数生成する、

みたいなアプローチは、必ずしも良いとは言い難い。

メモリの消費量の問題もある。

 

なので、シーンの中にはあまりモノを置かず、

極力プレハブから動的初期化、ということにしたいのだが、

これがまた重い。作りやすさを重視すると、

行列乗算の必要がなくても「単にまとめるためにgameObject置く」

みたいなことになりがちで、どうしても無駄に重くなる。

匙加減としか言いようがない。

 

ガベコレも面倒だ。ガベコレは完全には抑制できないし、

C#的な普通の書き方をしていれば毎フレームキロバイト

量のnewがされるのはやむをえない。

だいたい通信が来れば、そのデシリアライズ

避け難くnewが走るわけで、エフェクトの類をいくら

事前生成+使い回ししてもゼロにはできない。

foreachも配列以外では全てnewが走る。

となれば「気にしたら負け」くらいに思いたいところなのだが、

さすがに限度がある。数十ms止まる、みたいな状態は

さすがに許容し難い。ガベコレの負荷は使用するメモリブロックの

数に比例するはずなので、とにかく数を減らしたいのだが、

なにせnewは見えにくい。boxingはとりわけ厄介で、

デシリアライザの類でやむをえずobjectを経由する、

みたいなのはなかなかに避け難い。

 

DrawCallも重い。よくSetPassが問題でDrawCallは

問題ではないというようなことが言われるが、

GLES2でもそうなのかは確認していない。

DX11やGL4はスマホからは遠い世界であり、

PC向けインディーを作っている人達の知見が

そのまま使えるとは限らない。

GLES2はよほどうまく実装してもそう速くできないだろう。

ドライバ層が厚すぎる。

metalやGLES3、vulkanなどもあるが、

そういうAPIが使えるのは高い機械で、

高い機械は放っておいても速いのだ。

「いい機械を持っている人にはいい体験をしてほしい」

という気持ちはあるし、やれる範囲ではやるが、

手元にない機種に多数対応せねばならない状況では、

できるだけ同じプログラムを通したい。

GLES2を捨てられないなら全部GLES2の方が

問題は起こりにくかろう。

 

通信関連負荷も馬鹿にならない。

文字列処理は元来重い処理で、ゲーム中に動的にやるのは

避けたいものだ。しかし、作りやすさを考えると、

文字列は扱いやすく、仕様変更にも強い。

そうなると、JSON的なものが中核に入ってくる。

しかしこれがまた重い。まして自動でクラスインスタンス

生成する現代的な形でのデシリアライズをやると、

リクレクションまみれのコードになる。

データ設計が現代的だと、クラスが多数ネストするので、

newも増える。

こういう状況で、通信を無視できる負荷

に抑えるのは結構しんどい。

「無視できる負荷」というのは、

想定する一番ショボい機械で1ms未満だ。

今のところの感覚として、エディタの3倍くらい

は時間がかかる感じなので、エディタで0.3ms

に収まっていればいいのだろうが。

 

あとは2Dゲームの重ね塗りのエグさも如何ともし難い。

画面を暗くするのに上に黒い透けた板を重ねれば、

それで一画面プラスだ。

不透明なものがほとんどないので、奥から描かざるを得ず、

Zで削ることもできない。

画像を不透明部と半透明部に割って先に不透明部を

手前から順にZテスト付きで描く、

という最適化は理屈上可能だが、

それを内部で自動でやるように仕組みを作るのは至難だ。

内部自動でできなければ開発コストが増すわけで、

ただでもかさむUI実装負荷を増すようなマネはできない。

UIは仕様変更が多く、組む手間が大きくコストに響く。

極力「Unity的にフツーの組み方」そのままでやれないとキツい。

かといって謎のスクリプトで最適化をかけてからビルド、

のようなアプローチは、「ビルドした時だけ絵が変なんだけど」

みたいなバグの原因になって、それはそれでコストになる。

 

しかも、スマホの機械はおおよそメモリ帯域が狭いだろうから、

なんぼシェーダが単純でもテクスチャフェッチと塗り

でごっそり持っていくだろう。

透明な部分をポリゴンを切ってくれれば軽減はできるが、

標準のUI.Imageはそんなことはしてくれない。

透明部を切り落とすものを自作することはできるが、

それで頂点が増えればCanvasの詰め直し負荷が上がる。

 

canvasの詰め直しの負荷は結構深刻で、

頂点数が数百でも無視できない負荷になる。

とりわけ、無駄にgameObjectの階層を重ねて

Transformが多数ある場合は重いっぽい。

それだけ行列演算のコストがかさんでくるということだろう。

 

動かないものと動くものを分けろ、と言うが、

そんな面倒くさいことはできればやりたくないし、

下手にやればDrawが増えて逆効果になりかねない。

その意味で、よほどはっきりと割れない限りは上策とは言い難い。

それに現代的なゲームでは大抵のものは動くのだ。

 

頂点数が増えがちなのは文字描画だ。

最低でも文字数×4の頂点が必要になり、

標準のOutlineをつけるとそれが7.5倍にふくれ上がる。

リッチテキスト有効だとタグ分も頂点も加わり、

それはもうひどいことになる。

面積ゼロの三角形を削るフィルタをかましてタグ

分は削れるが、その負荷もタダじゃない。

また品質が許容できれば軽量版のOutlineを用意したりするのだが、

それでも頂点が多い。

レンダーテクスチャに焼くなどせねばならないのだろう。

面倒なことだ。

 

ロクにまとめないままズラズラと書いてきたが、

本当、容易じゃないなあ。