テラシュールブログ

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

【Unity】ECSで配列を格納する Dynamic Buffers

今回はECSで配列を使用する方法についてです。

ECSで配列を使用する

ECSに格納するComponentDataには配列を格納出来ません。とはいえ、マネージドなメモリも格納出来るSharedComponentDataは大量の種類を生成するとECSの効率が著しく下がるので余り良くありません。
とはいえ、 "接触したキャラクターの一覧"や"NavMeshのパス"など、連続した(可変の)データを必要とするケースはソコソコ多いです。

今回はDynamic Buffersを使用してこの問題を回避します。
下のような感じで特定のEntityが他のEntityを追跡する機能を作成してみました。中央のシリンダーが他の3つのEntityへの参照を保持していて、その参照経由でEntityへの線を引きます。

f:id:tsubaki_t1:20181107223223g:plain

作ってみる

まず線を引く対象となるEntity群を用意します。コードで配置するのが面倒くさいのでハイブリットです。

GameObjectEntityとPositionComponentを用意します。 Transformの位置とPositionを同期したいのでCopyTransformFromGameObjectでPositionにTransformの座標を毎フレーム登録してもらいます。
今回はこれを青い枠で囲んだオブジェクト全てにセットしています。

f:id:tsubaki_t1:20181107224104j:plain

f:id:tsubaki_t1:20181107224627j:plain

線を引くためにEntityに他のEntityを登録するコードを用意します。

最初にバッファを定義します。IBufferElementDataを継承したstructに含めたい要素を登録します。今回は複数のEntityを登録することが目的なのでEntityです。
設定後はInternalBufferCapacityで配列の長さを設定します。下の図の場合は配列に4つの要素が登録出来ます。
長ければ長いほど、チャンクに格納出来るEntityの数が減ります。オーバーしても良いですが、その場合はヒープメモリとして処理されるらしいです。

f:id:tsubaki_t1:20181107231523j:plain

f:id:tsubaki_t1:20181107231651j:plain

あとはバッファをEntityに追加して、要素を登録します。

アーキタイプから作っている場合はComponentTypeで一発で足せるのですが、ハイブリットはザクっと見た感じバッファを最初から持てなかったので、entityManager.AddBuffer(entity)でバッファを追加しています。
追加後はバッファを取得し、中身を埋めていきます。

f:id:tsubaki_t1:20181107232215j:plain

gist.github.com

あとは線を引くシステムを用意します。

まずグループですが、普通にComponentTypeが使えます。今回の場合EntityBufferを持つEntity一覧を取得しています。

f:id:tsubaki_t1:20181107233217j:plain

次にグループからバッファを取得します。GetBufferArrayで取得するわけですが、Entity単位ではなくグループ単位で持ってきます。
例えばEntityBufferを持つEntityが2つある場合、下のように取得します。

buffer[0] { entity[0], entity[1], entity[2], entity[3] },
buffer[1] { entity[0], entity[1], entity[2], entity[3] },

Reinterpret()は、取得する要素を別の型へ解釈するという機能です*1。内容物が同じレイアウトなら別の型として使用することが出来ます。今回の場合、中身がEntityのみなのでEntityの配列と解釈して処理させています。

f:id:tsubaki_t1:20181107233942j:plain

あとは使用する部分ですが、まんま配列としてアクセス出来ています。 もしパラレルで並列処理したい場合は、ToNativeArrayでバッファをNativeArrayとしてアクセスするのが良いかもしれません。
今回の場合、別スレッドだとEntityから要素を取得できないので、ジョブは使用していません。

f:id:tsubaki_t1:20181107234010j:plain

gist.github.com

サンプル2

Dynamic Bufferに格納した複数の要素を、毎フレーム別スレッドで加算するだけ

gist.github.com

関連

Dynamic Buffersの説明

https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/reference/dynamic_buffers.mdgithub.com

*1:実験的な機能

【Unity】SpriteAtlasでパックしたスプライトをUIに使用したときに出るゴミの対処

SpriteAtlasを始めとしたパッキングのアプローチを使用することで、スプライトの描画時に必要なパスの数が減り、パフォーマンス的に良い感じになります。
よく言われるDrawCallやSetPassの削減というやつです。

さて、SpriteAtlasでスプライトをパッキングした後、スプライトをUIに乗せると、下のような謎の絵が紛れ込む事があります。
今回はその対処についてです。

f:id:tsubaki_t1:20181105221418j:plain

 

 

キャラクターの背後に出現した不可解な顔、その正体とは…!?

正体はSpriteAtlasでスプライトをパックした時に詰め込まれた他のスプライトです。幽霊の正体見たり枯れ尾花。
UIは基本的に矩形で切り出されるので、下のようにタイトな設定で詰め込むと矩形の範囲に他のスプライトが入り込み、心霊写真のように他のスプライトが紛れ込んでしまいます。

f:id:tsubaki_t1:20181105222027j:plain

 

対策1:タイトパッキングを外す

この非常に単純な解決法は、UIに使用するスプライトのタイトパッキングを外す事です。タイトパッキングを外せば矩形の余裕を持ってパックするので、UIで取り出した時に他のスプライトが紛れ込む事はありません。

f:id:tsubaki_t1:20181105222349j:plain

 

対策:UIでUse Sprite Meshを使用する

Unity 2018.3から新しい対策が追加されました。UIコンポーネントでUse Sprite Meshを使用することです。

この設定を有効にすると、UIが利用するメッシュが矩形ではなくスプライトのポリゴンを使用するようになり、タイトなパッキングを利用した場合もゴミが出なくなります。

f:id:tsubaki_t1:20181105223045j:plain

当然、表示にかかる頂点数も跳ね上がりバッチの為のコストも上がります。が、特にキャラクターをUIで表示するような場合は「頂点数」より「隙間」が生む透明塗りつぶしの部分の方が問題になるかもしれません。

なお、Use Sprite Meshが使用できるのは「シンプル」のみです。

 

関連

下のをUIで作ると、今回の現象が起こります

tsubakit1.hateblo.jpポリゴンは最初は超高解像度なので、適当に間引いておくと良いです。

tsubakit1.hateblo.jp

SpriteAtlasという機能について

tsubakit1.hateblo.jpUIとSpriteの使い分け

tsubakit1.hateblo.jp

【Unity】Enumeratorを使用して、List<string>から文章を一つずつ取り出す

f:id:tsubaki_t1:20181105001137g:plain

今回はEnumeratorを使用して文章を一つずつ取り出していきます。
余りに使わない技術なのでよく忘れるのですが、その度に思い出すのも面倒なのでココにメモしておきます。

 

 

リストから要素を取り出す

配列やリスト等の要素を取り出すのは非常に単純で、配列(Array)に添え字(Index)を渡してやれば良いです。

なので”文字列のリストから要素を順次取り出す”という観点で言えば、下のようなコードでも十分に問題ありません。

messageBox.text = messages[current];で現在の文字列を取り出し、current = Mathf.Min(current + 1, messages.Count - 1);で添え字を文字列の長さ以上にならないように判定しつつ更新…という感じです。

gist.github.com

 そうだ、Enumeratorを使おう

大体の場合は上記の方法を使うのですが、時々思いつきます。そうだEnumeratorを使用して要素を取得しようと。

まずprivate List<string>.Enumerator enumeratorを使います。IEnumerator enumeratorではありません。これで毎回キャストするのを回避出来ます。

あとはmessages.GetEnumerator()でListからEnumeratorを取得し、enumerator.MoveNextで要素を一つずつズラしていきます。

 

gist.github.com

使えるのはList

なお、この方法でサクッと取得できるのはListのみです。List<string>.Enumeratoを使用しているのもあります。やり方は有るのかもしれないですが、知りません。

UnityでInspectorに表示する目的だと、ArrayもListも挙動が似たり寄ったりなので差し替えるのは簡単ですが、Inspector以外で使用する場合は色々と考えて選択されていることも多いので注意です。

 

感想

何故か定期的に使いたくなるけど、毎回忘れてるのでメモ。

 

関連

tsubakit1.hateblo.jp

【Unity】ECSでシーンをロードする様にEntity群をロードする

今回はECSにおけるEntityのセーブ及びロードについてです。
つまりGameObjectをSceneやPrefabに固めるように、Entity(Entity & ComponentData)をファイルとして書き出します。

なおUnity 2019辺りのタイミングでちゃんとしたエディターサポートが始まるので、多分今回紹介する内容は良くてローレベルAPI、悪くて産廃となりそうです。ただまぁ、一応調べたのでメモとして残しておきます。

Entityをファイルに保存

現状、Entityの生成はスクリプトから行われます。
ただ昨今の常識的なゲーム開発環境では、ゲーム内オブジェクトの構築を全てスクリプトで制御するのではなく、ある程度メタファイルに情報を書き込んでロードする形を取ります。UnityのGameObjectやScene、Prefab等がコレに該当しますが、残念ながらECSはエディターレベルでのサポートがまだないので、スクリプト全フリとなるのは仕方のない事です。 とはいえ、単一種類のEntityをランダムに配置するもの(ECSでよく見る!)なら兎も角、ちゃんとしたゲームにしたいならばゲーム内に任意のEntityを配置し、それをゲーム実行時にロードするといった事は欲しい所でしょう。

なので今回はECSで配置したEntityの情報を記録(セーブ)しておき、ロードするといった方法を紹介します。

なお幾つか制限があります。

  • Pure ECS向けの機能です。
  • GameObjectEntityをシーンに配置した(つまりEntityにTransform等への参照が有る)物には使えません。
  • GameObjectEntityをEntityManager.Instantiateしたものには使えます。

実装

Entityの生成とセーブ

まずはEntityの生成と保存をやってみます。

生成は良いでしょう。何時ものランダム生成です。問題は保存の方で、SerializeUtilityHybrid.Serializeを使用します。 最初の引数にはEntityManagerを指定しますが、この中にハイブリットな(Componentを含む)Entityが存在すると失敗します 。場合によっては処理用のWorldにEntityを移してからファイルに書き込む等の工夫が必要かもしれません。もしくはPreBuildProcessでビルドする際に色々とやるか。

gist.github.com

これを実行すると、下のようにSpawnObjectにセットしたオブジェクトが大量に生成されるので、Saveを呼び出してファイルに書き出します。

ここで重要なのは、 Saveを押したタイミングで生成されるSharedComponentsオブジェクト です。このオブジェクトをProjectビューへドラッグ&ドロップしてPrefab化します。
意外と知られていない事ですが、ゲームの再生中であってもオブジェクトをドラッグ&ドロップすればPrefab化出来ます。Prefabの上書きで「ゲーム再生中の変更をなかったコトにしない」為のテクニックの一つです。

f:id:tsubaki_t1:20181102233325j:plain f:id:tsubaki_t1:20181102233608g:plain

SharedComponentsオブジェクトの中身は保存した時に利用していたSharedComponentDataの一覧です。SharedComponentsオブジェクトは登録されているコンポーネントの並び順が非常に重要なので、保存するたびにPrefabはちゃんと更新しておくことをお勧めします。

f:id:tsubaki_t1:20181102234639j:plain

セーブしたEntityをロードする

次にファイルに書き出したEntityをロードしてみます。

EntityのロードはSerializeUtilityHybrid.Deserializeを利用します。三番目の引数のsharedDataには、セーブのタイミングでPrefab化したSharedComponentsオブジェクトを指定します。

なおロードできるのは空のWorldのみなので、空のWorldを一旦生成した後MoveEntitiesFrom異世界転移してもらう形を取ります。転移すればWorldは基本的に空になるので、一旦Worldを作成した後は使い回すのが良いです。

gist.github.com

f:id:tsubaki_t1:20181102235120g:plain

なおEntityは同期的に生成されます。

f:id:tsubaki_t1:20181103000648j:plain

セーブしたEntityを非同期でロードする

上でも書いたとおりSerializeUtilityHybrid.DeserializeのEntityのロードは同期的です。せっかく別のWorldにEntityを生成するので、今度は非同期でロード出来るように変更します。

まず最初にSerializeUtilityHybrid.DeserializeSharedComponentsでsharedDataに登録されているSharedComponentDataをEntityManagerに登録しておきます。
次にEntityをロードする部分ですが、SerializeUtility.DeserializeWorldSerializeUtilityHybrid.ReleaseSharedComponentsentityManager.BeginExclusiveEntityTransaction()で取得したExclusiveEntityTransactionを使用してる辺りから察せられる通り、非同期でも動かせます。

あとは 【Unity】ECSのEntityの作成やComponentDataの追加等の操作を別スレッドから実行する - テラシュールブログ でやったような事と同じで、非同期で別WorldにEntityを作り、最後にメインのWorldに同期してやれば、殆ど非同期でロードが完了です。

gist.github.com

f:id:tsubaki_t1:20181103001615j:plain

感想

Entityのロードでした。
今回はランダムなオブジェクトを生成しましたが、実際にはGameObject&ComponentをEntity&ComponentDataに変換して保存…とすれば、かなり良い感じに動作するのではないかなと思います。コレとか正にそのノリです

www.youtube.com

ただEntityのエディターワークフローがもう少しマシになれば多分APIも追加されるでしょうから、今回の内容は「繋ぎ」程度に考えたほうが良さそうな気がします。

追記:こちらの機能でワークフロー対応したSubSceneが追加されました。

tsubakit1.hateblo.jp

関連

非同期でEntityの生成

tsubakit1.hateblo.jp

Worldという概念について

tsubakit1.hateblo.jp

ハイブリットECS

tsubakit1.hateblo.jp