テラシュールブログ

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

【Unity】ECSのSubSceneでISharedComponentDataを使う

f:id:tsubaki_t1:20191025225414j:plain

 この記事はUnity 2019.2.8f1Entities 0.1.1 を利用しています。また設計がイケてないので修正されてしまいそうな感じはありますが、現状SubSceneを試す上で知らないで苦労したのでメモします。

SubSceneでISharedComponentDataが使えない

 ISharedComponentDataはUnityのAssetやPrefabとECS上で使う上で非常に便利な機能なので、これが使えないと正直面倒くささが半端無いです。それ以外にもEntityの属性やユニットのAIを差し替えるといった点でも ISharedComponentDataは便利です。ただ、SubScene上で ISharedComponentDataを使用すると、エラーが出て構築できないという問題が発生する事があります。LiveLink*1を使うにはSubSceneが必要なので、このエラーを何とかする方法を考えてみます。

f:id:tsubaki_t1:20191025233720j:plain

とりあえずISharedComponentDataを使用してみる

 例えば「その場で回転する」と「単純に前進する」という二つの挙動が存在し、それを切り替えるコードを考えてみます。まずデータ構造。単純なISharedComponentDataを継承したデータです。

 何の変哲もないISharedComponentDataです。

[Serializable]
public struct UnitBehaviour : ISharedComponentData { public Mode Value; }

[Serializable]
public enum Mode {
    MoveForward,    // 前方に移動する
    Rotate          // その場で回転する
}

 システムでは該当する UnitBehaviourを持つEntityに対して処理を実行します。ISharedComponentData によりChunkが分けられるので、最小限のデータ取得&同じ処理を連続して実行&Burst最大効率が実現します。モードの切り替え時にコピーが発生しますが、最終的にこっちの方が早い事があります。それも切替対象と範囲と頻度に寄りますが。

using static Unity.Entities.ComponentType;

/// <summary>
/// UnitBehaviourがMoveForwardの時の挙動
/// </summary>
public class UnitSystemMove : JobComponentSystem
{
    EntityQuery query;
    protected override void OnCreate()
    {
        query = GetEntityQuery(ReadOnly<UnitBehaviour>(), ReadWrite<Translation>(), ReadOnly<LocalToWorld>());
        query.SetFilter(new UnitBehaviour { Value = Mode.MoveForward });
    }

    struct MoveJob : IJobForEach<Translation, LocalToWorld> { public void Execute(ref Translation c0, [ReadOnly] ref LocalToWorld c1) => c0.Value += c1.Up * 0.1f; }

    protected override JobHandle OnUpdate(JobHandle inputDeps) => new MoveJob().Schedule(query, inputDeps);
}

/// <summary>
/// UnitBehaviourがRotateの時の挙動
/// </summary>
public class UnitSystemRotate : JobComponentSystem
{
    EntityQuery query;
    protected override void OnCreate()
    {
        query = GetEntityQuery(ReadOnly<UnitBehaviour>(), ReadWrite<Rotation>());
        query.SetFilter(new UnitBehaviour { Value = Mode.Rotate });
    }

    struct RotateJob : IJobForEach<Rotation> { public void Execute(ref Rotation c0) => c0.Value = math.mul(c0.Value, quaternion.RotateZ(1)); }
    protected override JobHandle OnUpdate(JobHandle inputDeps) => new RotateJob().Schedule(query, inputDeps);
}

 それとオーサリングのコードです。上の二つと違いクラス名とファイル名が一致する必要があります。

[DisallowMultipleComponent]
[RequiresEntityConversion]
public class UnitBehaviourComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    [SerializeField] Mode mode;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var behaviour = new UnitBehaviour { Value = mode };
        dstManager.AddSharedComponentData(entity, behaviour);
    }
}

f:id:tsubaki_t1:20191025232828g:plain

 このコードはSubSceneをEditにしている時は正常に動作します。ただしCloseでSubSceneを閉じたり、Rebuild Entity Cacheボタンでデータをリビルドした時にはエラーが発生します。

 例えば上のような値型を持つデータをシリアライズしている場合、Inspector上でデータを変更すると「A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details.」のようなエラーが表示されます。

 またはMaterialのようなオブジェクトを参照するデータを持っている場合「All ComponentType must be known at compile time. For generic components, each concrete type must be registered with [RegisterGenericComponentType].」のようなエラーが表示されます。

 これを解決しなければSubScene上でISharedComponentDataが使えません。

対策1:SharedComponentDataProxyを用意する

 最初にやるべきは、SharedComponentDataProxyを利用する事です。今回の場合、SharedComponentDataProxyを継承したクラスを用意すればエラーが出なくなります。このクラスは当然ファイル名とクラス名が一致する必要がある点に注意が必要です。

public class UnitBehaviourProxy : SharedComponentDataProxy<UnitBehaviour>{}

 ComponentDataProxy!? 馬鹿な…死んだはずでは…!!となりますが、SubScneeではまだ現役で使われています。Assets>EntityCache>Resourcesの中身を見てみると、SharedComponentDataの初期設定以外のデータ一覧が格納されているのを確認出来ます。これは正確にはSection単位ですが、とにかく使われたデータはココにあります。

 正直ここが一番納得行かない事で、今後アセットを参照する形が変わるならココも変わるかもしれません。

f:id:tsubaki_t1:20191025235845p:plain

 一応、SharedComponentDataProxyを用意すればenumやintで挙動を分けるといった用途のISharedComponentDataは動作します。ただマテリアルを参照している場合はコレでは不十分です。

対策2:IEquatableとGetHashCodeを上書きする

 MaterialやMeshなど、アセットを参照するデータを持っている場合、Proxyだけでは不十分です。例えば下のようなデータ構造の場合はエラーになります。

[Serializable]
public struct UnitColor: ISharedComponentData
{ 
    public Material mat; // アセットを参照するデータがある場合
}

「All ComponentType must be known at compile time. For generic components, each concrete type must be registered with [RegisterGenericComponentType].  「意味:すべてのComponentTypeはコンパイル時に認識されている必要があります。 ジェネリックコンポーネントの場合、各ComponentTypeは[RegisterGenericComponentType]で登録する必要があります。」」

 このエラーを解決するには、以下の2つが必要です。

  1. IEquatableを継承し、中身を実装
  2. GetHashCodeをオーバーライドして実装
using System;
using Unity.Entities;
using UnityEngine;

[Serializable]
public struct UnitColor: ISharedComponentData, IEquatable<UnitColor> // IEquatableが必要
{ 
    public Material mat; // アセットを参照するデータがある場合

    // Equalsの実装と、GetHashCodeのオーバーライドが必要
    public bool Equals(UnitBehaviour other) => ReferenceEquals(mat, other.mat);
    public override int GetHashCode() => mat.GetHashCode();
}

 あれRegisterGenericComponentTypeは?となりますが、これが必要なのはジェネリックコンポーネントなので、今回は不要みたいです。

オマケ:RegisterGenericComponentTypeが必要なケース

 IComponentDataを継承したコンポーネントデータがジェネリックなデータを利用している場合、RegisterGenericComponentTypeが必要です。

 例えば下のように、MyData<T>というデータがあり、そこにnew MyData<float3>で格納するといった場合、[assembly: RegisterGenericComponentType(typeof(MyData<float3>))]が必要です。ただラベリングが出来ないのでコレを利用するケースが全く思いつきません。

using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
[assembly: RegisterGenericComponentType(typeof(MyData<float3>))]

// コンポーネント
public struct MyData<T> : IComponentData where T : struct
{
    public T value;
}

// オーサリング

[DisallowMultipleComponent]
[RequiresEntityConversion]
public class NewComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new MyData<float3> { value = new float3(1, 2, 3) });
    }
}

// システム

public class MySystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref MyData<float3> data) => {
            Debug.Log(data.value);
        });
    }
}