Edited at

SharedComponentDataのIndex管理手法について

More than 1 year has passed since last update.

今日が朱子の誕生日なので初投稿です。


前提環境


  • Unity2018.3.0b5

  • ECS version0.0.12-preview.18


問題提起 SharedComponentDataManager絶対許早苗


SharedComponentDataとは

SharedComponentDataはComponentDataと異なり参照型をフィールドに持てます。

また、EntityManager.SetSharedComponentDataを行うとほぼ必ずChunk移動または生成が行われます。

そして、マルチスレッドなC# Job Systemで扱う方法をECS標準では用意していません。

そんなSharedComponentDataをEntityから得る方法は2つあります。


  • SharedComponentDataArray

  • EntityManager.GetSharedComponentData

EntityManager.GetSharedComponentData(int sharedComponentIndex)が一番性能的にマシですが、やはり使うべきではないです。

Dictionary<int, T>を用意してそれにキャッシュするのが一番いいのです。

ただ、sharedComponentIndexは割とコロコロ変わる可能性があります。

struct A : ISharedComponentData { public int Value; }

readonly List<A> aList = new List<A>();
readonly List<int> indexList = new List<int>();

void OnUpdate()
{
var archetype = EntityManager.CreateArchetype(ComponentType.Create<A>());
var e = EntityManager.CreateEntity(archetype);
void Set(Entity entity, int value) // ローカル変数のキャプチャを避けているが、これは
{
EntityManager.SetSharedComponentData(entity, new A{ Value = value });
aList.Clear();
indexList.Clear();
EntityManager.GetAllUniqueSharedComponentData(aList, indexList);
for(int i = 0, end = aList.Count; i < end; ++i)
Debug.Log($"{indexList[i]}:{aList[i].Value}");
}
Set(e, 1);
// 0:0
// 1:1
Set(e, 16);
// 0:0
// 1:16
Set(e, 1023);
// 0:0
// 1:1023
var e1 = EntityManager.CreateEntity(archetype);
Set(e1, 1);
// 0:0
// 1:1023
// 2:1
Set(e1, 1023);
// 0:0
// 1:1023
}

SharedComponentDataIndexは以上のような振る舞いをします。

Indexと値の対応がコロコロ変わるのを防ぐにはどうすればいいのでしょうか?


解決策

Prefabコンポーネントを使います。

Indexを固定するにはその値を持つEntityが最低1つ存在すればいいのです。

Index固定用Entityは通常のComponentSystemの処理対象に含めたくないのです。

Prefabコンポーネントを持つEntityはComponentSystemの処理対象外になります。

これを利用して次のようなコードでDictionary<int, T>を初期化すればかなり性能良くSharedComponentDataを取得できます。

TがBlittable型ならばNativeHashMapという選択肢も十分にアリだと思います。

[SerializeField] MeshInstanceRenderer[] renderers;

MeshInstanceRenderer[] As;
int Offset;

void Start()
{
InitializeWorld();
InitializeSharedComponent(renderers);
}
void InitializeSharedComponent(MeshInstanceRenderer[] array)
{
var manager = World.Active.GetExistingManager<EntityManager>();
var archetype = EntityManager.CreateArchetype(ComponentType.Create<MeshInstanceRenderer>(), ComponentType.Create<Prefab>(), ComponentType.Create<Position>(), ComponentType.Create<Rotation>(), ComponentType.Create<Scale>(), ComponentType.Create<LocalToWorld>());
var entities = new NativeArray<Entity>(array.Length, Allocator.Temp);
manager.CreateEntity(archetype, entities);
for(int i = 0; i < entities.Length; ++i)
manager.SetSharedComponentData(entities[i], array[i]);
entities.Dispose();

var aList = new List<MeshInstanceRenderer>(array.Length + 1);
var indexList = new List<int>(array.Length + 1);
EntityManager.GetAllUniqueSharedComponentData(aList, indexList);
int max = Offset = indexList[1];
for(int i = 2, end = indexList.Count; i < end; ++i)
{
if(indexList[i] > max) max = indexList[i];
else if(indexList[i] < Offset) Offset = indexList[i];
}
As = new MeshInstanceRenderer[max - Offset + 1];
for(int i = 1, end = indexList.Count; i < end; ++i)
As[indexList[i] - Offset] = aList[i];
}

void OnDestroy() => World.DisposeAllWorlds();

上のコードでは同期的に連続的にSetSharedComponentDataした場合SharedComponentIndexが連続した値を取りやすいという性質を利用して配列に格納しています。

こうするとC# Job Systemからでも以下の例のようにSharedComponentDataを利用しやすくなります。


ComponentSystem

struct A : ISharedComponentData { public float Value; }

A[] As; // これとoffsetはコンストラクタから初期化する
int offset;
ComponentGroup g; // これはOnCreateManagerで初期化する

protected override void OnUpdate()
{
fixed(A* ptr = &As[0])
{
new Job{
As = ptr,
offset = offset,
ATypeRO = GetArchetypeChunkSharedComponentType<A>(),
PositionTypeRW = GetArchetypeChunkComponentType<Position>(),
}.Schedule(g).Complete();
}
}

[BurstCompile]
struct Job : IJobChunk
{
[ReadOnly] [NativeDisableUnsafePtrRestriction] public A* As;
public int offset;
[ReadOnly] public ArchetypeChunkSharedComponentType<A> ATypeRO;
public ArchetypeChunkComponentType<Position> PositionTypeRW;
public unsafe void Execute(ArchetypeChunk chunk, int chunkIndex)
{
var aIndex = chunk.GetSharedComponentIndex(ATypeRO);
if(aIndex == 0) return;
NativeArray<Position> positions = chunk.GetNativeArray(PositionTypeRW);

// Positionはfloat3と同一構造である
var ptr = (float3*)positions.GetUnsafePtr(ATypeRO);
for(int i = 0; i < positions.Length; ++i, ++ptr)
ptr->y = As[aIndex - offset].Value;
}
}



感想

SharedComponentDataはやっぱり使いにくいですね。