テラシュールブログ

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

【Unity】Device Simulatorでノッチとセーフエリアの対策

f:id:tsubaki_t1:20191030233736j:plain

 Unity 2019.3で確認できるDevice Simulatorを使用して、ノッチやセーフエリアといった色々と面倒くさい問題の対策してみます。

Device Simulator

 Device Simulator はUnity 2019.3で追加されたパッケージです。このパッケージを使用するとエディター上でノッチの位置やセーフエリアの位置を大雑把に確認できる機能です。この機能の素晴らしい事はエディターでScreen.SafeAreaを確認出来る点です。この機能がなければ実際に動かさないと分からない部分も、この機能を使用することで確認することが出来ます。ということでSafeAreaやNotchの内容を確認して、安全な形でUIを構築してみます。

f:id:tsubaki_t1:20191030233853j:plain
被らない範囲を視覚的に確認できる

 また、この画面はGameViewであることから察せられる通り、普通に端末解像度やセーフエリアを維持しつつゲームを再生出来ます。ポップアップ等、ゲーム画面外にあるという前提で作成されている演出をSafeAreaコミコミで作ると正しく表示されないことがあるので、そのあたりも含めてよく確認するのが良さそうです。
 実際、ここで作成したUIはゲームにビルドしても想定通りに表示されました。

f:id:tsubaki_t1:20191030235843g:plain
DeviceSimulatorで調整した後にビルドしたもの。セーフエリア内でちゃんと表示出来ている

インストール

 Device Simulator のインストールはいつも通りのPackageManagerからです。現状はPreviewなのでShowPreviewPackageを有効にする必要があります。

f:id:tsubaki_t1:20191030223752p:plain
PackageManagerから選択してInstall

 パッケージ導入後はGameViewをSimulation Modeに切り替えます。これでGameViewが表示する内容を、ゲーム端末と同じ解像度、同じDPIに切り替えることが出来ます。またノッチの位置やピクセルが潰される位置をグラフィカルに表現してくれます。  その際にSafeAreaも使用可能にしてくれます。

f:id:tsubaki_t1:20191030223958g:plain
GameViewを切り替える

セーフエリア情報を元にUIを調整可能にする

f:id:tsubaki_t1:20191030223655j:plain

 ノッチの位置が確認できるぜヤッターだけでは片手落ちです。ノッチの位置や大きさは端末によって千差万別。iPhoneのように殆ど画面上をノッチが埋めている端末もあれば、幾つかのAndroid端末のようにほんの少ししかノッチで埋めていない端末もあります。セーフエリアの位置も色位rと微妙に違います。なので、セーフエリアの位置を元にUIを調整出来る方法を考えてみます。

 まずセーフエリアの位置を元にUIを配置します。UIの場合、親のRectTransfromのサイズSafeAreaの大きさに調整してやれば概ね問題は解決します。アンカーの位置さえ調整すれば良いわけですから。ということで、親となるRectTransformのサイズをSafeAreaの位置に合わせます。この時、RectTransformのLeft、Rightは0にしてください。

using UnityEngine;

[RequireComponent(typeof(RectTransform))]
[ExecuteAlways]
public class SafeAreaPadding : MonoBehaviour
{
    private DeviceOrientation postOrientation;

    void Update()
    {
        if (Input.deviceOrientation != DeviceOrientation.Unknown && postOrientation == Input.deviceOrientation)
            return;

        postOrientation = Input.deviceOrientation;

        var rect = GetComponent<RectTransform>();
        var area = Screen.safeArea;
        var resolition = Screen.currentResolution;

        rect.sizeDelta = Vector2.zero;
        rect.anchorMax = new Vector2(area.xMax / resolition.width, area.yMax / resolition.height);
        rect.anchorMin = new Vector2(area.xMin / resolition.width, area.yMin / resolition.height);
    }
}

 このコードをCanvas直下の適当なRectTransformに設定すれば、RectTrasnfromはSafeAreaの大きさになります。あとはSafeAreaのサイズになったUIの子としてアンカーを調整してやれば、セーフエリア内を出ない形でUIを配置出来ます。

https://user-images.githubusercontent.com/1644563/67865571-63e4ae80-fb1f-11e9-90f5-64b715e8b379.gif

f:id:tsubaki_t1:20191030234539j:plain
セーフエリアの範囲にRectTransformを調整

 なお、内部でアンカーを使用しているのは単純に「Canvas Scalerでサイズが調整されても問題なく動作する」事を保証するためです。

ノッチの範囲だけ回避したい

 ノッチの部分だけピンポイントで回避したい…という強気の場合はScreen.cutoutsが使用できます。ただしAndroidのCutoutの位置は必ずしも一つではない点に注意してください。ノッチは必ずしも上にある訳ではないですし、そもそもノッチという形をとっていない端末も存在します。またノッチが左右どちらかに偏っている場合もあります。

f:id:tsubaki_t1:20191031122549j:plain
おや、ノッチの位置が

 とはいえ、今回はノッチは上に一つだけあるという前提の実装です。

 例えば下のコードでノッチの左側と右側に一致するRectTransformを構築します。これにより、ノッチ外の部分のUIを構築出来ます。あとはRectTransformのサイズをはみ出ないようにUIを実装すれば、ノッチでUIが隠れるといった事を回避出来ます。

using UnityEngine;

[ExecuteAlways]
public class NotchPadding : MonoBehaviour
{
    [SerializeField] RectTransform left, right;

    void Update()
    {
        if (Screen.cutouts[0] == null || left == null || right == null)
            return;

        var resolition = Screen.currentResolution;

        left.anchorMin = new Vector2(0, Screen.cutouts[0].yMin / resolition.height);
        left.anchorMax = new Vector2(Screen.cutouts[0].xMin / resolition.width, 1);

        right.anchorMin = new Vector2(Screen.cutouts[0].xMax / resolition.width, Screen.cutouts[0].yMin / resolition.height);
        right.anchorMax = new Vector2(1, 1);
    }
}

https://user-images.githubusercontent.com/1644563/67867527-73b1c200-fb22-11e9-973d-e4b52d619087.gif

f:id:tsubaki_t1:20191030235404j:plain
ノッチに被らない形でUIを配置

対応する端末が一覧に無い場合

 対応する端末が一覧に無い場合、ノッチの設定を自分で追加することも出来るみたいです。com.unity.device-simulator/com.unity.device-simulatorフォルダ内に設定と使用する画像ファイルがあり、コレを編集することで独自のレイアウト設定が作れます。

f:id:tsubaki_t1:20191030234018j:plain
セーフエリア・ノッチの設定と、その際に表示する描画範囲の画像

関連

blogs.unity3d.com

www.gaprot.jp

www.fast-system.jp

【Unity】ISystemStateComponentDataという機能

今回は微妙に詳細な説明のないISystemStateComponentDataという機能を紹介します。

EntityがDestroyされても破棄されないコンポーネント

 ISystemStateComponentDataはすごく特殊なデータ構造です。具体的には、ISystemStateComponentDataが存在するEntityは、EntityをDestoryされても破棄されなくなるというルールを持っています。

 例えば簡単なEntityを作り、それを破棄してみます。通常であればそのままEntityのIDがリサイクルされるだけなのですが、Entityが破棄されずに残るのを確認出来ます。

// 適当なISystemStateComponentData 
public struct MyData : ISystemStateComponentData { }
    // Entityを作ってから破棄する処理
    IEnumerator Start()
    {
        var dst = World.Active.EntityManager;
        var entity = dst.CreateEntity(typeof(MyData), typeof(LocalToWorld), typeof(Translation));

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.D)); // Dキーを押すまで待機

        // MyData以外のComponentDataが破棄される。Entityは破棄されない
        dst.DestroyEntity(entity); 
    }

f:id:tsubaki_t1:20191027124742j:plain
ISystemStateComponentDataがあるEntityは破棄されない

 ISystemStateComponentDataのあるEntityを消すには、ISystemStateComponentDataをRemoveする必要があります。全てのISystemStateComponentDataが破棄されるとEntitiyも破棄されリサイクルに回されます。

    IEnumerator Start()
    {
        var dst = World.Active.EntityManager;
        var entity = dst.CreateEntity(typeof(MyData), typeof(LocalToWorld), typeof(Translation));

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.D));
        dst.DestroyEntity(entity); // MyData以外のComponentDataが破棄される

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.C));

        dst.RemoveComponent<MyData>(entity); // 改めてEntityが破棄される
    }

Entity生成・破棄のタイミングで処理を一回実行する

 ISystemStateComponentDataの応用で、ComponentDataを生成(追加)したタイミングと破棄したタイミングに処理を実行する方法を考えてみます。本来ならコールバックでこういった事をやりたい所ですが、コールバックはECSの思想とは合ってないので、少し回りくどい方法になっています。使用するのは通常のComponentDataであるBehaviourコンポーネントと、ISystemStateComponentDataのBehaviourInitializedです。

public struct BehaviourInitialized : ISystemStateComponentData{}

public struct Behaviour : IComponentData {}

初期化時の処理(Awake的な)

 最初の状態はBehaviourのみEntityに存在する状態を作ります。これは単純にBehaviourコンポーネントのみをEntityに追加すれば良いです。

f:id:tsubaki_t1:20191027171846j:plain

 上の状態のEntityはEntityQuery的にはBehaviourがあるがBehaviourInitializedが無いという条件で取得することが可能です。ここで見つけたEntity一覧には初期化処理を行い、BehaviourInitializedを追加して初期化処理に呼ばれないようにします。(onStartQuery.CalculateChunkCount() > 0)は、このシステムが

 他のシステムで初期化完了までに処理を行いたい場合、Entityの生成から下のシステムが呼ばれる間にシステムを挿入します。

    protected override void OnCreate()
    {
         // BehaviourはあるがBehaviourInitializedが無いEntity
        onStartQuery = GetEntityQuery(ReadOnly<Behaviour>(), Exclude<BehaviourInitialized>());
    }

    protected override void OnUpdate()
    {
        Entities.With(onStartQuery).ForEach((Entity entity) =>
        {
            // 初期化処理を記述
            Debug.Log($"On Create Enemy {entity.Index}");
        });
        // BehaviourInitializedが無いEntityにBehaviourInitializedを一括追加
        EntityManager.AddComponent<BehaviourInitialized>(onStartQuery);
    }

初期化後の毎フレーム実行する処理(Update的な)

 初期化が完了すれば、下のようにBehaviourBehaviourInitializedが揃った状態になります。もしアップデート処理を行う場合、この2つが揃っていることを確認すれば安全に初期化済みのEntityを操作することが出来ます。

f:id:tsubaki_t1:20191027173108j:plain

protected override void OnUpdate()
{
    Entities.WithAnyReadOnly<Behaviour,BehaviourInitialized>().ForEach((Entity entity) =>
    {
        Debug.Log($"On Update Enemy {entity.Index}");
    });
}

破棄時の処理(OnDestroy的な)

 Entityが破棄された時の処理です。Entityが破棄されてもISystemStateComponentDataであるBehaviourInitializedがあるので破棄されません。それ以外の全てのコンポーネントが破棄された状態になります。つまり、BehaviourInitializedのみのEntityを検索すれば、破棄されてクリーンアップ待ちのEntityを見つけることが出来ます。後はBehaviourInitializedを破棄すれば、破棄処理を完了出来ます。

 こちらもクリーンアップする前に他の処理を挟めば、クリーンナップ処理を他のシステム側で行うことが出来ます。

f:id:tsubaki_t1:20191027174017j:plain

protected override void OnCreate()
{
    // BehaviourInitializedはあるがBehaviourが無いEntity
    onDestroyQuery = GetEntityQuery(Exclude<Behaviour>(), ReadOnly<BehaviourInitialized>());
}

protected override void OnUpdate()
{
    Entities.With(onDestroyQuery).ForEach((Entity entity) =>
    {
        Debug.Log($"On Destroy Enemy {entity.Index}");
    });
    // クェリーからBehaviourInitializedを一括削除
    EntityManager.RemoveComponent<BehaviourInitialized>(onDestroyQuery);
}

その他

  • この例ではISystemStateComponentDataは値を持っていませんが、普通に持てます。データを使用する場合はIComponentDataと同じように取得します。
  • ISystemStateComponentDataのアプローチはOnDestroyと異なり、破棄された後に破棄された座標を確認といった事が出来ません。正確にはISystemStateComponentDataに座標を突っ込んでおけば取得出来ますが、小頻度の更新はあまり推奨されません。
    キャラクターの死亡演出の出力やスコアの計上は破棄するシステム側が責任を持ってやるべきです(もしくは破棄されたフラグを付けて、他のシステムに任せる)
  • SubSceneのコンバージョンワークフローでISystemStateComponentDataを追加すると、追加されないです。コレはバグかもしれませんが。

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