ichiyamanのブログ

基本Tech Logです

写経 : BatchRenderGroup Phase2.5

写経 : BatchRenderGroup Phase0 - ichiyamanのブログ

■Phase2.5 BatchRendererGroupの理解を深めるためもう少し細かいサンプルを参照する

BatchRendererGroup でレンダラーを作成する
docs.unity3d.com

とても良いサンプルがあったのでこちらを分解します。

2.5.1 BatchRendererGroup オブジェクトの初期化
おれはBatchRendererGroupを使うぜ!という宣言ですね。
この段階では何も動きません。  

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class SimpleBRGExample : MonoBehaviour
{
    private BatchRendererGroup m_BRG;

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
    }

    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // この例はジョブを使用していないので、空の JobHandle を返します。
        // パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
        // カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
        // Burst ジョブの終了時に完了するハンドルをここに返します。
        return new JobHandle();
    }
}

2.5.2 メッシュとマテリアルの登録
meshとmaterialは登録しないといけません

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class SimpleBRGExample : MonoBehaviour
{
    public Mesh mesh;
    public Material material;

    private BatchRendererGroup m_BRG;

    private BatchMeshID m_MeshID;
    private BatchMaterialID m_MaterialID;

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
        m_MeshID = m_BRG.RegisterMesh(mesh);
        m_MaterialID = m_BRG.RegisterMaterial(material);
    }

    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // この単純な例はジョブを使用しないので、空の JobHandle を返します。
        // パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
        // カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
        // Burst ジョブの終了時に完了するハンドルを返します。
        return new JobHandle();
    }
}

2.5.3 バッチの作成
ここからが本番です
ShaderCodeを自分で翻訳する感じですね

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class SimpleBRGExample : MonoBehaviour
{
    public Mesh mesh;
    public Material material;

    private BatchRendererGroup m_BRG;

    private GraphicsBuffer m_InstanceData;
    private BatchID m_BatchID;
    private BatchMeshID m_MeshID;
    private BatchMaterialID m_MaterialID;

    // 計算を便利にするいくつかのヘルパー定数。
    private const int kSizeOfMatrix = sizeof(float) * 4 * 4;
    private const int kSizeOfPackedMatrix = sizeof(float) * 4 * 3;
    private const int kSizeOfFloat4 = sizeof(float) * 4;
    private const int kBytesPerInstance = (kSizeOfPackedMatrix * 2) + kSizeOfFloat4;
    private const int kExtraBytes = kSizeOfMatrix * 2;
    private const int kNumInstances = 3;

    // PackedMatrix は、行列を Unity 提供の SRP シェーダーが想定する形式に
    // 変換する便利な型です。
    struct PackedMatrix
    {
        public float c0x;
        public float c0y;
        public float c0z;
        public float c1x;
        public float c1y;
        public float c1z;
        public float c2x;
        public float c2y;
        public float c2z;
        public float c3x;
        public float c3y;
        public float c3z;

        public PackedMatrix(Matrix4x4 m)
        {
            c0x = m.m00;
            c0y = m.m10;
            c0z = m.m20;
            c1x = m.m01;
            c1y = m.m11;
            c1z = m.m21;
            c2x = m.m02;
            c2y = m.m12;
            c2z = m.m22;
            c3x = m.m03;
            c3y = m.m13;
            c3z = m.m23;
        }
    }

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
        m_MeshID = m_BRG.RegisterMesh(mesh);
        m_MaterialID = m_BRG.RegisterMaterial(material);

        AllocateInstanceDateBuffer();
        PopulateInstanceDataBuffer();
    }

    private void AllocateInstanceDateBuffer()
    {
        m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
            BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
            sizeof(int));
    }

    private void PopulateInstanceDataBuffer()
    {
        // インスタンスデータバッファの先頭にゼロ行列を置くことで、アドレス 0 からの読み込みが 0 を返すようにします。
        var zero = new Matrix4x4[1] { Matrix4x4.zero };

        // 3 つのサンプルインスタンスの変換行列を作成します。
        var matrices = new Matrix4x4[kNumInstances]
        {
            Matrix4x4.Translate(new Vector3(-2, 0, 0)),
            Matrix4x4.Translate(new Vector3(0, 0, 0)),
            Matrix4x4.Translate(new Vector3(2, 0, 0)),
        };

        // 変換行列を、シェーダーの想定するパックされた形式に変換します。
        var objectToWorld = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0]),
            new PackedMatrix(matrices[1]),
            new PackedMatrix(matrices[2]),
        };

        // パックされた逆行列も作成します。
        var worldToObject = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0].inverse),
            new PackedMatrix(matrices[1].inverse),
            new PackedMatrix(matrices[2].inverse),
        };

        // すべてのインスタンスに固有の色を持たせます。
        var colors = new Vector4[kNumInstances]
        {
            new Vector4(1, 0, 0, 1),
            new Vector4(0, 1, 0, 1),
            new Vector4(0, 0, 1, 1),
        };

        // この単純な例では、インスタンスデータは以下のようにバッファ内に配置されます。
        // オフセット | 説明
        //      0 | 64 バイトの 0。つまりアドレス 0 からの読み込みは 0 を返す。
        //     64 | 初期化されていない 32 バイト。SetData を扱いやすくするもので、それ以外には不要。
        //     96 | unity_ObjectToWorld。3 つのパックされた float3x4 行列。
        //    240 | unity_WorldToObject。3 つのパックされた float3x4 行列。
        //    384 | _BaseColor。3 つの float4。

        // インスタンス化された各種プロパティーの開始アドレスを計算します。
        // SetData の computeBufferStartIndex パラメーターはソース配列要素として表されるので、
        // unity_ObjectToWorld はアドレス 64 ではなくアドレス 96 から開始します。
        // このため、sizeof(PackedMatrix) の倍数を使用するほうが簡単です。 
        
        uint byteAddressObjectToWorld = kSizeOfPackedMatrix * 2;
        uint byteAddressWorldToObject = byteAddressObjectToWorld + kSizeOfPackedMatrix * kNumInstances;
        uint byteAddressColor = byteAddressWorldToObject + kSizeOfPackedMatrix * kNumInstances;

        // インスタンスデータを GraphicsBuffer にアップロードしてシェーダーがそれらを読み込めるようにします。
        m_InstanceData.SetData(zero, 0, 0, 1);
        m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        m_InstanceData.SetData(colors, 0, (int)(byteAddressColor / kSizeOfFloat4), colors.Length);

        // このインスタンスデータを指すメタデータ値を設定します。それぞれ最上位ビット 0x80000000 を設定します。
        // これは "このデータは、インスタンスインデックスによってインデックスされる、インスタンス毎に 1 つの値を持つ配列である" 
        // とシェーダーに指示します。シェーダーの使用するメタデータ値でここに設定されていないものはすべて 0 になります。
        // 0 の値が UNITY_ACCESS_DOTS_INSTANCED_PROP (つまりデフォルトなし) に使用された場合、シェーダーは
        // 0x00000000 のメタデータ値を解釈してバッファの先頭から読み込みます。バッファの先頭はゼロ行列なので、
        // このような読み込みは必ず 0 を返します。これは合理的なデフォルト値です。      
        var metadata = new NativeArray<MetadataValue>(3, Allocator.Temp);
        metadata[0] = new MetadataValue { NameID = Shader.PropertyToID("unity_ObjectToWorld"), Value = 0x80000000 | byteAddressObjectToWorld, };
        metadata[1] = new MetadataValue { NameID = Shader.PropertyToID("unity_WorldToObject"), Value = 0x80000000 | byteAddressWorldToObject, };
        metadata[2] = new MetadataValue { NameID = Shader.PropertyToID("_BaseColor"), Value = 0x80000000 | byteAddressColor, };

        // 最後に、これらのインスタンスのバッチを作成し、そのバッチに、このインスタンスデータを持つ GraphicsBuffer と
        // プロパティの場所を指定するメタデータ値を使用させます。
        m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);
    }

    // Raw バッファは int で割り当てられます。これはデータに必要な int の数を計算する
    // ユーティリティメソッドです。
    int BufferCountForInstances(int bytesPerInstance, int numInstances, int extraBytes = 0)
    {
        // バイト数を int の倍数に丸めます。
        bytesPerInstance = (bytesPerInstance + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        extraBytes = (extraBytes + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        int totalBytes = bytesPerInstance * numInstances + extraBytes;
        return totalBytes / sizeof(int);
    }


    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // この単純な例はジョブを使用しないので、空の JobHandle を返します。
        // パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
        // カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
        // Burst ジョブの終了時に完了するハンドルをここに返します。
        return new JobHandle();

    }
}

2.5.4 描画コマンドの作成
あとは整形したデータを投げつけるだけです
sampleではCubeですが、このCubeを広げたり縮めたり色を変えたりなどが、指定した個数の範囲内で自由にできるということなのです!
すばらしいですね

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class SimpleBRGExample : MonoBehaviour
{
    public Mesh mesh;
    public Material material;

    private BatchRendererGroup m_BRG;

    private GraphicsBuffer m_InstanceData;
    private BatchID m_BatchID;
    private BatchMeshID m_MeshID;
    private BatchMaterialID m_MaterialID;

    // 計算を便利にするいくつかのヘルパー定数。
    private const int kSizeOfMatrix = sizeof(float) * 4 * 4;
    private const int kSizeOfPackedMatrix = sizeof(float) * 4 * 3;
    private const int kSizeOfFloat4 = sizeof(float) * 4;
    private const int kBytesPerInstance = (kSizeOfPackedMatrix * 2) + kSizeOfFloat4;
    private const int kExtraBytes = kSizeOfMatrix * 2;
    private const int kNumInstances = 3;

    // PackedMatrix は、行列を Unity 提供の SRP シェーダーが想定する形式に変換する
    // 便利な型です。
    struct PackedMatrix
    {
        public float c0x;
        public float c0y;
        public float c0z;
        public float c1x;
        public float c1y;
        public float c1z;
        public float c2x;
        public float c2y;
        public float c2z;
        public float c3x;
        public float c3y;
        public float c3z;

        public PackedMatrix(Matrix4x4 m)
        {
            c0x = m.m00;
            c0y = m.m10;
            c0z = m.m20;
            c1x = m.m01;
            c1y = m.m11;
            c1z = m.m21;
            c2x = m.m02;
            c2y = m.m12;
            c2z = m.m22;
            c3x = m.m03;
            c3y = m.m13;
            c3z = m.m23;
        }
    }

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
        m_MeshID = m_BRG.RegisterMesh(mesh);
        m_MaterialID = m_BRG.RegisterMaterial(material);

        AllocateInstanceDateBuffer();
        PopulateInstanceDataBuffer();
    }

    private void AllocateInstanceDateBuffer()
    {
        m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
            BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
            sizeof(int));
    }

    private void PopulateInstanceDataBuffer()
    {
        // インスタンスデータバッファの先頭にゼロ行列を置くことで、アドレス 0 からのロードが 0 を返すようにします。
        var zero = new Matrix4x4[1] { Matrix4x4.zero };

        // 3 つのサンプルインスタンスの変換行列を作成します。
        var matrices = new Matrix4x4[kNumInstances]
        {
            Matrix4x4.Translate(new Vector3(-2, 0, 0)),
            Matrix4x4.Translate(new Vector3(0, 0, 0)),
            Matrix4x4.Translate(new Vector3(2, 0, 0)),
        };

        // トランスフォーム行列を、シェーダーの想定するパックされた形式に変換します。
        var objectToWorld = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0]),
            new PackedMatrix(matrices[1]),
            new PackedMatrix(matrices[2]),
        };

        // パックされた形式の逆行列も作成します。
        var worldToObject = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0].inverse),
            new PackedMatrix(matrices[1].inverse),
            new PackedMatrix(matrices[2].inverse),
        };

        // すべてのインスタンスに固有の色を持たせます。
        var colors = new Vector4[kNumInstances]
        {
            new Vector4(1, 0, 0, 1),
            new Vector4(1, 1, 0, 1),
            new Vector4(1, 1, 1, 1),
        };

        // この単純な例では、インスタンスデータは以下のようにバッファ内に配置されます。
        // オフセット | 説明
        //      0 | 64 バイトの 0。つまりアドレス 0 からの読み込みは 0 を返す。
        //     64 | 初期化されていない 32 バイト。SetData を扱いやすくするもので、それ以外には不要。
        //     96 | unity_ObjectToWorld。3 つのパックされた float3x4 行列。
        //    240 | unity_WorldToObject。3 つのパックされた float3x4 行列。
        //    384 | _BaseColor。3 つの float4。

        // インスタンス化された各種プロパティーの開始アドレスを計算します。unity_ObjectToWorld は
        // アドレス 64 ではなくアドレス 96 から開始するので、32 ビットが初期化されずに残ります。これは、 
        // computeBufferStartIndex パラメーターが、"開始オフセットがソース配列の要素の型のサイズで割り切れる"
        // ことを必要とするためです。ここでは、これは PackedMatrix のサイズ、つまり 48 です。
        uint byteAddressObjectToWorld = kSizeOfPackedMatrix * 2;
        uint byteAddressWorldToObject = byteAddressObjectToWorld + kSizeOfPackedMatrix * kNumInstances;
        uint byteAddressColor = byteAddressWorldToObject + kSizeOfPackedMatrix * kNumInstances;

        // インスタンスデータを GraphicsBuffer にアップロードしてシェーダーがそれらを読み込めるようにします。
        m_InstanceData.SetData(zero, 0, 0, 1);
        m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        m_InstanceData.SetData(colors, 0, (int)(byteAddressColor / kSizeOfFloat4), colors.Length);

        // Set up metadata values to point to the instance data. Set the most significant bit 0x80000000 in each 
このインスタンスデータを指すメタデータ値を設定します。それぞれ最上位ビット 0x80000000 を設定します。
        // これは  "このデータは、インスタンスインデックスによってインデックスされる、インスタンス毎に 1 つの値を持つ配列である" とシェーダーに指示します。
        // シェーダーが使用するメタデータ値でここに設定されていないものはすべて 0 になります。このような値が
        // UNITY_ACCESS_DOTS_INSTANCED_PROP (つまりデフォルトなし) に使用された場合、シェーダーは
        // 0x00000000 のメタデータ値を解釈してバッファの先頭から読み込みます。バッファの先頭はゼロ行列なので、
        // このような読み込みは必ず 0 を返します。これは合理的なデフォルト値です。
        var metadata = new NativeArray<MetadataValue>(3, Allocator.Temp);
        metadata[0] = new MetadataValue { NameID = Shader.PropertyToID("unity_ObjectToWorld"), Value = 0x80000000 | byteAddressObjectToWorld, };
        metadata[1] = new MetadataValue { NameID = Shader.PropertyToID("unity_WorldToObject"), Value = 0x80000000 | byteAddressWorldToObject, };
        metadata[2] = new MetadataValue { NameID = Shader.PropertyToID("_BaseColor"), Value = 0x80000000 | byteAddressColor, };

        // 最後に、これらのインスタンスのバッチを作成し、そのバッチに、インスタンスデータを持つ GraphicsBuffer と
        // プロパティの場所を指定するメタデータ値を使用させます。 
        m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);
    }

    // Raw バッファは int で割り当てられます。これはデータに必要な int の数を計算する
    // ユーティリティメソッドです。
    int BufferCountForInstances(int bytesPerInstance, int numInstances, int extraBytes = 0)
    {
        // バイト数を int の倍数に丸めます。
        bytesPerInstance = (bytesPerInstance + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        extraBytes = (extraBytes + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        int totalBytes = bytesPerInstance * numInstances + extraBytes;
        return totalBytes / sizeof(int);
    }


    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // UnsafeUtility.Malloc() はアラインメントを必要とするので、 
        // (合理的なデフォルトとして) 最大の int 型のアラインメントを使用してください。
        int alignment = UnsafeUtility.AlignOf<long>();

        // BatchCullingOutputDrawCommands 構造体へのポインターを取得し、
        // それを簡単に直接変更できるようにします。
        var drawCommands = (BatchCullingOutputDrawCommands*)cullingOutput.drawCommands.GetUnsafePtr();

        // 出力配列にメモリを割り当てます。より複雑な実装では、
        // 何が可視であるかに応じて動的に割り当てるメモリのサイズを計算します。
        // この例では、すべてのインスタンスが可視であると仮定するので、 
        // そのそれぞれにメモリを割り当てます。必要な割り当ては以下の通りです。
        // - 単一の描画コマンド (kNumInstances インスタンスを描画する)
        // - 単一の描画範囲 (単一の描画コマンドをカバーする)
        // - kNumInstances 可視インスタンスのインデックス
        // 必ず Allocator.TempJob を使用して配列を割り当てる必要があります。
        drawCommands->drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawCommand>(), alignment, Allocator.TempJob);
        drawCommands->drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawRange>(), alignment, Allocator.TempJob);
        drawCommands->visibleInstances = (int*)UnsafeUtility.Malloc(kNumInstances * sizeof(int), alignment, Allocator.TempJob);
        drawCommands->drawCommandPickingInstanceIDs = null;

        drawCommands->drawCommandCount = 1;
        drawCommands->drawRangeCount = 1;
        drawCommands->visibleInstanceCount = kNumInstances;

        // この例ではデプスソートを使用していないため、instanceSortingPositions は null のままになります。
        drawCommands->instanceSortingPositions = null;
        drawCommands->instanceSortingPositionFloatCount = 0;

        // この単一の描画コマンドを、"配列内でオフセット 0 から開始し、Start() メソッド内に登録されたバッチ ID と
        // マテリアル ID とメッシュ ID を使用して kNumInstances インスタンスを描画する" ように設定します。
        // 特別なフラグは設定しません。
        drawCommands->drawCommands[0].visibleOffset = 0;
        drawCommands->drawCommands[0].visibleCount = kNumInstances;
        drawCommands->drawCommands[0].batchID = m_BatchID;
        drawCommands->drawCommands[0].materialID = m_MaterialID;
        drawCommands->drawCommands[0].meshID = m_MeshID;
        drawCommands->drawCommands[0].submeshIndex = 0;
        drawCommands->drawCommands[0].splitVisibilityMask = 0xff;
        drawCommands->drawCommands[0].flags = 0;
        drawCommands->drawCommands[0].sortingPosition = 0;

        // オフセット 0 にあるこの単一の描画コマンドをカバーするように描画範囲を設定します。
        // is at offset 0.
        drawCommands->drawRanges[0].drawCommandsBegin = 0;
        drawCommands->drawRanges[0].drawCommandsCount = 1;

        // この例ではシャドウやモーションベクトルは考慮してないので、すべてがデフォルトの 0 の値のままになります。 
        // ただし、すべての 1 に設定される renderingLayerMask は除きます。これによって
        // Unity がマスクの設定に関わらずインスタンスをレンダーするようになります。
        drawCommands->drawRanges[0].filterSettings = new BatchFilterSettings { renderingLayerMask = 0xffffffff, };

        // 最後に、実際の可視のインスタンスのインデックスを配列に書き込みます。
        // より複雑な実装では、この出力は何か可視であるかによって変わりますが、
        // この例ではすべてが可視であると仮定しています。
        for (int i = 0; i < kNumInstances; ++i)
            drawCommands->visibleInstances[i] = i;

        // この単純な例はジョブを使用しないので、空の JobHandle を返します。
        // パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
        // カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
        // Burst ジョブの終了時に完了するハンドルをここに返します。
        return new JobHandle();
    }
}

写経 : BatchRenderGroup Phase0

今回は gitの解説記事をを読みながら、新規プロジェクトで同等のプログラムを作成することを写経とします。

元記事
https://blog.unity.com/engine-platform/batchrenderergroup-sample-high-frame-rate-on-budget-devices

github
https://github.com/Unity-Technologies/brg-shooter

■今回のサンプルの趣旨
多くのデモはHigh-end PC用のものだが、今回はスマホのGLES3.0で動作するものです。 BatchRenderGroup, Burst compiler と C# Job System を使います。 Unity2022.3 が必要ですが、DOTSパッケージは必要ありません。

■最終形
こんなの

実行中の画面

■要件分解
- BGがAnimaionしてる、多分Vertex animation
- カメラの移動とともにBGを自動生成している
- Playerと思しきPlaneが左右に移動している
- 弾を撃っている
- 敵が出てくる
- 敵を玉に当てると爆発する
- Playerが爆発している

■Phase
1. 環境を作る(Burst Compiler, C# Job System)
2. BGの自動生成
3. カメラを動かす
4. Playerを動かす
5. 敵を出す
6. 弾を撃つ
7. 敵が玉に当たると爆発する
8. Playerが玉に当たると爆発する



■Phase 1. 環境を作る(Burst Compiler, C# Job System)
1-1 とりあえず URP 付き 3D コアでプロジェクト作成
1-2 Packageをinstall ?
 BurstはDefaultで入ってるから、BRGとJobSystemは必要なのかな?
 元のソースではそのようなPackageは見つからないのでそのまま進む

■Phase2. BGの自動生成
 ゼロから作るのは難しそうなのでスケルトンアプローチする

  2-1 以下ファイルをコピー
BRG_Background.cs
BRG_Container.cs
BRG_Debris.cs

以下のエラーが出るので
error CS0227: Unsafe code may only appear if compiling with /unsafe. Enable "Allow 'unsafe' code" in Player Settings to fix this error.
ここをCheck

2-2 background objectを作成して、BRG_Backgroundを追加

BRG_Debrisがエラーになるので、一旦コメントアウトする

BGっぽいものが出ましたね

ソースを見ていきます。

よくわからないのでさらに Simpleな Tutorialを探す。

写経:WAYN Games (DOTS 基礎編)

はじめに

この記事は以下の動画を写経したものです。

WAYN Games - YouTube

DOTS101-DOTS111 シンプルなTowerDeffenceのモックをDOTSで構築する方法を学びます。

できたもの

  • 基本的な構造の理解
  • Object を いくつかPointに沿って移動させる
  • Object に モーション付き 3D モデルを使用する
  • Entity の InstantiateとDestroy
  • 基本的な Collision 判定

成果物

各動画

DOTS101

https://www.youtube.com/watch?v=N-xoBFi2YF0

この動画では、DOTSのInstallとWindowの説明です。ごくごく基本的な解説でCodeは作成しません。

※後に version up して、これらの手順や install するpackageは1部変更されます。

DOTS102

https://www.youtube.com/watch?v=FXgVuewJ_qU

この動画では SystemBase が解説されます。 既存のUnityでは、ScriptはなんらかのGameObjectにAttachしなければ動作しませんでしたが、これらのClassはただ書くだけで実行されます。 感の良い人はこの動画の時点でデータ指向はそういうことかとわかると思います。 BOX Objectを2つ表示してそれぞれ違うSpeedで移動させています。

DOTS103

https://www.youtube.com/watch?v=uXvuFMlBI3Y

この動画では、DynamicBuffer が解説されます。 複数のPointを配置されたGameObjetから取得し順番に移動させます。

DOTS104

https://www.youtube.com/watch?v=iDPfH0cCPuY

Version up された entities の install方法と ISystemが解説されます。 既存の code の修正方法などの説明があります。 この動画中のソースに TransformAspect が使用されている部分がありますが、 entities ver1.0.0 pre 63 で remove されます。最新版では代わりににLocalTransformを使用します。

  • TransfomrAspect -> LocalTransform

※TransformAspectのmethodを使用している場合は、それぞれの方法で実装が必要です。

※ここでめでたいことに entitiesが’1.0.0に大幅 Version up され、正式リリースとなります。

DOTS105

https://www.youtube.com/watch?v=nXcLOnRGG0w&t=203s

この動画では、Baker が解説されます

Authoring -> Baking - > Runtime の図はとてもわかりやすいので必見です 4:00 くらいから始まります。

DOTS106

https://www.youtube.com/watch?v=uIfdINDCstQ

この動画では ISystem が解説されます。

DOTS107

https://www.youtube.com/watch?v=_35CzCxPqTg

この動画では IAspect が解説されます。

DOTS108

https://www.youtube.com/watch?v=n2NvbNtzkCE

この動画では Entity を Instantiateする方法などが解説されます。 Fee Asset モーション付き 3D model を表示させる方法などが解説されます。 この動画からぐっと見栄えが派手になります。 メモ:URPじゃないアセットのMaterialをURPように変換するコマンド

Edit > Rendering > Materials > Convert Selected build-in Materials to URP

DOTS109

https://www.youtube.com/watch?v=vs6h4waQpPA

DOTS上でモーション付きモデルを表示させる方法について解説されます。 EntitiyのInstatiateとDestroyまでの流れも解説され、ようやくゲームっぽい感じになってきます。 以下のようなQueryを使うことによって、オブジェクトを横断して共通なComponentを取得することなどがコードから理解出来るようになります。

foreach (var entity in SystemAPI.Query<Speed>() {
  // do something
}

DOTS110

https://www.youtube.com/watch?v=YzezqDqr7RM

MouseでClickした箇所にTowerを配置する方法が解説されます。

DOTS111

https://www.youtube.com/watch?v=YzezqDqr7RM

Towerから弾を発射し、EnemyにHitしたらEnemyのHelthを減少させ、Healthが0以下になったら削除する。 といった一連の流れが解説されます。 この動画から動画中のソースと実行画面の不整合が発生しはじめ写経ポイントです!

  • PhysicsCategory Assetsが突然現れる

Create -> Unity Physics -> Physics category name で作成可能です。

  • 弾が配置されない!

以下のように修正する必要があります。 ProjectileSystem.cs

                       ecbBOS.SetComponent(e, new Translation() { Value = towerPosition });

vvv

                        ecbBOS.SetComponent(e, new LocalTransform
                        {
                            Position = towerPosition,
                            Rotation = quaternion.identity,
                            Scale = 1
                        });

こちらも先述したTransfomrAspect の removeの影響です

  • 敵にあたらない弾が消えない 動画中だと消えているように見えるのですが、おそらく次の動画で説明するソースにCodeが記述されているようです。 この動画までの記事にしたいので自前で実装します。

Target.cs

public struct Target : IComponentData
{
    public Entity Value;
    public float timeLife;  // << 追加
}

ProjectileSystem.cs

                 ....

                    ecbBOS.AddComponent(e,
                        new Target()
                            { Value = closestHitCollector.ClosestHit.Entity});

                 ....

        foreach(var (speed,target,transform,entity ) in SystemAPI.Query<RefRO<Speed>, RefRO<Target>, RefRW<LocalTransform>>().WithEntityAccess())
        {
            //Debug.Log($"HasComponent:{positionLookup.HasComponent(target.ValueRO.Value)},{target.ValueRO.timeLife},{Time.time}"); 
            if(positionLookup.HasComponent(target.ValueRO.Value) )
            {
                 ....

vvv

                 ....

                    ecbBOS.AddComponent(e,
                        new Target()
                            { Value = closestHitCollector.ClosestHit.Entity, timeLife = Time.time+ 3.0f });

                 ....

        foreach(var (speed,target,transform,entity ) in SystemAPI.Query<RefRO<Speed>, RefRO<Target>, RefRW<LocalTransform>>
().WithEntityAccess())
        {
            //Debug.Log($"HasComponent:{positionLookup.HasComponent(target.ValueRO.Value)},{target.ValueRO.timeLife},{Time.time}"); 
            if(positionLookup.HasComponent(target.ValueRO.Value) && target.ValueRO.timeLife > Time.time)
            {
                 ....

最後に

各動画のソースは branch に分かれて公開されているので、どうしても動かないという方は各 package の version を揃えてそちらも参考にしてみると良いと思います。素晴らしいですね。

https://github.com/WAYN-Games/DOTS-Training/tree/main

これはもう別Engineなんじゃない?というくらい書き方変わりますが、プログラマ的にはごりごりに最適化出来そうなので ボクハエガカケナイ という方は全力でいくのもアリなんじゃないでしょうか。 STGを書くのが楽しそうです。Collider周りも大きく変更されているので、それはまた別の記事で。

Thanks ! WAYN Games