Posted at

【Unity】EntityManager.Instantiateを少し便利にする拡張メソッド【小技】

More than 1 year has passed since last update.

丹下健三が今日生まれたから初透光です()


前提環境


  • Unity2018.2.6f1

  • ECS 0.0.12-preview.11


問題提起

ECSにおいてISharedComponentDataはSetする度にチャンク移動を引き起こす割と面倒な存在です。

でもチャンク毎に処理する際にはかなり便利ですから使わざるを得ません。

何千ものEntityを生成して初期化する際にはISharedComponentDataも初期化せねばりませんよね?

その時に個々のEntityに対してEntityManager.SetSharedComponentDataするのは非効率です。

故に雛形となるEntity1つを先に生成してそれに対してのみEntityManager.SetSharedComponentDataした後、EntityManager.Instantiateすると効率が良いのです。

こういう場合は以下の例のようなコードになります。


Spawn114514

void Spawn(EntityArchetype archetype)

{
var template = EntityManager.CreateEntity(archetype);
EntityManager.SetSharedComponentData(template, new MeshInstanceRenderer
{
// 適当な初期化
});
using (var rest = new NativeArray<Entity>(114514 - 1, Allocator.Temp, NativeArrayOptions.UninitializedMemory))
{
EntityManager.Instantiate(template, rest);
for (int i = 0; i < rest.Length; i++)
InitializeEntity(rest[i]); // 適当にIComponentDataを初期化
}
InitializeEntity(template);
}

……IComponentDataを初期化するInitializeEntityが2箇所で呼び出されている点がいただけませんね。

それに、このSpawnメソッドは114514個Entityを生成したいのにそれをストレートに表現しきれていない気がします。


解決策

次の拡張メソッドを定義してあげましょう。


NativeArrayUtility.cs

public static class NativeArrayUtility

{
public static unsafe NativeArray<T> SkipFirst<T>(ref this NativeArray<T> array) where T : struct
{
var dataPointer = ((byte*)NativeArrayUnsafeUtility.GetUnsafePtr(array)) + UnsafeUtility.SizeOf<T>();
var answer = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(dataPointer, array.Length - 1, Allocator.Invalid);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref answer, AtomicSafetyHandle.GetTempUnsafePtrSliceHandle());
#endif
return answer;
}
}

ハイ、unsafe使います。

やっていることとしては引数のarrayのC++ポインタを受け取って最初の一つを飛ばした残りの配列を戻り値にしているだけです。

途中#ifの部分はEditorで実行する際に安全に読み書きを行うためのおまじないです。 正直この部分をこう書くのが正しいのかわからないです。

この拡張メソッドで戻されたNativeArrayはDisposeしてはいけません。

なお、ref拡張メソッドである理由ですが、これは筆者がref教徒なだけであり、別にrefをつけない通常の拡張メソッドであってもよいでしょう。

NativeArray<T>のサイズは16byte程度ですからrefするかどうかギリギリのラインですしね。

using文との食い合わせを考えるとrefを使わないのも十分にアリだと思います。

in拡張メソッドにしないのはメソッド中でarray.Lengthという風にプロパティを呼び出しているからです。

非readonly structであるNativeArray<T>のプロパティを呼び出すと無駄コピーが発生してしまいますから避けるべきでしょう。LengthプロパティがLengthフィールドになるIL2CPP環境でどうなるのかは未検証なのでわかりませんが、よしたほうが良さそうです。

さて、上記拡張メソッドを定義した場合最初の例は以下のように書き換えられます。


改良版

void Spawn(EntityArchetype archetype)

{
var array = new NativeArray<Entity>(114514, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
try
{
array[0] = EntityManager.CreateEntity(archetype);
EntityManager.SetSharedComponentData(array[0], new MeshInstanceRenderer
{
// 適当な初期化
});
EntityManager.Instantiate(array[0], array.SkipFirst());
for (int i = 0; i < array.Length; i++)
InitializeEntity(array[i]); // 適当にIComponentDataを初期化
}
finally
{
array.Dispose();
}
}

かなりスッキリしましたね!


感想

ECSもっと盛り上がって、どうぞ。