テラシュールブログ

旧テラシュールウェアブログ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);
        });
    }
}

【Unity】structでも拡張メソッドを使用する

f:id:tsubaki_t1:20191023221221j:plain

対応するAPIを探すのが若干面倒くさい

 DOTS系テクノロジーの殆どはstruct(構造体)を利用しており、構造体自体にメソッドを追加していることは殆どありません。大抵の場合、Utility系のAPIが幅を利かせており、利用する場合には対応する構造体に対応するUtility、APIに対応するデータを渡す必要があります。

 例えば「オブジェクトを回転させる」という場合、少なくとも「Unity.Mathematics.quaternion.RotateYで回転行列を作る」「Unity.Mathematics.math.mulはquaternionにも対応している」という情報を知らなければ作るのは割と面倒くさいです。回転するジョブは下のような感じ。

struct RotationJob : IJobForEach<Rotation, RotationSpeed>
{
    public void Execute(ref Rotation c0, ref RotationSpeed c1)
    {
        c0.Value = math.mul(c0.Value, quaternion.RotateY(c1.Value));
    }
}

 しかもECS初期コードではusing static Unity.Mathematics.math;が設定されてるせいでquaternionクラスが即使えないっていう。

f:id:tsubaki_t1:20191023225535j:plain
Rotatoinの中身を確認しても、何をどうすればよいのか良くわからん

拡張メソッドで拡張する

 これを拡張メソッドを使用して簡単に記述できるようにしてみます。構造体の拡張メソッドも基本的には通常の拡張メソッドと同じで、 static クラス内に定義したstaticメソッドであることメソッド引数の最初にthis を使用 の二つです。

struct RotationJob : IJobForEach<Rotation, RotationSpeed>
{
    public void Execute(ref Rotation c0, ref RotationSpeed c1)
    {
        // c0自身を変更するのではなく、戻り値で値を更新
        c0 = c0.RotateY(c1.Value); 
    }
}

// 拡張メソッドを格納するクラス
public static class EXClass
{
    /// <summary> RotatoinをY軸に回転する </summary>
    /// <param name="angle">速度(rad)</param>
    /// <returns>回転後のRotatoin</returns>
    public static Rotation RotateY(this Rotation q, float angle)
    {
         q.Value = math.mul(q.Value, quaternion.RotateY(angle));
        return q;
    }
}

 入力補完が効くので、一覧からできることを指定するだけで使えます。

f:id:tsubaki_t1:20191023222717j:plain
入力補完で何ができるのかを表示してくれる

構造体と拡張メソッドの参照渡し

 構造体なので基本的にメソッドが使用するデータはコピーしたデータです。なので、クラスの拡張メソッドと異なり値を返していました。例えば下のコードは正常に動作しません。これが単純にstruct内部にメソッドを定義していたら話は別なのですが、拡張メソッドは外部のstaticメソッド扱いなので、構造体自身に反映されません。

struct RotationJob : IJobForEach<Rotation, RotationSpeed>
{
    public void Execute(ref Rotation c0, ref RotationSpeed c1)
    {
        // c0は更新されていないので、Entityは回転しない
        c0.RotateY(c1.Value); 
    }
}

public static class MathEX
{
    public static void RotateY(this Rotation q, float angle)
    {
        // qはコピーした値なので、これを変更しても反映されない(Rotation内で定義した場合は話は別)
        q.Value = math.mul(q.Value, quaternion.RotateY(angle));
    }
}

   とはいえ、構造体が巨大だったり、メソッドで内部データを書き換えたい場合もあります。そんな時はinrefを使用できます。試したら出来ました。これが理に適っているかは微妙な所ですが、まぁ出来たので問題なく。ref を使えば、戻り値の構造ではなく直接構造体を操作出来ます。これを許容出来るかとかは兎も角として、出来ます。

struct RotationJob : IJobForEach<Rotation, RotationSpeed>
{
    public void Execute(ref Rotation c0, ref RotationSpeed c1)
    {
        // refを使用しているので、c0の中身を書き変わりEntityは回転する
        c0.RotateY(c1.Value); 
    }
}

public static class MathEX
{
    public static void RotateY(this ref Rotation q, float angle)
    {
        q.Value = math.mul(q.Value, quaternion.RotateY(angle));
    }
}

 単純にデータをコピーしたくない場合はin が使えます。例えば下のコードの場合、LocalToWorldに対しての距離を求める拡張メソッドです。

struct DistanceJob : IJobForEach<LocalToWorld>
{
    public float3 playerPos;
    public void Execute(ref LocalToWorld c0)
    {
        if (c0.Distance(playerPos) < 1)  {  /* Do it. */  }
    }
}

public static class MathEX
{
    /// <summary> 距離を求める </summary>
    /// <param name="position">対象の座標</param>
    /// <returns>実際の距離</returns>
    public static float Distance(this in LocalToWorld world, in float3 position)
    {
        return math.distance(world.Position, position);
    }
}

 なおNativeCollectionを使用している場合、内部でポインタを使用して別のデータを参照しているのでコピーされても問題ないです。実際、NativeSort.csを見てみると、特に参照渡しをせずに普通に受け取っています(その直後に内部のポインタを渡してたり)

f:id:tsubaki_t1:20191023224752j:plain
NativeSort.csでNativeArrayの拡張メソッドを定義

【Unity】ECSで子Entityの”座標”と”向き”を使いたい

f:id:tsubaki_t1:20191021213839g:plain

このサンプルは Entities.0.1.1およびUnity 2019.2f8を使用しています

子オブジェクトの座標を使いたい

 Unityでオブジェクトを構造化していると、よく親子関係を使用したくなります。

 例えば戦車砲を持つユニットの砲弾は、砲塔の先から出てほしいです。これは砲の向きを回転させたり、戦車自体が動いた場合でも同様です。これをスクリプトで制御しても良いのですが、オブジェクトで発射位置と向きを設定出来れば非常に楽といえます。

f:id:tsubaki_t1:20191021212344j:plain
戦車砲の発射位置をオブジェクトで指定する

LocalToParentを使用する

 LocalToParentは、子オブジェクトのローカル座標をワールド座標に変換したマトリクスを格納しています。このコンポーネントTranslation(ローカル座標)Rotation(ローカル回転)、`Scale(ローカル拡縮)等の情報を元に自動的に更新されるコンポーネントで、基本的には値を更新する必要はありません。

f:id:tsubaki_t1:20191021212857j:plain
LocalToWorldは他のコンポーネントがあると勝手に更新される

 このLocalToWorldに格納されている情報を使用すれば、面倒くさい計算を行わず子オブジェクトのワールド座標が取得できます。

回転しながら弾を撃つ砲台

 トップ絵の動き(回るキューブが指定方向に弾をばらまく)を作ってみます。最初に砲台と回転する物体を作ります。これはConversionWorkflowで作るのでGameObjectで配置していく感じです。線の方向がGameObjectの上方向です。

f:id:tsubaki_t1:20191021213605j:plain
砲台と砲台の向き

 コンポーネントを設定します。配置は大体こんな感じです。オーサリングのコードは割愛しますが、基本的にComponentという名前を付けています。

/// <summary> 前方に移動する </summary>
public struct BulletTag : IComponentData { }

/// <summary> 弾を発射する </summary>
public struct GunTag : IComponentData { }

/// <summary> 回転する </summary>
public struct RotationSpeed : IComponentData
{
    /// <summary> キューブの回転速度(rad)  </summary>
    public float Value;
}

f:id:tsubaki_t1:20191021214522j:plain
Entityにコンポーネントを設定

キューブを回す

 とりあえず砲台となるキューブを回します。Gunの親オブジェクトにでもRotationSpeedを登録してValueを1とかに設定(早すぎると微妙)、後はRotationSystemを記述して勝手に回します。ここはよくある記述です。ただし、親オブジェクトが移動・回転することで子オブジェクトも移動・回転していることが確認できます。

public class RotationSystem : JobComponentSystem
{
    struct RotationJob : IJobForEach<Rotation, RotationSpeed>
    {
        public void Execute(ref Rotation c0, [ReadOnly] ref RotationSpeed c1)
        {
            c0.Value = math.mul(c0.Value, quaternion.RotateZ(c1.Value));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        inputDeps = new RotationJob().Schedule(this, inputDeps);
        return inputDeps;
    }
}

f:id:tsubaki_t1:20191021215143g:plain
キューブが回ると、子オブジェクトの赤い丸も一緒に動く

弾を生成

 次にGUNの位置に弾を生成します。弾の位置と向きは親Entityによって動くのでLocalToWorldを使用することで最終的な位置と向きを取得する感じです。

 位置は LocalToWorld.Position から、向きは math.quaternion(LocalToWorld.Value)でマトリクスを回転に変更すれば良いです。このマトリクスにはスケール等の情報も格納されてるので、親オブジェクトのスケールが(1,1,1)じゃないと変な動きをするかもしれません。

using static Unity.Entities.ComponentType;

public class GunSystem : ComponentSystem
{
    // 弾の発射間隔
    private float interval;
    // PrefabとBulletを持つEntityのQuery
    private EntityQuery bulletPrefab;

    protected override void OnCreate() => bulletPrefab = GetEntityQuery(ReadOnly<Prefab>(), ReadOnly<BulletTag>());

    protected override void OnStartRunning() => interval = 0;

    protected override void OnUpdate()
    {
        var bulletEntity = bulletPrefab.GetSingletonEntity();

        // 一定間隔で射撃を行う
        interval += Time.deltaTime;
        if (interval > 0.1f)
        {
            interval -= 0.1f;

            // GUNを持つEntityから弾を発射する
            Entities.WithAllReadOnly<GunTag>().ForEach((ref LocalToWorld localToWorld) => Shot(bulletEntity, localToWorld));
        }
    }

    void Shot(Entity bulletEntity, in LocalToWorld gunTransform)
    {
        var instance = PostUpdateCommands.Instantiate(bulletEntity);

        // 新しく作成したEntityに座標と向きを登録
        PostUpdateCommands.SetComponent(instance, new Translation { Value = gunTransform.Position });
        PostUpdateCommands.SetComponent(instance, new Rotation() { Value = math.quaternion(gunTransform.Value) });
    }
}

f:id:tsubaki_t1:20191021220145g:plain
GUNの位置に弾を生成する

Entity生成は、EntityCommandBufferがBurstに対応してないのでメインスレッドでやってしまいます。

弾を上方向に飛ばす

 最後に、生成した弾を弾の向きに飛ばします。Entityの向きのfloat3は LocalToWorld.ForwardLocalToWorld.Upで取得できるので、これを足せば良いです。

 この速度もScaleの影響を受けるので注意が必要です。

public class BulletSystem : JobComponentSystem
{
    [RequireComponentTag(typeof(BulletTag))]
    struct MoveBulletJob : IJobForEach<Translation, LocalToWorld>
    {
        public float DeltaTime;
        public void Execute(ref Translation translation, [ReadOnly] ref LocalToWorld localtoworld)
        {
            translation.Value += localtoworld.Up * DeltaTime * 12;
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        inputDeps = new MoveBulletJob { DeltaTime = Time.deltaTime }.Schedule(this, inputDeps);
        return inputDeps;
    }
}

f:id:tsubaki_t1:20191021222140g:plain
生成した弾は、弾にとっての上方向に向かって進む

注意点

  • ハイブリットECS…つまりConvertToEntity.ConvertAndInjectGameObjectを使用している場合、使用できません。普通に子オブジェクトのTransformを使用してください。
  • 親子関係を作る場合、親のオブジェクトを消しても子が残る事があります。LinkedEntityGroupを使用して親を消したら子も消えるようにします。
// 何故か標準で存在しない、LinkedEntityGroupオーサリングの例
// ※複数マテリアル等、複数のEntityを生成するオブジェクトが含まれている場合は正常に動作しません
[DisallowMultipleComponent]
[RequiresEntityConversion]
public class LinkedEntityGroupComponent : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var buffer = dstManager.AddBuffer<LinkedEntityGroup>(entity);

        var children = transform.GetComponentsInChildren<Transform>();
        foreach(var child in children)
        {
            var childEntity = conversionSystem.GetPrimaryEntity(child.gameObject);
            buffer.Add(childEntity);
        }
    }
}

f:id:tsubaki_t1:20191021223738g:plain
親のEntityを消した時に子のEntityも消えるようにする

関連

tsubakit1.hateblo.jp

【Unity】構造体のデータを異なる型に「再解釈」する

 DOTS…特にECSで色々とやっていると、内部のデータは同じなのに型が違うせいで変換しないといけないというケースがあります。

 例えば float のバッファを流し込んでバッチ処理を行うAPIがあった時、ローカルのデータで NativeArray<FloatData> のような構造体の配列は直接流し込めません。しかしメモリ的にはこの二つはほぼ同じものです。そこで Reinterpret<T, U>() でデータを再解釈して NativeArray<FloatData>NativeArray<float>として扱ってみます。

動かない例

 例えば下のようなコードがあったとします。 MyData という構造体が定義されていて、これを ShowFloatLog(NativeArray<float> inputs) で一気に表示したい。当然MyDataは中身がfloatであっても扱いはfloatではないので、下のコードはエラーになります。

using Unity.Collections;
using UnityEngine;

struct MyData { public float Value; }

public class Sample: MonoBehaviour
{
    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        ShowLog(input1 );

        input1.Dispose();
    }

    static void ShowFloatLog(NativeArray<float> inputs)
    {
        foreach (var data in inputs)
            Debug.Log(data);
    }
}

f:id:tsubaki_t1:20191017232117j:plain
型が違うのでエラー

Reinterpret<T, U>()で異なる型へ「再解釈」する

 Reinterpret<T, U>()を使用して、NativeArray<MyData>()をNativeArray()に解釈します。このAPICollectionパッケージに含まれているので、パッケージのインポートが必要です。

f:id:tsubaki_t1:20191017232535j:plain
Entitiesを使うと勝手に入るCollectionパッケージ

 下のように使用します。MyDataの中身がfloatしかないので、floatと解釈することが出来ています。なお、あくまで解釈してるだけなのでDisposeはしてはいけません。参照先のポインタは同じものです。

    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        // MyDataをFloatに再解釈
        var floatInput = input1.Reinterpret<MyData, float>();

        // NativeArray<float>なので動作
        ShowLog(floatInput);

        input1.Dispose();
    }

 この再解釈はポインタ的に、内部データが同じなら色々なデータに解釈できるみたいです。例えばfloat3の配列をfloatに解釈するといった事も可能です。当然、データの長さが異なるので注意が必要ですが、少し面白いと思わなくもないです。

 なお float3 -> float の場合は特に問題は無いですが、 float -> float3 の場合は3で割り切れる数の要素数でないとエラーになります。多い方がベクタライズに有利になりやすいみたいですが、そこのところは注意が必要です。

    void Start()
    {
        var input1 = new NativeArray<float3>(new[] {
            new float3(111, 222, 33),
            new float3(444, 555, 666),
            new float3(777, 888, 999),
        }, Allocator.Temp);

        // float3をfloatに再解釈
        var floatInput = input1.Reinterpret<float3, float>();

        // output : input length 3, reinterpret length 9
        // float3をfloatにするにあたり、配列の長さが変わっている
        Debug.Log($"input length {input1.Length}, reinterpret length {floatInput.Length}");

        // 111 ~ 999 までの要素を個別に出力
        ShowLog(floatInput);

        input1.Dispose();
    }

感想

 NativeArray<Vector3>を返してくる古いAPIと、NativeArray<float3>を要求する新しいAPIの間で苦しんだ時に思い出すと幸せになるかも。