テラシュールブログ

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

【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の拡張メソッドを定義