テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】実践的なパフォーマンス分析と最適化

f:id:tsubaki_t1:20180609202944j:plain

今回はUnite Tokyo 2018で紹介された「実践的なパフォーマンス分析と最適化」のセッションの動画から得られた物をメモしていこうと思います。

なおセッションの音声をそのまま文字にするのではなく、メモを元に自分なりの理解を文章に起こしています。内容の順番も幾つか変更しています。

f:id:tsubaki_t1:20180609234258g:plain

なおセッションタイトルは「実践的なパフォーマンス分析と最適化」ですが原題は「Real world performance analysis and optimisation」です。

内容は、ローエンドなモバイル向けのパフォーマンスの最適化についてです。

 

 

はじめに

この話は、エンタープライズサポートでよく言われる問題、例えばメモリ使用率、ロード時間、CPU負荷について、どのように改善するのかといった事を話します。

f:id:tsubaki_t1:20180609203416j:plain

ただ最初はプロファイリングの話をします。
何故かと言えば、プロファイリングはパフォーマンスを話す上で迚も大事な要素だからです。実際アドバイスでは多くの場合プロファイリングについて話します。

プロファイリング

まずはプロファイリングを行い原因の抽出を行うことは非常に大事なことです。

f:id:tsubaki_t1:20180609204108j:plain

プロファイリングでよくある改善ポイントの一つは、リアルな環境でプロファイリングするという事です。言い換えればエディターの中でプロファイルしないという事です。
エディターのパフォーマンスは特定プラットフォームの動作と異なる場合があります。例えばエディターではアンロードされないリソースによってメモリ使用率が変わったり、スクリプトの動作が変わったり。

では何処でプロファイリングするべきかと言えば、想定するハードウェアやプラットフォーム上でプロファイリングします。古いモバイルなら古いモバイル、コンソールならコンソール。
特にCPUやファイルIOの特性も異なるので、出来る限りそれぞれの環境でやるべきです。スケーリングする場合は、頑張ってください。

次にゲーム全体の状況をプロファイリングするのも重要です。これは2つ理由があって、単体の動作確認では不十分である場合があります。

  • ゲームの原因を把握してると思ったら違った…というケースを発見する
  • リソースが複数の場所で共有している事がある

あと基本ですがプロファイリングは変更前と変更後で行う事も大事です。

 

いくつかプロファイリングの機能と新機能の紹介です。

プロファイラーのCPUプロファイラーウィンドウ。これはCPUの使用率であったりGCによるスパイクの把握等に役立ちます。
最近は以前は未対応だったスレッドのサポートや、ビルド済みStandaloneプレイヤーのDeepProfilingが追加されました。

f:id:tsubaki_t1:20180609205329j:plain

フレームデバッガは描画のコマンドを確認出来ます。
これはどのような順番で描画コマンドが発行されたかや、何故バッチされなかったのかといった情報を確認出来ます。

f:id:tsubaki_t1:20180609205646j:plain

メモリプロファイラという機能もあります。
これはUnityの使用しているメモリのスナップショットを取り確認できる機能で、Unity 2017.3からIL2CPP環境外でも使えるようになりました。

この機能ではメモリを使用しているものの名前と参照が分かります。例えば大きなテクスチャがメモリを専有しているとき、そのテクスチャは何という名前か、そして誰が参照しているのか…といった情報がわかる訳です。

tsubakit1.hateblo.jpUnityのプラットフォーム機能も紹介しましたが、プラットフォームのプロファイラも重要です。OSレベルでより高度な、全体的なパフォーマンスの把握に使用できます。

f:id:tsubaki_t1:20180609210227j:plain

 

最適化

ここから、よくある問題と解決方法についてです。

 

アセットの設定はルール付けして自動処理

アセット(Texture/Mesh/Animation/AudioClip等)はアセットの設定次第で容量が大きく異なります。アセットを後から調整しない為には、アセットのルールが必要です。

アセットのルールがあるのならば、AssetPostProcessor等で自動的にルールを適応するのが良いです。
そんなの面倒くせえと言う貴方の為に、エンタープライズチームが作成したアセットインポートの自動化ツールがありますGithubからダウンロード可能!

https://github.com/MarkUnity/AssetAuditor

f:id:tsubaki_t1:20180609211043j:plain

過剰な高解像度、無用な設定を排除

アセットでよくあるのが、過剰に高解像度なアセットです。例えば超高解像度テクスチャとか超細かいポリゴンとか。
この辺り殆どの場合ゲームにメリットはありません
得られる表現の内容と比較して、ランタイムのメモリサイズやアセットのサイズ、ローディング時間やGPUアップロードコストがバランス取れているのか考える必要があります。

またよくあるのはReadWriteが有効というケースです。例えばReadWriteが有効なテクスチャは約二倍くらいのメモリを消費します
これはGPUにアップロードしたテクスチャに加えてCPUのRAMにもテクスチャの情報が乗る為です。
これは主にCPUがテクスチャのピクセル情報を読み込んだり書き込んだりする為に使用します。これらが不要な場合はReadWriteを外せばRAMのコピーが無くなります。

生成したTexture2DはReadWriteが最初から有効です(自分でCPUからアクセスするので当然と言えば当然)。これはApplyでGPUにアップロードするときにRAMの情報を破棄することが出来ます

f:id:tsubaki_t1:20180609212107j:plain

これはメッシュも同様で、メッシュのReadWriteを有効にすると頂点情報がRAMに常駐します。
ReadWriteは以下の条件が当てはまる場合は外すとマズイですが、それ以外なら外しても良さそうです。

  • コードの中でMeshにアクセスしている
  • MeshColliderを使用していて、Transformがマイナススケールを持っている
  • MeshColliderを使用していて、Transformが斜めになっている場合

下2つは親オブジェクトが不均一にスケーリングしたり回転してる場合に起こるみたいです。この状況ではPhysXが正しく形状を取得するためにメッシュの情報が必要です。

多分こういう感じです。オブジェクト毎のスケールと回転がめちゃくちゃの時、Cubeが変な形状になります。

f:id:tsubaki_t1:20180609212938g:plain

コードで作られたメッシュも当然ReadWriteが有効です。これはUploadMeshDataメソッドでメッシュを開放できます。

f:id:tsubaki_t1:20180609213150j:plain

なおテクスチャにMipmapという項目があります。
これは画面の表示対してテクスチャのサイズが大きい時に描画効率が上がるというものですが、サイズが33%増えます。
画面に対して大きさが一致する、つまりカメラとの距離が変わらないなら無効にするのが良いです。

 

メッシュの圧縮

メッシュサイズを減らす方法として、Vertex Comporessionというオプションがあります。これはPlayerSettingsの設定です。
これは対象のチャンネルの精度を半分にしてランタイムメモリとディスクサイズを稼ぎます

なおSkinnedMeshRendererには非対応でしたが、Unity 2018.2からTextureの座標(UV?)だけ対応するようになります。

f:id:tsubaki_t1:20180609213846j:plain

ただし、ファイルで圧縮の設定が上書きされている場合は例外です。
Meshの設定に同様にMeshCompressionの設定があるのですが、これを有効にしているとVertex Compressionは無効になります
またReadWriteが有効の場合も無効になります。この辺りも含めてアセットのルールはよく考えるのが良いです。

 

アニメーションの最適化

次にアニメーションとその周辺です。
アニメーションの圧縮設定を変えることでメモリが改善します。
この辺り、Compression Error設定で微調整が可能です。見た目とランタイムの負荷で良いポイントを探すのが良さそうです(当然プロファイリングしながら!)

なおアニメーション圧縮は初期設定では無効になっています。これを有効にすることでクリップサイズはかなり小さくなります。

f:id:tsubaki_t1:20180609214610j:plain

Legacyの場合はKeyframe Reductionで冗長なキーフレームを削除します。GenericはKeyframe reductionかOptimalを選べます。

 

さてアニメーションシステムは大きく分けて3つあります。LegacyとHumanoidとGenericです。ドレを使うべきかというのを考えてみます。
CPU負荷で見てみます。
iPhone 4Sという非常に古い(コアが少ない)端末で12カーブという小さな要素を持つアニメーションを100個再生してみます。結果はLegacyのほうがGenericより高速です。

f:id:tsubaki_t1:20180609215243j:plain

逆に640という沢山のカーブを持つアニメーションの場合、LegacyよりもGenericの方が高速になります。

この内容から考えるに、カーブの数でLegacyかGenericか決めるのが良さそうです。
分岐点は大体300カーブぐらいで、その辺りを超えるとGenericのほうがよく、それ以下(例えばUIアニメーション等)だとLegacyの方が良いという感じです。ただし、この負荷はコア数によって変わってきます
(Mecanimはマルチスレッドで動作するが、Legacyはシングルスレッド)

f:id:tsubaki_t1:20180609215406j:plain

では、この一番重いHumanoidは何時使うべきでしょう。
Humanoidは同じクリップでもリターゲット出来ることがメリットです。またIKなどにも対応しています。
逆を言えば、IKやリターゲットが必要ならばHumanoid、使わないならばGenericという形で良さそうです。

f:id:tsubaki_t1:20180609215843j:plain

またアニメーションの動作において知っておいたほうが良いのがCullingModeという機能です。
Always Animateは例え画面外でも常にアニメーションとステートマシンを実行します。最も負荷が一定で高いです。
一方CullComplete設定では画面外にいるキャラクターはアニメーションしないようにします。これにはステートも含みます。つまり画面外だとステートマシンが止まります
CullUpdateTransformsはステートをアップデートするがIK等は画面外では動作しないという設定です。

 

Animatorで喜ばしい重要なアップデートがあります。
Animatorはデータバッファーやステート情報をアクティブ時に保持しており、非アクティブになると破棄していました。つまりAnimatorを持つオブジェクトをプーリングすると、バッファーの破棄や再取得、ステートの再設定が高いCPUスパイクにつながっていました。

Unity 2018.1からKeepAnimationControllerOnDisableという設定が追加され、データバッファとバインディングを保持できるようになります
なおこの設定はスクリプトでしか設定できません。

f:id:tsubaki_t1:20180609220540j:plain

 

オーディオの最適化

オーディオでもメモリをセーブしたいと思います。

オーディオクリップのロードタイプですが、ファイルの大きさが1MB以上の場合はStreamにするとメモリ負荷が少なくて済みます

一方ファイルの大きさが200KB~1MB程度の場合はCompressed Memory200KB未満ならばDecompress on loadが推奨されます。

この区分は、ファイルの展開に200KBのバッファーを使用しているので、それ以下のファイルサイズの場合バッファーサイズがランタイムサイズを上回ってしまうためです。

f:id:tsubaki_t1:20180609230437j:plain

圧縮率及びCPU負荷も見てみましょう。
これはモバイル向けの設定で、どの程度圧縮されるのかと言ったものを見ています。

クォリティは高ければ高いほど圧縮効率は悪いです。最も悪いのはVorbisをクォリティ100%で圧縮した場合で31%まで圧縮されます。一方MP3ならば22%までと、比較すると高圧縮です。
つまりクォリティが高い場合はMP3の方が良い結果に見えます
なおクォリティが50%の場合は同じくらいの圧縮率になります。

f:id:tsubaki_t1:20180609230811j:plain

次はロードにかかる負荷を見てみます。
iPhone4SでCPUに掛かる負荷を比較しています。
最も負荷が低いのはADPCMで、1.4%と非常に低く解凍も早いです。
Vorbis100%は負荷も高くMP3と比較すると倍ぐらいコストがかかっています。こちらも50%にすると大体同じぐらいの負荷になります。

f:id:tsubaki_t1:20180609231129j:plain

このことから、圧縮率がそれほど重要ではないものはADPCMが良さそうです。長い場合はVorbisですが使えるならMP3の方が良さそうという結果になりました。

 

起動時間の改善

起動のコストですが、一つの関数がすごい負荷を計上していました。
GetScriptingClassという関数で、アプリケーションの起動時にクラスタイプのためのアセンブリを探しにいくAPIです。
元々は文字列で色々やっており、特にIL2CPPで高い負荷を形状していました。これを改善することでゲームによっては起動時間が5秒も改善したとか何とか。

これは既に修正済で、現在Unity 2017~の全てのバージョンにパッチが適応されています。(Fixは2018.2のタイミングなので、たぶん最近のパッチです)
要するにパッチバージョンを使えば治るという感じでしょう。

f:id:tsubaki_t1:20180609231902j:plain

 

Crunch圧縮

もう一つ、UnithはCrunch圧縮をサポートしました。
これはDXTでしか使えなかったものがETCやETC2でも対応したというもので、Unity 2017.3から使用できるようになりました。

Crunchは圧縮テクスチャを追加で圧縮する手法で、GPUへアップロードする直前に解凍するというものです。
これをAssetBundleに格納して比較した所、ファイルサイズは大幅に小さくなりました。但しロード時間が劇的に伸びてしまっています。
この辺りは現在調査中です(普通に伸びると思うのですが、発表者的には短くなってほしかったのか…?)

f:id:tsubaki_t1:20180609232144j:plain

 

感想

来週の月曜日(6/11)から5日間にかけて「1日5時間×5日間連続 全25時間放送」というUniteの動画をチャットしつつみんなで見直そうぜ的なイベントがあるので、先駆けて少し見たセッションを文章にしてみました。

connect.unity.com

内容に関しては、正直モニョる部分もあったりしますが、かなり有益かなという印象があります。
まぁこの内容を鵜呑みにせず”分析”して”最適化”するのが良さそうです。

 

関連

資料:【Unite Tokyo 2018】実践的なパフォーマンス分析と最適化

動画:【Unite Tokyo 2018】実践的なパフォーマンス分析と最適化 - YouTube

 

最近の個人的に思う有益な動画と言えばコレ

tsubakit1.hateblo.jpUnite tokyo 2018の動画は全て公開されました

tsubakit1.hateblo.jp

【Unity】ドット絵に照明効果を追加する

今回はドット絵にライティングを追加するアプローチについてです。

f:id:tsubaki_t1:20180607221511g:plain

 

 

ドット絵 は 光の演出 を手に入れた!!!

今回はドット絵に光の演出を試してみました。
例えば暗い洞窟や完全自動で動く工場など、ステージ全体が明るくない場所での表現において、光による演出は中々に楽しそうです。

なお、今回使用するのは殆どTilemapかSpriteRendererで構成されたものです。ライティングを施さなければ大体こんな感じになります。

f:id:tsubaki_t1:20180608003039j:plain

 

この辺り通常の3Dであれば、この辺りは最初からうまい感じに表現してくれるのですが、ドット絵はあくまでも2Dのため、一工夫が必要になってきます。

さて、光表現は大きく分けて4つの要素が考えられます。

  • 光の届く範囲が見える
  • 光を向いてる面が明るくなる
  • 光るパーツは暗闇でも明るく見える
  • 光を遮り影ができる

この4つの要素を全て入れる事も可能かもしれませんが、手間とか色々な要素を考えて入れるかどうかを判断する必要があります。

 

光が届く範囲が明るくなるだけで良い

まず「光が届く範囲が明るくなる」…という考えです。スーパーニンテンドーRPGのような物でよくある表現で、目的は「視界を狭めること」です。
やり方は二通りあって、一つ目はSpriteMaskのようなステンシルを使う方法、2つ目は普通にライティングの機能を使う方法です。

まずSpriteMaskの方法を考えてみます。

  1. 画面前面に黒いSpriteを描画します。この時、Mask InteractionはVisible Outside Mask(マスクの外側だけ表示)に設定しておきます。
  2. GameObject > 2D Object > SpriteMask で光の形状のスプライトをマスクに登録すれば、マスクの範囲がくり抜かれます。

マスクの形は変形しないので若干雑な感じの切り抜きに見えるかもしれませんが、個人的には暗闇表現なら大抵はコレで良い気もします。

f:id:tsubaki_t1:20180608000451g:plain

普通にライティングを利用する方法は、Spriteに使用するシェーダーを変更します。Spriteが光の影響を受けないのは、光の影響を受けない(代わりに高速)シェーダーを使っているからなので、コレを変更してやればOKという話です。

  1. DiffuseSpriteという名前のMaterialを作る
  2. DiffuseSpriteのシェーダーはSprite>Diffuseにする
  3. TilemapRendererやSpriteRendererのMaterialをDiffuseSpriteに変更する
  4. ポイントライトやスポットライトを適当に配置

これで光の陰影をピクセルベースで計算したものを使用できます。

f:id:tsubaki_t1:20180608001355g:plain

 

光の陰影や光るパーツなどを表現するのも欲しい

光の陰影や光るパーツなどを表現してみます。

なお光の陰影が追加される事で光の演出とテクスチャの光表現が重複する可能性があることには注意する必要があります。
(Unityゲームでよく見た「残念な感じの絵」の一つは、テクスチャに光表現を焼き込んだ上でシェーダーでも光表現を…という物があります)

f:id:tsubaki_t1:20180608005117j:plain

まず考えることは、陰影を2Dであるドット絵で表現する方法です。
これにはノーマルマップ(法線マップ)やエミッションなど表現に対応したテクスチャ用意します
なおエミッションは全体を暗くして光らせたい所に色を付ければ良いのですが、ノーマルマップの方はどうやって作ればよいのか…

RGB のカラー値は X,Y,Z のベクトルの方向を保存するために使われます。Z が“上方向”になります(逆に、Unity では慣例的に Y が“上”)。加えて、テクスチャの Z 値は、0.5 が足されて半分になります。これは、すべての向きのベクトルを保存するためです。そのため、RGB カラーをベクトル方向に変換するには、2 倍してから 1 をひかなくてはなりません。

法線マップ(Normal Map)(Bump mapping) - Unity マニュアル

f:id:tsubaki_t1:20180608084842j:plain

あとはノーマル(法線)マップに対応したシェーダー…例えばStandardShaderなどをセットしたマテリアルを用意し、NormalやEmissionに設定、SpriteRendererやTIlemapRendererに登録すれば、光の表現が出来るようになります。

f:id:tsubaki_t1:20180608084444j:plain

 なおSpriteRendererもTilemapRendererも、メインのテクスチャはレンダラが使用したいものが自動でセットされ、そのUVを使用してノーマルもエミッションマップも設定されます。つまりノーマルマップとエミッションのマップのレイアウトはスプライトに使用したものと同一である必要があります。

この何が問題かといえば、SpriteAtlasのようにパッキング後のレイアウトが確定せずスプライトをレイアウトする機能を使用すると、ドット絵とノーマルマップのレイアウトがずれてしまう可能性があるという事です。

つまり、この手法を使用する場合はドット絵側でスプライトを詰めてやる必要があります

 

ドット絵に影もつけたい

最後にドット絵に影を付けてみます。
この時の考え方は単純で、ドット絵の上に見えない影だけを落とすモデルを配置するという力技を使います。

建物の高さはCubeの高さで、実際3Dで見たらどの辺りに配置されてるのかな…的なものを見たら上手くいきます。
配置後CubeはのMeshRendererのCastShadowの設定をShadowOnlyに設定しておけばゲーム画面では表示されなくなります。

キャラクターの影くり抜きの場合、Cutoutシェーダーでキャラクターのシルエットにくり抜いた影を使うというアイディアもありますが、Spriteに対応してないのでその辺り保留
f:id:tsubaki_t1:20180608090301j:plain

ただ残念な事に、背景…というか地面にTilemapを使用している場合このアプローチは使えません。TilemapはRecieveShadowを無視するみたいです(例えスクリプトでONにしても、影対応シェーダーを使用しても効果はありませんでした) 
なので影の手法を使う場合はTilemap以外の方法で背景を表示する必要があります。上の図では背景は普通にQUADを使用しています。

f:id:tsubaki_t1:20180608091020g:plain

まぁ建物の構造を強調するための影という観点で言えば、2DDLのような2D影を使ったほうが良いかもしれません。

assetstore.unity.com 

感想

ということで照明効果についてでした。

オールインワンで無条件で突っ込めるという感じでは無いので、用途によって分けました。必要に応じて追加していくのが良さそうです

関連

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp

「Unityテストを完全に理解した」の動画とスライドが公開

f:id:tsubaki_t1:20180608093128j:plain

先日行われた「Unityテストを完全に理解した」の動画とスライドが公開されてました。

Unity テスト完全に理解した - connpass

 

動画

動画はこちら。動画では少しスライドが見にくいので、スライドは別に見るのをお薦め

www.youtube.com

各セッションのスライド

Unityでどのようにテストを導入していくか…という話です。

speakerdeck.com

speakerdeck.com

SOLIDの設計について

speakerdeck.com

関連

monry.hatenablog.com

【Unity】ECSのメモリレイアウトとその周辺

今回はECS(Entity Component System)とはどういった機能かといった話について書いていこうと思います。
この情報はハッキリ言えば知らなくても問題ない情報なのですが、知っておくとECSを採用するべきケースや作り方に迷ったときに助けになるかもしれません。

 

なお、ECSの概念や使い方については、Unity 2018のEntity Component System(通称ECS)について(1) Unity 2018のEntity Component System(通称ECS)について(2)で紹介しています。
実際の使い方イメージはECSで簡単なゲームを作ってみたので、その解説 です。

 

 

メモリ転送速度がCPUのボトルネックになる世界

ここ数年でCPUの性能は飛躍的に向上し、またマルチコア等の複数並列同時計算能力も手に入れました。それに対してメモリの速度はそこまで高速化しておらず、メモリの転送速度がCPU性能を引き出す上でのボトルネックになっている事が多々あります。

 

例えばメインメモリからデータをコアに渡す場合、その読み込みを行う間にコアでは200サイクル以上もの処理を行えます。もし処理が完了したならば、情報が届くまでの間は待ち時間が発生します。
この辺りのギャップを無くすため、大抵のCPUにはL1キャッシュやL2キャッシュといった高速な(しかし非常に小さい)キャッシュが存在します。このキャッシュに乗っている情報の取得は3~4サイクルと非常に短い時間で取得出来るので、毎回メインメモリから取得するのに比べて圧倒的に効率的です。

f:id:tsubaki_t1:20180605230619j:plain

 

また普通は必要なデータを1バイトずつ転送するのではなく64バイト程度のメモリを一気に転送します。1バイトずつ転送したり必要な分を抽出する手間よりも一気に転送してしまったほうが高速という形です。

このため、もし必要なデータが例え1バイトしかなくとも、63バイトの無関係なデータと共にロードされます。

f:id:tsubaki_t1:20180605231249j:plain

 

オブジェクト指向はCPUに優しくない

オブジェクト指向コンポーネント指向は、オブジェクトに処理やデータを格納し、内部でデーややステートを制御するアプローチです。このオブジェクトは各々が独立しており、基本的に単体で完結出来るのが理想です。
またオブジェクト毎に独立した形でメモリに展開されており、参照という形で各々のオブジェクトにアクセス出来るようになっています。

この設計はメモリの空きを作らないという点や設計しやすい点で非常に優れているのですが、キャッシュという観点から見ると少し良くないです。
各オブジェクトはメモリ上に散らばる形で配置されるため、メインメモリからデータを取り出す時に何度もメインメモリから転送する事になります。

f:id:tsubaki_t1:20180605232441j:plain

なお正しくはオブジェクト指向が悪いというよりは、オブジェクト指向だと大量のデータを操作するときに効率が悪くなりやすいという話です。
なので、少ないオブジェクトだったらソコまで気にしなくても良いと自分は思います(そう言っても大抵は最終的にオブジェクトの量が増えることが多いのですが)

 

その名はデータ指向設計

キャッシュに優れた場合のメモリレイアウトはどういったものか?
簡単に言えば、データが固まっているものです。

1回64バイトで可能な限りデータをロードしてキャッシュに載せられれば、以降のデータ読み込みはL1キャッシュから行われるようになります。
これでコアの待ち時間が殆どなくCPUは効率的に処理を実行出来るようになります。

f:id:tsubaki_t1:20180605235806j:plain

これでアクセスが高速化出来る訳ですが、この高速化を十分に活用する為には一つの処理中に様々なデータにアクセスするのではなく、アクセス対象を決めて一気に処理してしまうのが効率的です。

これは並列処理した場合でも同様で、むしろデータ構造的に並列処理しやすいといったメリットもあります。

f:id:tsubaki_t1:20180606125316j:plain

このようなメモリレイアウトの設計やデータのアクセス方法に着目した設計をデータ指向設計と呼ばれます。

 

UnityのECSはデータ指向設計

UnityのECS(Entity Component System)はC#上で実装されたデータ指向設計(Data Oriented Design)です。

 

利用者側から見た場合、Entityとはコンテナです。EntityにComponentDataを追加したり削除する事で、Entityの振る舞いを作ることが出来るようになっています。
その点で見ると、Entity=GameObjectでComponent=Monobehaviourといえるかもしれません。この見た目は人類に優しいです。

f:id:tsubaki_t1:20180605234712j:plain

実際には下のように、ComponentData毎の連続したデータでありEntityはタダのIDでしかありません。Entity2と要求すればA[2]とB[2]、C[2]に簡単にアクセス出来るという感じです。
この辺りはEntityManagerが管理してくれています。

f:id:tsubaki_t1:20180605234922j:plain

SystemはComponentDataの配列にアクセスします。
実際には後述するもう少し面倒な話があるのですが、何にしてもメモリはある程度まとまった所にあるので一気にキャッシュに乗せて一気に処理するといった事が可能になっています。

f:id:tsubaki_t1:20180605235141j:plain

 

アーキタイプとチャンク

ECSのメモリ管理の説明をする上で外せないのが、チャンクとアーキタイプの2つの要素です。基本的にEntityは任意のアーキタイプを持ち、特定のチャンクに格納されます。この辺りは混乱しますよね。えーと、

 

まずアーキタイプとはその名の通りEntityの雛形です。Entityはアーキタイプを元に作成します。またEntityのメモリレイアウト等もココに格納されています。

チャンクは実際にComponentDataを格納するメモリ空間を管理するものです。チャンクは必ずドレかのアーキタイプに所属していて、同じアーキタイプのEntityしか格納しないようになっています。
同じアーキタイプしか格納しないので、もしEntityにComponentDataを足したり減らしたりすると、Entityはアーキタイプが一致するチャンクへ移動します。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604132604.jpg

このチャンクですが1チャンク毎に64KByteの連続したメモリを確保します。ここで確保し利用するのはメモリはネイティブなメモリです。
メモリ確保後は確保したメモリを塗るだけでオブジェクトの増減が表現出来るのでかなり低コストに生成・削除が実現出来る訳です。
また64KByteを使い切ると、新しくチャンクを増やす形で対応します。

ComponentDataArrayは複数のチャンクを一つの配列に見せる

チャンクは複数のアーキタイプ・そして64KByte毎に生成されています。要するに、「特定の種類のComponentData」を扱いたい場合には飛び飛びになってしまっています。

この辺りを使いやすくしてくれているのがComponentDataArrayです。ComponentDataArrayは一見連続した配列のようなインターフェースを持っているのですが、実際にはチャンク毎の「ComponentDataのポインタ」と「要素の数」を持ったリストのようなものです。

要素を取得する場合、下のような処理が行われているっぽいです。

  1. チャンクの先頭のComponentDataを取得
  2. 要素の分だけ横にズラしつつデータを取得
    (連続してるのでキャッシュも効く
  3. 要素を読み終わったら次のチャンクへ(1へ戻る

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604140149.jpg

似たような形でShardComponentDataArrayというものがありますが、こちらは少し異なります。

そもそもShardComponentDataは複数のEntityで一つのShardComponentDataを共有する機能です。そのため、チャンクの中に含まれるのはShardComponentDataの実体ではなくアクセス用の何か(たぶんハッシュ値みたいです。
なおShardComponentDataを含めると値の種類毎にチャンクが分割されます。そのためユニークなShardComponentDataが大量に有る場合、効率が激減する可能性があるので注意が必要です。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604190952.jpg

何にせよ、ComponentDataArray系をunsafeで触ると面倒なことになるかもしれません。

 

ComponentGroupによるフィルタリング

ComponentGroupは特定のComponentDataArraをフィルタリングして特定のComponentDataを持つ物のみのリストを作ってくれる便利機能です。

一致する要素毎に繋いでリストにする感じと予想してますが果たして。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604135443.jpg

なおComponentGroupのタイミングはフレームの最初か最後らへんと聞きましたが果たして。普通にオブジェクトを足したタイミングでチェックしてる印象があるんですが、うーん。

ちなみにフィルタリングコストは結構安いらしいです。これならラベル用のComponentData(0バイトの構造体)とかも普通に使って良さそう。

 

チャンク単位の操作

ComponentDataArrayは内部でチャンクの切替えを行っています。これは簡単にアクセス出来る反面、チャンクを跨ぐような処理を行ったり(componentDataArray[i-1]等)何度もチャンクを舐める処理を行うと効率が悪化していきます。
(例えばfor文で同じComponentDataArrayを何度も最初から最後まで舐める等)

こういったケースではチャンク単位の操作を行うことで、比較的効率的な処理が実現出来ます。

tsubakit1.hateblo.jp

 

メモリのレイアウト

メモリは実際には下のようになっているらしいです。一つのチャンク(64k)に同数のComponentDataが入るように、レイアウトが調整されています。
https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604184817.jpg

このComponentData一つ一つはStructなので構造体の配列(AOS)です。現状ECSはこの形で動作しているみたいです。
ただSIMD等に全部お任せする場合の最大効率は配列の構造体(SOA)と呼ばれる形で、その形が使えるようになってほしい所もあります。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604175232.jpg

この辺りはC# Job SystemとNativeArrayの形で見ると、ComponentDataを分解して配列の構造体として再レイアウトするSOAやAOSOA利用オプションが付くっぽいです。
現状C# Job SystemではNativeArraySOAを使用するという形でSOAを使えそうですが、ECSで利用するオプションは不明(未定)です。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604175800.jpg

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20180604/20180604175815.jpg

 

Systemの実行順はGroup様を見てる

ECSのSystemの実行順番ですが、Systemが要求するComponentGroupを見て、決定しているみたいです。内部的に依存関係グラフが作られてるとか何とか。
例えばデータの読み込み(ReadOnly)は書き込み(WriteOnly)が終わった後に呼ばれるといった具合です。これで「データを入力し、入力されたデータを使って何かをする」の流れを特に意識せずとも実現出来ています。

f:id:tsubaki_t1:20180606014523j:plain

これはJobComponentSystemも同じっぽいです。UpdateAfter等の明示的に順番を制御する機構も、多分同じ仕組みを使ってます。

 

ECSのソースコードはUnity 2018.2から確認できる

 Unity 2018.2からPackagesから取得したパッケージがProjectビューに映るようになり、ECSの内部コードも確認できるようになりました。

内部でどんな処理が行われているか等が確認出来ます。なおunsafe使いまくりなので面倒さがマッハです

f:id:tsubaki_t1:20180604183751j:plain

感想

ということでECSは何をやってるのか?というお話でした。
確かにECSは面倒ですが、逆を言えばこのレベルの最適化を開発者は殆ど意識せず組めるというのが、なかなかに素晴らしい印象です。

とりあえずコレで組んどけば Performance By Default というのは、なる程という感じ。

 

これでUnity開発でよくある、プロジェクト後半・オブジェクトを配置していく段階で想定外の負荷がどんどん増える…というのは避けられるかもしれません
C#実装部分に関しては)

 

参考リンクメモ

DODについて
Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP) – Games from Within

ECSメモリレイアウトについての議論
https://forum.unity.com/threads/ecs-memory-layout.532028/

【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~Entity Component System 編~
https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018-training-daycjobsystem-ecscpu-entity-component-system-1

データ指向設計について
http://tech.cygames.co.jp/archives/2843/

ECSとDODの組み合わせ

Nomad Game Engine: Part 2 — ECS – Nikolai Savas – Medium

コンポーネントベースのEntity Systemについて
CppCon2015/Implementation of a component-based entity system in modern C++ - Vittorio Romeo - CppCon 2015.pdf at master · CppCon/CppCon2015 · GitHub

Unityでのデータ指向のアプローチについて

Unity at GDC - A Data Oriented Approach to Using Component Systems - YouTube

Unity at GDC - C# to Machine Code - YouTube

ECSとJobSystemを使うと上がる問題と対処法
[Unity ECS] All of the Unity’s ECS + Job system gotchas (so far)

以下略(メモ残すの忘れてた