テラシュールブログ

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

【Unity】ECSでチャンク単位のバッチ処理を実現するChunk Iteration、それとComponentGroup

最近のECS界隈で特に理由もなくChunk Iterationを採用されている事をよく見るので、今回そのChunk Iterationついて書いてみます。

ComponentDataの組み合わせの爆発

ECSは基本的に「特定のComponentDataの組み合わせ」で処理する対象を判定しています。Interfaceのような特定の型ではなく組み合わせです。
例えば下の図では「人力で移動するシステム」と「エンジンで動くシステム」があり、荷物のComponentDataを保持している場合には荷物に関する処理が追加されます。 System要求するComponentDataを持つEnittyが処理されていきます。この要求は「全てのブツを所持している(AND)」及び「該当のブツを所持していない(NOT)」です。

f:id:tsubaki_t1:20181016191933j:plain

さて上の図で少し気になるのが「人力」と「エンジン」のシステムです。基本的に動き自体は人力だろうとエンジンだろうと本質的には同じ「移動システム」です。コードで比較すると、多分殆ど同じようなコードになります。

ここで動力の種類の数だけSystemやComponentGroupを用意すると同じようなグループのクエリーが大量に生成されることになるので余り宜しくないです。Interfaceで抽出出来るなら楽なのですが、ComponentData単位でしか取得出来ません。

f:id:tsubaki_t1:20181016230302j:plain

そこでECSでは「OR」を新しく追加して大雑把にEntityの組み合わせを取得、処理内で行うべき処理を割り振る機能を追加しました。
それがEntityArchetypeQuery、そしてChunk Iterationという機能です。

Chunk Iterationという回避方法

考え方は割と単純です。

ComponentGroupを要求する際に緩い条件で検索をかけて、内包するComponentDataの組み合わせに応じて処理を切り替えを行うだけです。
これで要求するGroupの種類が減らせます。

検索に使用できるのはこの3種類

  • AND(全てのブツを含む)
  • OR(指定したブツが含まれる)
  • NONE(指定したブツを含まない)

例えば下の場合、今までは「人力&タイヤ」OR「エンジン&タイヤ」の組み合わせで取得していましたが、これを「(人力 OR エンジン) & タイヤ」で取得します。そして処理を行う際、保持するComponentDataが「人力」か「エンジン」かを確認し処理を切り替えます。

f:id:tsubaki_t1:20181016195416j:plain

このように複数のComponentDataを許容すると処理を行う際に1Entity毎に「実行する処理の内容を切り替える」事になりそうですが、そこらへんは大丈夫になっています。

ECSでは基本的にComponentDataの組み合わせ(archetype)毎にまとめて配置されています。この塊はチャンク(Chunk)と呼ばれます。同じチャンク内なら全て同じComponentDataの組み合わせであることが保証されています。

要するに、処理の切替えはEntity毎ではなくチャンク毎です。

f:id:tsubaki_t1:20181016201957j:plain

tsubakit1.hateblo.jp

ということで、このChunkの処理はじめに指定のComponentDataを持っているかチェックし、処理を切り替えを行っていくことで効率的なバッチ処理を実現しています。

これ逆を言えば、ORを使用しないのであれば(付属するComponent次第で処理を切り替えしないならば)ComponentGroupを素直に使ったほうが多分良いです。

また効率で言えばIJobProcessComponentDataを使うのが良いそうです。

コードを書いてみる

詳しい記述方法は 【Unity】Entity Component System入門(その2)【2018.2】 で紹介されています。この書き方は色々と応用が効きそうですが正直この書き方は苦痛なので自分はIJobChunkを使用して楽します。

まずEntityを取得する部分ですが[Inject]ではなくGetComponentGroupを使用します。その際にEntityArchetypeQueryを使用すると取得時にORを指定できます。

f:id:tsubaki_t1:20181016220857j:plain

次はJob側の定義です。
ジョブの取得にはIJobChunkを使用します。こちらはグループを渡すと、グループが参照するチャンクの数だけ並列で処理を実行してくれます。

取得したチャンクにHasを使用して、指定するチャンクが任意のComponentDataを保持しているかチェックします。
あとはComponentDataの有無に応じて処理を分けます。

f:id:tsubaki_t1:20181016222257j:plain

あとはジョブを実行します。

IJobChunkで実行する場合、ComponentGroupを渡せば良いのでかなり楽が出来ます。
この時、チャンクのComponentDataにアクセスするためにGetArchetypeChunkComponentType()を使用します。これはEntityManagerにも同名のAPIがありますが、必ずComponentSystemのものを使用します。 自分はこのトラップに引っかかり「InvalidOperationException: The previously scheduled job」を散々見る事になりました。

f:id:tsubaki_t1:20181016225352j:plain

全文

gist.github.com

感想

Chunk Iterationでした。

利用するシーンの多い良い機能ではありますが、無条件で使うべきものでもないので、状況次第で使っていくのが良さそうです。

関連

Chunk Iterationのマニュアルです。

EntityComponentSystemSamples/chunk_iteration.md at master · Unity-Technologies/EntityComponentSystemSamples · GitHub

Chunk Iterationについてディープな解説をしている記事です。

【Unity】Entity Component System入門(その2)【2018.2】

【Unity】Auto Sync Transformと移動時の判定、それと注意点

たぶん2017.2辺りからAuto Sync Transformという項目が追加されました。 この設定はパフォーマンスの観点からOFFが推奨ですが、ONにすべきタイミングもあるかもしれないので、その辺り少し紹介です。

Transform変更時に即座にColliderに反映させるAuto Sync Transform

Auto Sync TransformはProject SettingsのPhysicsの設定項目の一つです。
この項目をONにすると、Transformで座標を変更するたびにColliderの情報を更新するというものです。
つまり同じTransformを何度も操作するようなコードを記述していると、もしかしたら無駄な負荷を生じさせているのかもしれません。

docs.unity3d.com

f:id:tsubaki_t1:20181005231406j:plain

確認のため下のようなコードを書いてみます。

gist.github.com

このコードを実行した時、Auto Sync Transformの有無で結果が変わります。

ONだった場合、座標の変化は即座に反映するので4.5fという数値になります。逆にOFFだった場合、前フレームの最終的な座標である9.5fという数値になります。

つまりAuto Sync TransformがOFFの場合、対象の座標移動後にRaycast等のAPI接触結果を取得するために1フレーム待たなければならないという事です。
更新のタイミングはFixedUpdate後です。

テストを使用している場合に注意

これが問題になるのは、テストの時かもしれません。
例えば下のようにテスト時にCollider付きGameObjectを生成している場合、Auto Sync Transformの有無により結果が変わります。

gist.github.com

Auto Sync TransformをONにしていると全てのテストは通りますが、OFFだと生成直後や動かした直後にyield return null;を挟まないと反映されません。

f:id:tsubaki_t1:20181006005342j:plain f:id:tsubaki_t1:20181006005354j:plain

更に驚くことに、この結果はRun IN Playerを実行した場合にも変化します。
問題となるのは「1フレーム待つ」のテストが失敗している事で、その原因は「オブジェクトが生成されていない」となります。

f:id:tsubaki_t1:20181006005821j:plain

この原因はyield return null;だとFixedUpdateまで到達しないことじゃないかなと思います。試しにyield return null;yield return new WaitForFixedUpdate();に変更したところ、エディターでのテストと同じ結果になりました。

f:id:tsubaki_t1:20181006010153j:plain

後はテスト時に前のテストのゴミが残るかなーと予想していたんですが、とりあえず試した感じは残りませんでした。
はて?

感想

Collider関連をテストする時には、Auto Sync Transformは有効にしとくのが良さそうです。
逆に普通に動かす場合、Auto Sync TransformがOFFなのは理にかなっていそうです。でも当たり判定をPhysics系APIを基本としている場合は少しトラップになるかな?

軽い気持ちで調べたら意外と深刻だったでござる

【Unity】新・AnimatorのGameObjectを非アクティブにするとステートマシンがリセットされる問題の対処法

f:id:tsubaki_t1:20181004235357j:plain

以前、下のリンクで紹介した「AnimatorのGameObjectを非アクティブにするとステートマシン(その他諸々)が破棄される」問題の、対処法です。

tsubakit1.hateblo.jp

Animator.keepAnimatorControllerStateOnDisable

新しい解決方法は簡単で、animator.keepAnimatorControllerStateOnDisable = true;を呼び出すだけです。これでオブジェクトが非アクティブになった時でもステートマシンがリセットされません。

docs.unity3d.com

keepAnimatorControllerStateOnDisableはシリアライズが可能

ここで驚くべきというか面白いというかユニークなのは、Animator.keepAnimatorControllerStateOnDisableという設定、実はシリアライズが可能という事です。 エディター拡張等でAnimatorを持つPrefab郡に対して片っ端からanimator.keepAnimatorControllerStateOnDisable = true;を実行してやれば、初期化コード等が無くとも非アクティブ時のステートマシン破棄を防ぐ事が出来ます。

なおInspectorをDebugにすると、Inspector側からでも設定が可能です。

f:id:tsubaki_t1:20181004235930j:plain

何故か言語設定を「日本語」にしてると、Inspectorで表示される項目名が全く違う名前(アニメーターコントローラーレイヤー)になってるので、そこんとこ注意です。

追記: WriteDefaultは相変わらずリセットされる(正確にはアクティブ時に書き込まれてしまう)ので、GameObjectのアクティブ切替はまだ注意が必要です。

tsubakit1.hateblo.jp

配列を使わずシューティング、またの名を初心忘るべからず

Twitterのタイムラインで「配列を知らずにシューティングゲーム作る人もいた」というパワーワードを聞いてから頭から抜けなかったので、作ろうとしてみました。
テーマは初診わするべからず(始めた頃のダメダメっぷりを忘れるべからず)

配列を使わずシューティング

作ろうとしてみました…というか、配列を使わずシューティングは自分も以前に作った記憶があります。 確か高校の展示で何となくパソコン使いたかったのでゲームを作ってみた的な。コンピューター部のPC借りてコッソリ作ってました。

最終的にはシューティングという名の別のなにか(回避ゲーム)になった事は覚えています。あと画像が用意出来なくて文字で全部代用したことも。

弾の定義

まず最初の前提として配列を使わず弾を動かします。今回使ったのはUnityですが昔の自分はコンポーネント指向なんて間違いなく理解してなかったので、今回はMain.csを用意してソコに全部書きます。最初の自分はそうしてました。

まず複数の動く弾が存在します。コレを解決する画期的な方法は、変数をたくさん用意することです。 ゲームを初めて作ったときには構造体とかクラスも知らなかったので(というか言語的になかった)座標だけです。

f:id:tsubaki_t1:20181003214506j:plain

おおっと名前が酷い? 最初の自分は確かにこう書いてました。それで変数がどれがどれかわからなくなった的な。HAHAHA

まぁ実際にはpppaとかppaとかppbそうのを合わせて100発分は作ったハズです。リネーム機能?知らない子ですね。
あとは記憶ではxとyが独立した変数として存在したので、その管理も割と大変なことになっていたという記憶ががが

弾を移動させる

次に弾を移動させます。まぁ移動方法は固定で下に移動するだけです。 そこで昔の自分は華麗に関数で移動処理を実装します。
自分の記憶での移動の関数名はコレだった気がします。日記には、なにか画期的な意味があったが思い出せんというコメントを書いたような、書かなかったような。

f:id:tsubaki_t1:20181003215334j:plain

しかし、この関数だけでたくさんの弾を移動する事は出来ません。どうするかって?こうするんだよ

f:id:tsubaki_t1:20181003215513j:plain

for文が使えないなら、その数だけ処理を繰り返せばいいじゃない。
ちなみに当たり判定も似たような感じです。

f:id:tsubaki_t1:20181003215711j:plain

戻り値が無いのは一発即死だからです。コードで言うとApplication.Quit()

感想

こんな感じでクッソ簡単なゲームを作ろうと思いましたが、流石に飽きたので終了。 恐るべきはこのノリでゲームを作りきってしまった昔の自分か

まぁ、あの時文化祭に展示して作る楽しさを知ったからこそ、今の自分があるわけで。今思うとアレだけど良い思い出です。作りきってなかったら多分いまごろ自分は別のことをしています。

勉強して効率的に作れる事も大切ですが、まず作ってみる…というのが自分にとって良い勉強になりました

f:id:tsubaki_t1:20181003220530g:plain

え?コード全文? 勘弁して下しあ