スポンサーリンク
最適化・パフォーマンス

【Unity 6対応】Job SystemとBurst Compilerの使い方|スレッド処理でゲームを高速化する方法

最適化・パフォーマンス

Unityでゲームを作っていると、敵やオブジェクトが増えたタイミングで「急に重くなった……」と感じることがありますよね。

そんなときに候補になるのが、CPU処理を効率よく分担できるJob Systemと、処理をさらに最適化できるBurst Compilerです。

ただし、どんな処理にも使えばよいわけではありません。この記事では、Unity 6でJob SystemとBurst Compilerを使う前に、どの処理へ導入すべきか、どう実装すればよいかを順番に見ていきます。


Unity Job Systemを使うべき処理

Job Systemは便利な機能ですが、すべての処理をJob化すればゲームが速くなるわけではありません。

まずは「自分の処理がJob Systemに向いているか」を判断することが大切です。ここを間違えると、実装の手間が増えるだけで、ほとんど効果が得られないこともあります。

数千回以上の計算なら検討する

Job Systemが特に効果を発揮しやすいのは、CPUが同じような計算を大量に繰り返す処理です。

例えば、次のようなケースが挙げられます。

  • 大量のオブジェクトの座標を毎フレーム更新する
  • 多数の敵AIの移動や行動を一括で計算する
  • 距離計算や当たり判定の前処理を大量に行う
  • 物理シミュレーションや数値計算を繰り返す

これらは各データを独立して処理できることが多いため、複数のCPUコアへ分散しやすく、Job Systemの恩恵を受けやすい処理です。

一方で、数個しか処理しないような軽い計算では、Jobをスケジュールするオーバーヘッドのほうが大きくなり、かえって遅くなることもあります。

軽い処理やUI更新には使わない

次のような処理は、基本的にJob Systemへ移行するメリットはあまりありません。

  • UI(TextやButton)の更新
  • GameObjectやTransformを直接操作する処理
  • 毎フレーム数回しか実行されない軽い処理
  • Unity APIへのアクセスが中心となる処理

Job Systemでは、通常のMonoBehaviourから利用できるUnity APIの多くをそのまま使用できません。

そのため、「ゲームオブジェクトを直接動かす処理」をそのままJobへ移すのではなく、座標や速度などの計算だけをJobで行い、その結果をメインスレッドで反映するという役割分担が基本になります。

迷ったらProfilerでCPU負荷を見る

導入するか迷った場合は、まずUnity ProfilerでCPU負荷を確認するのがおすすめです。

例えば、Update内で同じ計算が何千回も繰り返されていたり、CPU使用時間の大半を占めていたりする場合は、Job Systemを導入する価値がある可能性があります。

逆に、描画処理やGPU処理がボトルネックであれば、Job Systemでは期待した効果が得られないこともあります。

CPU負荷の見つけ方やUpdate処理を減らす方法については、こちらの記事でも詳しく解説しています。




Job System導入前の確認点

Job Systemを使う前に、今の処理がJob化しやすい形になっているか確認しておきましょう。

ここを飛ばして実装を始めると、「書き換えたのに動かない」「思ったより速くならない」という状態になりやすいです。

Unity APIを直接使う処理は避ける

Jobの中では、GameObjectや通常のTransform操作など、Unity APIの多くを直接扱えません。

たとえば、次のような処理をそのままJobへ入れるのは避けたほうが安全です。

  • GameObject.Find()でオブジェクトを探す
  • transform.positionを直接変更する
  • GetComponent()でコンポーネントを取得する
  • UIテキストや画像を更新する

Job Systemでは、こうした操作を直接行うのではなく、計算だけをJobで行い、結果の反映はメインスレッドで行う形にします。

データをNativeArrayに分けられるか見る

Job Systemに向いている処理は、入力データと出力データを配列のように分けられる処理です。

たとえば、敵の現在位置を入力として受け取り、計算後の移動先を出力するような処理はJob化しやすいです。

確認することJob化しやすい例
入力データ現在位置、速度、方向、HPなど
出力データ移動後の座標、計算結果、判定結果など
処理の特徴1件ずつ独立して計算できる処理

反対に、処理の途中で別のGameObjectを探したり、コンポーネントの状態を何度も参照したりする処理は、Job化の前にデータの持ち方を整理する必要があります。

Dispose管理できないならまだ使わない

Job Systemでは、NativeArrayなどのNativeContainerを使うことが多いです。

これらは通常のC#配列とは違い、使い終わったら自分でDispose()してメモリを解放する必要があります。

Disposeを忘れると、メモリ使用量が増え続けたり、Unityから警告が出たりする原因になります。

まずは次のチェックを満たせる処理から始めると安心です。

  • 使うデータを配列として用意できる
  • Jobの完了後に結果を受け取る流れが作れる
  • Complete()後にDispose()する場所を決められる

メモリ管理やGCによる処理落ちについて整理したい場合は、こちらも参考になります。




Job Systemの最小実装手順

Job Systemは難しく見えますが、最初に覚える流れはとてもシンプルです。

基本は、データを用意する → Jobを作る → 実行予約する → 完了を待つ → メモリを解放するという順番です。

NativeArrayを用意する

Jobでは、通常の配列ではなくNativeArrayなどのNativeContainerを使います。

まずは、計算に使う入力データと、結果を入れる出力データを用意しましょう。

using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public class JobSample : MonoBehaviour
{
    private NativeArray<float> numbers;
    private NativeArray<float> results;

    private void Start()
    {
        numbers = new NativeArray<float>(1000, Allocator.TempJob);
        results = new NativeArray<float>(1000, Allocator.TempJob);

        for (int i = 0; i < numbers.Length; i++)
        {
            numbers[i] = i;
        }
    }
}

Allocator.TempJobは、短時間で終わるJob向けの指定です。まずは数フレーム以内で完了する処理に使う、と覚えておくと扱いやすいです。

structでJobを作る

Jobはクラスではなく、structで作ります。

1つずつ順番に処理するならIJob、配列の各要素を並列処理したいならIJobParallelForを使います。

大量のデータを扱う場合は、まずIJobParallelForから考えると実用的です。

public struct MultiplyJob : IJobParallelFor
{
    public NativeArray<float> numbers;
    public NativeArray<float> results;

    public void Execute(int index)
    {
        results[index] = numbers[index] * 2f;
    }
}

Executeの中に、Jobで行いたい計算を書きます。

ここでは、配列の各要素を2倍にして、結果用の配列へ入れています。

Scheduleで実行する

Jobを作ったら、Schedule()でUnityに実行を予約します。

IJobParallelForでは、処理するデータ数と、まとめて処理する単位を指定します。

private void Start()
{
    numbers = new NativeArray<float>(1000, Allocator.TempJob);
    results = new NativeArray<float>(1000, Allocator.TempJob);

    for (int i = 0; i < numbers.Length; i++)
    {
        numbers[i] = i;
    }

    MultiplyJob job = new MultiplyJob
    {
        numbers = numbers,
        results = results
    };

    JobHandle handle = job.Schedule(numbers.Length, 64);
}

上の例では、1000個のデータをJobで処理します。

64はバッチサイズです。細かく分けすぎると管理コストが増え、大きすぎると分散しにくくなるため、まずは32〜128あたりを試して実測するのがおすすめです。

CompleteしてDisposeする

Jobの結果を使う前には、必ずComplete()で完了を待ちます。

その後、使い終わったNativeArrayDispose()で解放します。

private void Start()
{
    numbers = new NativeArray<float>(1000, Allocator.TempJob);
    results = new NativeArray<float>(1000, Allocator.TempJob);

    for (int i = 0; i < numbers.Length; i++)
    {
        numbers[i] = i;
    }

    MultiplyJob job = new MultiplyJob
    {
        numbers = numbers,
        results = results
    };

    JobHandle handle = job.Schedule(numbers.Length, 64);

    handle.Complete();

    Debug.Log(results[10]);

    numbers.Dispose();
    results.Dispose();
}

Complete()を呼ぶ前に結果を読もうとすると、Jobがまだ終わっていない可能性があります。

また、Dispose()を忘れるとメモリが解放されません。Job Systemでは、処理の速さだけでなく、CompleteとDisposeをセットで管理することが大切です。

まずはこの最小形で動かしてから、処理内容を少しずつ実際のゲーム用に置き換えていくと、つまずきにくいです。




Burst Compilerで高速化する

Job Systemで処理を分けられるようになったら、次はBurst Compilerを使って、Jobの中の計算をさらに高速化できるか確認してみましょう。

Burst Compilerは、Jobで書いたC#の計算処理を、CPUが実行しやすい形に最適化してくれる機能です。特に、大量の数値計算や配列処理では効果が出やすいです。

Burstを有効化する

まず、Unity上でBurstが有効になっているか確認します。

  1. Unity上部メニューからJobsを開く
  2. Burstを選ぶ
  3. Enable Compilationにチェックを入れる

環境によっては、Burstパッケージがまだ入っていない場合があります。その場合は、Package ManagerからBurstを追加してください。

確認の目安は、メニューにBurst関連の項目が表示されているかどうかです。表示されていなければ、まずパッケージの導入状態を見直しましょう。

JobにBurstCompileを付ける

Burstを使うには、Jobの構造体に[BurstCompile]を付けます。

先ほどのJobにBurstを適用すると、次のようになります。

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;

[BurstCompile]
public struct MultiplyJob : IJobParallelFor
{
    public NativeArray<float> numbers;
    public NativeArray<float> results;

    public void Execute(int index)
    {
        results[index] = numbers[index] * 2f;
    }
}

基本的には、これだけでBurstの対象になります。

ただし、Burstが効果を出しやすいのは、GameObject操作ではなく、数値計算や配列処理が中心のJobです。UI更新やコンポーネント取得のような処理を高速化する目的では使いにくいので注意しましょう。

Burstで書けないコードを避ける

Burst Compilerには、通常のC#よりも書き方の制約があります。

たとえば、次のようなコードはBurst向きではありません。

  • classなどの参照型を使う処理
  • stringを多用する処理
  • 通常のC#配列を使う処理
  • try-catchなどの例外処理
  • Unity APIを直接呼び出す処理

最初は、NativeArraystruct、数値型を中心にしたシンプルなJobから始めるのが安心です。

もしBurstを付けたあとにエラーが出た場合は、「Burstが悪い」のではなく、Jobの中にBurstで扱いにくいコードが混ざっている可能性があります。

判断に迷ったら、まずはBurstなしでJobが正しく動くか確認し、その後で[BurstCompile]を付けて比較しましょう。動作確認と高速化を分けて進めると、原因を切り分けやすくなります。




うまく速くならない原因

Job SystemとBurst Compilerを入れても、必ず大きく速くなるとは限りません。

効果が出ないときは、実装ミスだけでなく、そもそも高速化したい場所がJob System向きではない可能性があります。まずは原因を切り分けてみましょう。

処理が軽すぎる

Job Systemには、処理をJobとして登録したり、完了を待ったりするための管理コストがあります。

そのため、数回〜数十回程度の軽い計算では、Job化するより普通にUpdate内で処理したほうがシンプルで速い場合があります。

判断の目安は、次のような状態です。

  • 処理対象が少なく、数十個程度で収まっている
  • Profilerで見てもCPU負荷がほとんど目立たない
  • Job化してもFPSやCPU時間に変化がない

この場合は、無理にJob Systemへ移すより、コードの整理やUpdate回数の削減を優先したほうが効果的です。

メインスレッド待ちが多い

JobはSchedule()したあと、別の処理と並行して進められることに意味があります。

しかし、Scheduleした直後にすぐComplete()を呼ぶと、メインスレッドがJobの終了を待つ時間が増えます。

JobHandle handle = job.Schedule(data.Length, 64);
handle.Complete();

この書き方自体は間違いではありません。小さなサンプルではよく使います。

ただ、実際のゲームでは、Jobを予約したあとに他の処理を進め、結果が必要になるタイミングでComplete()するほうが、並列処理のメリットを活かしやすくなります。

「Jobにしたのに速くならない」と感じたら、Complete()を呼ぶタイミングが早すぎないか確認してみてください。

Job化する場所が違う

Job Systemが得意なのは、CPU上で行う大量の計算です。

もし重さの原因が描画、シェーダー、テクスチャ、ポストエフェクトなどにある場合、Job Systemを入れても改善しにくいです。

重い原因見直す対策
大量の数値計算Job System・Burst Compiler
Update内の処理が多い処理頻度の見直し・Job化の検討
描画やシェーダーが重い描画設定・Shader・Compute Shaderなど
生成と破棄が多いオブジェクトプール

描画やGPU側の処理が原因になっている場合は、Job Systemではなく別の最適化が必要です。

GPUを使った処理について知りたい場合は、こちらの記事も参考になります。




この記事でやる次の行動

ここまで読んで「Job Systemを使うべきかも」と感じたら、いきなりゲーム全体を書き換える必要はありません。

まずは、重くなっている処理を1つだけ選んで、小さく試すのがおすすめです。

まずProfilerで重い処理を探す

最初に見るべきなのは、Unity ProfilerのCPU使用時間です。

Update内の処理や、自作スクリプトの処理時間が目立っているか確認しましょう。

  • 毎フレーム大量に繰り返している計算がある
  • 敵や弾、オブジェクトが増えると急に重くなる
  • CPU側の処理時間がフレーム落ちの原因になっている

このような状態なら、Job Systemを試す候補になります。

1つの処理だけJob化する

次に、重い処理を1つだけ選んでJob化します。

おすすめは、座標計算や距離計算のように、入力と出力を分けやすい処理です。

いきなりAI全体やゲーム管理処理をJob化しようとすると、原因切り分けが難しくなります。

  • 現在位置を配列に入れる
  • Jobで移動先や判定結果を計算する
  • 結果をメインスレッドで反映する

このくらい小さな単位で始めると、動作確認もしやすくなります。

Burstあり・なしで比較する

Jobが正しく動いたら、最後にBurst Compilerを付けて比較します。

確認するときは、感覚だけではなくProfilerでCPU時間を見ましょう。

  • Job化前
  • Job化後
  • Job化+Burst後

この3つを比べると、本当に効果が出ているか判断しやすくなります。

もし差が小さい場合は、その処理はJob System向きではないか、別の場所がボトルネックになっている可能性があります。

Job Systemは「入れれば必ず速くなる魔法」ではなく、重い計算を見つけて、必要な場所にだけ使うための道具です。まずは1つの処理で試して、効果を見ながら広げていきましょう。




まとめ

Job SystemとBurst Compilerは、CPU負荷の高い処理を効率よく高速化するための強力な機能です。

ただし、すべての処理に導入すれば効果があるわけではありません。まずはProfilerでボトルネックを特定し、座標計算や大量の繰り返し処理など、Job Systemが得意な処理から試してみることが大切です。

最初は「NativeArrayを用意する → Jobを作る → Scheduleする → Completeする → Disposeする」という基本の流れだけ覚えれば十分です。そのうえでBurst Compilerを組み合わせ、Profilerで効果を確認しながら少しずつ適用範囲を広げていくと、無理なくパフォーマンス改善を進められます。

まずはゲーム内で最もCPU負荷の高い処理を1つ選び、Job SystemとBurst Compilerの効果を実際に比較してみましょう。


よくある質問(FAQ)

Q
Coroutineの代わりにJob Systemを使うべきですか?
A

目的が違うため、置き換えるものではありません。

Coroutineは、処理を数秒後に実行したり、時間をかけて少しずつ進めたりするときに使います。一方、Job SystemはCPUで行う大量計算を分散するための仕組みです。

待機や演出の流れを作りたいならCoroutine、大量の計算を軽くしたいならJob Systemを検討しましょう。

Q
ECSを使わなくてもJob Systemは使えますか?
A

使えます。

Job SystemとBurst Compilerは、ECSを使っていない通常のUnityプロジェクトでも利用できます。

まずはMonoBehaviourの中でNativeArrayを用意し、計算部分だけをJobに分ける形から始めると試しやすいです。ECSまで一気に学ぼうとせず、Job System単体で小さく使ってみるのがおすすめです。

Q
Job Systemを使えば必ずFPSは上がりますか?
A

必ず上がるわけではありません。

Job Systemが効果を出しやすいのは、CPU上で大量の計算をしている場合です。描画、シェーダー、ロード処理、オブジェクト生成などが原因で重い場合は、別の対策が必要になります。

導入前後はProfilerで比較し、CPU時間が実際に減っているかを確認しましょう。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク