0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity6のゲーム開発にて、DOTSを本格導入してみた!(Vol.1)

0
Last updated at Posted at 2026-05-17

Unity6のDOTSについて

今現在Unityは、Unity6へと進化したことで、今までのオブジェクト指向でのゲーム開発ではなく、データ型指向への転換を推奨し始めています。
私的には、ただUnityでゲームを作れる、オブジェクト指向でゲームが作れる、のではなく、どれだけ既存エンジンの限界を引き出し、どれだけパフォーマンスを下げずにゲームをプレイ出来るようにするのか、という人材が今後求められるのではと感じます。

Vol.1のテーマ

ということで、今回は今私が開発しようと企てている弾幕ゲームを作るために、JobSystem、そして、BurstCompilerを使い、ゲーム開発のCoreから作っていこうと思います。

第一の課題

弾幕ゲームにはもちろん、が必要です。
ですが、例えばヒエラルキーに球オブジェクトを追加、Prefab化、それを標準機能のInstantiateを使い、オブジェクトを大量に生成してしまうと、一気にFPSが低下し、最悪開発中にUnity自体がクラッシュする、ということもあります。
(※実際同じくゲーム開発をしている友人にも、開発中に何度もクラッシュし、コンテストの発表時間でフリーズしたりなどのアクシデントがありました)
それを防ぐために必要なのは、このDOTSの考え方なのです。

GameObjectは「デブすぎる」

UnityのGameObjectは位置情報だけじゃなく、レンダラー、物理、スクリプトなどを贅肉を一人で抱えた巨大な参照型の塊なのです。そのため、それをInstantiateすると、メモリ空間のあちこちに、データが断片化して配置されてしまいます。
それにより、CPUが「次の弾のデータを処理しよう」とした瞬間に、メモリのあちこちを探し回るハメになり、キャッシュミスが発生してしまい、それがゲームでの挙動のカクつき、そして最悪クラッシュに繫がってしまいます。

実験してみよう

実験として、まずは、Sphereをヒエラルキーに追加し、Prefab化、そしてInstantiateコードを書いて、やってみると...

image.png
Statisticsの結果にある通り、FPSはほぼ瀕死状態、インスタンスを9個生成しようとした時点で、DrawCallは35回、これ以上はUnityがクラッシュしかけたので、実行を止めました。

ここから本題

では、ここからCoreの本実装を入っていきます。
まずは、弾のデータ、そして今後作る必要のあるデータを管理するSharedData.csを作っていきます。
(バージョンは6000.4.5f1)

// SharedData.cs
using Unity.Mathematics;
using Unity.Entities;

namespace Core.Shared
{
   public struct BulletComponent : IComponentData
   {
      public float3 Position;
      public float3 Velocity;
      public float LifeTime;
      public bool isActive;
   }
}

まず、こちらのコードは、Unityが提供しているEntitiesパッケージを使用し、その中のIComponentDataというインターフェースをBulletComponentに継承させて、純粋なデータ構造体として、このDataを使います。

なぜ IComponentData(構造体)なのか?

ここがオブジェクト指向(OOD)からデータ指向(DOD)への最大の転換点です。
従来の MonoBehaviour はクラス(参照型)だったためメモリ上にバラバラに配置されていましたが、この IComponentData を実装した構造体(値型)は、Unityの内部システム(Archetypeチャンク)によって、メモリ上で隙間なく一列に並べられます。
これにより、CPUはメモリのあちこちを探し回る必要がなくなり、1つのデータを読み込むついでに、隣にある次の弾のデータも一気にキャッシュメモリへ先読み(プリフェッチ)できるようになります。キャッシュミスを極限までゼロにするための、これがデータ指向の第一歩です。

そしてここから、実際に弾の計算をJobSystem,BurstCompilerを使い、UpdateBulletJob.cs、描画を行うために、CoreTicker.csを作っていきます。

// UpdateBulletJob.cs
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;

namespace Core.Job
{
 [BurstCompile]
public struct UpdateBulletJob : IJobParallelFor
{
    // NativeArrayを使うことで、全コアから安全にアクセスできる
    public NativeArray<Shared.BulletComponent> Bullets;
    public NativeArray<Matrix4x4> Matrices;
    public float DeltaTime;

    // i は各スレッドに割り振られたインデックス
    public void Execute(int i)
    {
        Shared.BulletComponent bullet = Bullets[i];
        if(!bullet.isActive) return;

        // 座標更新
        bullet.Position += bullet.Velocity * DeltaTime;
        bullet.LifeTime -= DeltaTime;

        if(bullet.LifeTime <= 0)
        {
            bullet.isActive = false;
        }
        else
        {
            Matrices[i] = Matrix4x4.TRS(bullet.Position, Quaternion.identity, Vector3.one * 0.2f);
        }

        Bullets[i] = bullet;
    }
}   
}

// CoreTicker.cs
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

namespace Core
{
    public class CoreTicker : MonoBehaviour
{
    private const int MaxBullets = 100000;
    private NativeArray<Shared.BulletComponent> _bullets;
    private NativeArray<Matrix4x4> _gpuMatrices;

    [SerializeField] private Mesh _bulletMesh;   // 弾のモデル
    [SerializeField] private Material _bulletMat; //弾の材質

    private Matrix4x4[] _matrices = new Matrix4x4[MaxBullets]; // GPUに渡す行列データ
    private RenderParams _renderParams;

    void Start()
    {
        _renderParams = new RenderParams(_bulletMat);

        // メモリ確保
        _bullets = new NativeArray<Shared.BulletComponent>(MaxBullets, Allocator.Persistent);
        _gpuMatrices = new NativeArray<Matrix4x4>(MaxBullets, Allocator.Persistent);
    }


    void Update()
    {
        TickPlayer();
        TickBullets();
    }

    void OnDestroy()
    {
        // NativeArray必ず手動でDisposeが必要
        if(_bullets.IsCreated) _bullets.Dispose();
        if(_gpuMatrices.IsCreated) _gpuMatrices.Dispose();
    }

    private void TickPlayer()
    {
        // あくまで実験作業のため、旧来のInput.GetKeyを使用して、入力検知を実装
        if(Input.GetKey(KeyCode.D))
        {
            UnityEngine.Debug.Log("D key is pressed!");
            int count = 36;
            float speed = 10f;

            for(int i = 0; i < count; i++)
            {
                float angle = i * (360f / count) * Mathf.Deg2Rad;
                float3 dir = new float3(Mathf.Cos(angle), 0, Mathf.Sin(angle));

                EmitBullet(transform.position, dir * speed);
            }
        }
    }
    private void TickBullets()
    {
        var job = new Job.UpdateBulletJob
        {
           Bullets = _bullets,
           Matrices = _gpuMatrices,
           DeltaTime = Time.deltaTime  
        };

        JobHandle handle = job.Schedule(MaxBullets, 64);

        // Jobの完了を待機 
        handle.Complete();

        int activeCount = 0;

        for (int i = 0; i < MaxBullets; ++i)
        {
            if(_bullets[i].isActive)
            {
                _matrices[activeCount] = _gpuMatrices[i];
                activeCount++;
            }
        }
        
            // まとめて描画命令を発行
        if (activeCount > 0)
        {
           _renderParams.worldBounds = new Bounds(Vector3.zero, Vector3.one * 2000f);
           Graphics.RenderMeshInstanced(_renderParams, _bulletMesh, 0, _matrices, activeCount);
        }
    }

    private void EmitBullet(float3 position, float3 velocity)
    {
        for(int i = 0; i < MaxBullets;i++)
        {
            var bullet = _bullets[i];
            if(!bullet.isActive)
            {
                bullet.Position = position;
                bullet.Velocity = velocity;
                bullet.LifeTime = 60.0f;
                bullet.isActive = true;

                _bullets[i] = bullet;
                return;
            }
        }
    }
}   
}


ここまでやったあとの結果がこちらです。

スクリーンショット 2026-05-17 132153.png
image.png

FPSは65.3 FPS、それも10万個のオブジェクトを、242DrawCallまで抑えて描画できています。

なぜここまで差が出るのか

従来の Instantiate では4.8 FPSでクラッシュ寸前だった環境が、なぜこのコードに変えただけで、10万個を動かしても65.3 FPSを維持できるのか。

理由は大きく分けて3つ、すべて「CPUとメモリの物理的な特性を味方につけたから」です。

  1. [BurstCompile] と float3 による「SIMD自動ベクトル化」
    UpdateBulletJob の頭に宣言されている [BurstCompile] 属性。これがC#の実行速度を完全にネイティブ(C++)の領域へと引き上げます。

Unity 6のBurstコンパイラは、C#の中中間言語(IL)を完全に無視し、LLVMを介して対象CPU専用の最適化マシンコード(ネイティブアセンブリ)を直接吐き出します。
さらに、Shared.BulletComponent で使用している float3(Unity.Mathematics)による座標計算は、コンパイラによってSIMD(単一命令複数データ処理)命令へと自動変換されます。

CPUは10万個の弾を1個ずつモタモタ計算しているのではなく、1回の命令サイクルで4個、あるいは8個の弾の座標を同時に並列書き換えしているのです。

IJobParallelFor によるマルチスレッドの完全掌握

CoreTicker.cs 内の以下の呼び出しに注目してください。

JobHandle handle = job.Schedule(MaxBullets, 64);

これは、「10万個の弾の更新ループを、64個ずつの塊(インナーループ)にスライスして、CPUの空いている全Worker Thread(論理コア)へ一斉に分配しろ」という命令です。

従来のMonoBehaviourのようにメインスレッドを1本の巨大なループで占有しないため、裏で10万発をブン回している間もメインスレッドのフレームタイムは最小限に抑えられ、ゲームの入力検知やUIの更新が重くなる要素を根本から排除しています。

弾幕にPrefabやBakerは不要

既存のDOTS記事の多くは、Prefabを Baker システムでEntityに変換する手法を紹介しています。しかし、グラフィックスAPIの低レイヤー視点に立てば、描画に必要なのはMesh(形状データ)Material(材質データ)Matrix4x4(位置行列)の3つだけです。
複雑なFBXモデルならまだしも、単一のMeshで済む「弾」に対して、わざわざUnityの重たいPrefabやオーサリングを介してメモリを汚す必要はありません。
CoreTicker.cs で行っているように、生のアセット(Mesh/Material)を直接保持し、Jobで計算した生行列をそのままUnity 6の新API Graphics.RenderMeshInstanced へゼロコピーで流し込む。

Prefabという古い贅肉をメモリに1Bitも載せない。これこそが、本質的なデータ指向(DOD)設計の正体です。

まとめ

今回は、弾幕ゲームのCoreとなる「CPU側の並列化」と「Prefabを完全排除した最速の描画ルート」を実装し、10万個を実用プロトタイプとして動かすファクトを示しました。

ですが、いくらCPU側を爆速にし、Graphics.RenderMeshInstanced で10万発をねじ伏せたとしても、ゲームが複雑化して敵やステージのデータ、複雑なマテリアルが絡んでくると、今度はこのC#配列の詰め直しループ(for (int i = 0; i < MaxBullets; ++i))自体がメインスレッドの新たなボトルネックになり始めます。

そこで次回の Vol.2 では、このCPU・GPU間のデータのやり取りすらも完全に非同期・ゼロコピー化する、Unity 6の真の切り札「BatchRendererGroup(BRG)のハックと、グラフィックスバッファへの生データ流し込み」について、実装したのち、解説していきたいと思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?