DOTS を使ってゲームオブジェクト ゲームを最適化する: 『サバイバルキッズ』のケーススタディ | Unite 2025

281 Views

February 05, 26

スライド概要

profile-image

リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。

Docswellを使いましょう

(ダウンロード不可)

関連スライド

各ページのテキスト
2.

DOTS を使って ゲームオブジェクト ゲームを最適化する サバイバルキッズのケーススタディ

3.

発表タイトル Andy Bastable スタッフソフトウェアエンジニア、Unity Studio Productions

4.

サバイバルキッズ: プロジェクトと 制約 アジェンダ 細かい DOTS を使ってゲーム オブジェクトのゲームを強化する このプロジェクトの実践的な ケーススタディ 重要なポイントと質問の時間!

6.

サバイバルキッズ 完全に Unity による設計と開発 パブリッシャーパートナーの Konami Digital Entertainment B.V. と提携 のローンチタイトル 最大 4 人のプレイヤー向けの クラフトサバイバルゲーム 製品の内部検証に使用

8.

サバイバルキッズ みんなのためのサバイバル 一緒にいる方が良い 親しみやすいビジュアル Unity 6 がサポート Unity Netcode + UGS URP + Shader Graph 公開されているバージョンのエディターと パッケージで開発 Netcode for Entities のカスタムゲーム オブジェクトレイヤー、UGS の Lobby と Relay 動的ライティングと の 60 fps という画像品質に対応するアダプ ティブプローブボリュームと MSVAO

9.

なぜこれが 良いケーススタディ なのか? 大規模なゲームオブジェクトベースのゲー ムである (典型的なゲームではオブジェク トが 1000 を超える) 現実的なトレードオフ: 期限が決まっている + 高パフォーマンス 公開版のエディター + パッケージなので、こ れらの技術を使用できる

10.

DOTS 単なる Entities ではない System Burst Jobs Native Collections TransformAccessArray

11.

必要なツール ビルトインプロファイラー 2 つのプロファイルキャプチャを分析する PIX、Superluminal、Nintendo CPU プロファイラー Unity プロファイラー Profile Analyzer パッケージ サンプリングプロファイラー エディターに組み込まれています。 常に使用してください。 任意でダウンロード可能なパッケージ。 プロファイラーキャプチャの前後を比 較できます。 サンプリングプロファイラーを使って、 デバイス上で実行されている出荷タイトルから 正確なタイミングデータを取得できます。

12.

免責事項 すべてのパフォーマンスキャプチャは、 ワークステーションで実行 されている Windows 開発ビルドから取得 されました これらの結果は単一のプロジェクトのもので あり、他のタイトルや環境を必ずしも示す ものではありません 各自の結果は、ワークロード、ハードウェア、 または設定によって異なる場合があります

13.

MonoBehaviour を 更新するために System を使う

14.

MonoBehaviour の Update() について フレーム内の固定位置 潜在的にランダムな実行順序 貧弱なキャッシュローカリティ

15.

“L1 キャッシュは手に持ったビール、 L2 キャッシュはソファのそばの クーラーボックス、L3 は冷蔵庫まで行くこと、 メインメモリは店に歩いて行くこと、 ディスクアクセスはビールのために海外に 飛ぶようなものだ" Holly Cummins (ツイート)、 Richard Thompson によって拡散

16.

System で MonoBehaviour を収集、 更新できる System による救済 フレーム内のどこで System が更新 するかを選ぶ 関連するオブジェクトが一緒に更新 されるように更新をソートする

17.

Entities パッケージが 必要 com.unity.entities System にアクセスできるようになる Initialization、Simulation、Presentation シ ステムグループをデフォルトで実装

18.

フレームの順序 BehaviourUpdate InitializationSystemGroup LateBehaviourUpdate SimulationSystemGroup Rendering PresentationSystemGroup

19.

発表タイトル セクションタイ トル コードサンプル System の設定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [UpdateInGroup(typeof(SimulationSystemGroup))] public partial class MonoBehaviourUpdateSystem :SystemBase { protected override void OnUpdate() { // update here } }

20.

発表タイトル セクションタイ トル コードサンプル 更新したい場所で更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(EarlierSystem))] [UpdateBefore(typeof(LaterSystem)] public partial class MonoBehaviourUpdateSystem :SystemBase { protected override void OnUpdate() { // update here } }

21.

発表タイトル セクションタイ トル コードサンプル シングルトンにする 1 ... 2 public partial class MonoBehaviourUpdateSystem :SystemBase 3 { 4 public static MonoBehaviourUpdateSystem Instance { get; private set; } 5 6 protected override void OnCreate() 7 { 8 Instance = this; 9 } 10 11 protected override void OnDestroy() 12 { 13 Instance = null; 14 } 15 16 ... 17 18 19 20 21 22 23 24 25

22.

発表タイトル セクションタイ トル コードサンプル ドメインリロードに安全なシングルトンにする 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... public partial class MonoBehaviourUpdateSystem :SystemBase { public static MonoBehaviourUpdateSystem Instance { get; private set; } #if UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetState() { Instance = null; } #endif protected override void OnCreate() { Instance = this; } ...

23.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
MonoBehaviours を Update に収集する
1 ...
2 public partial class MonoBehaviourUpdateSystem :SystemBase
3 {
4
private List<MonoBehaviour> m_UpdateBehaviours = new();
5
6
public void Register(MonoBehaviour behaviour)
7
{
8
// check for double register
9
Debug.Assert(!m_UpdateBehaviours.Contains(behaviour);
10
11
m_UpdateBehaviours.Add(behaviour);
12
}
13
14
public void Unregister(MonoBehaviour behaviour)
15
{
16
// check for double remove
17
Debug.Assert(m_UpdateBehaviours.Contains(behaviour);
18
19
m_UpdateBehaviours.RemoveSwapBack(behaviour);
20
}
21
22
...
23
24
25

24.

発表タイトル セクションタイ トル コードサンプル MonoBehaviour はそれ自体を登録する必要がある 1 public class MyMonoBehaviour :MonoBehaviour 2 { 3 public void Awake() 4 { 5 MonoBehaviourUpdateSystem.Instance.Register(this); 6 } 7 8 public void OnDestroy() 9 { 10 if(MonoBehaviourUpdateSystem.Instance != null) 11 { 12 MonoBehaviourUpdateSystem.Instance.Unregister(this); 13 } 14 } 15 16 public void Update() 17 { 18 ¥¥ lots of expensive calculations 19 ... 20 } 21 } 22 23 24 25

25.

発表タイトル セクションタイ トル コードサンプル Update を呼び出す 1 ... 2 public partial class MonoBehaviourUpdateSystem :SystemBase 3 { 4 ... 5 6 protected override void OnUpdate() 7 { 8 foreach(var behaviour in m_UpdateBehaviours) 9 { 10 behaviour.Update(); 11 } 12 } 13 14 ... 15 16 17 18 19 20 21 22 23 24 25

26.

発表タイトル セクションタイ トル コードサンプル インターフェースが必要 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface IManualUpdate { void ManualUpdate(); } public class MyMonoBehaviour :MonoBehaviour, IManualUpdate { public void ManualUpdate() { ¥¥ lots of expensive calculations ... } }

27.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
ここで、インターフェースを使って更新する
1 ...
2 public partial class MonoBehaviourUpdateSystem :SystemBase
3 {
4
private List<IManualUpdate> m_UpdateBehaviours;
5
6
public void Register(IManualUpdate behaviour)
7
{
8
...
9
10
public void Unregister(IManualUpdate behaviour)
11
{
12
...
13
14
15
protected override void OnUpdate()
16
{
17
foreach(var behaviour in m_UpdateBehaviours)
18
{
19
behaviour.ManualUpdate();
20
}
21
}
22
23
24
25

28.

場合による 更新順序は重要? 大規模で、同様のデータにアクセスする オブジェクトがある場合は、重要! テストできる!

29.

ランダムな順序 vs ソートされた順序 3 種類の異なるオブジェクトがあるとする A B C B A C A B B C A A C B C A A C C C B B A B B A C B A A A A A A A A A B B B B B B B B B B C C C C C C C C C それらはランダムな順序で作成された 可能性がある つまり、ランダムな順序で更新される それらが表すオブジェクトのタイプ別に ソートできる

30.

発表タイトル セクションタイ トル コードサンプル 更新する前にオブジェクトをソートする 1 ... 2 public partial class MonoBehaviourUpdateSystem :SystemBase 3 { 4 private bool m_ListIsDirty = false; 5 6 public void Register(IManualUpdate behaviour) 7 { 8 m_ListIsDirty = true; 9 ... 10 11 public void Unregister(IManualUpdate behaviour) 12 { 13 m_ListIsDirty = true; 14 ... 15 16 protected override void OnUpdate() 17 { 18 if(m_ListIsDirty) 19 { 20 SortUpdateList(); 21 m_ListIsDirty = false; 22 } 23 ... 24 25

31.

未ソート vs ソート済み 未ソートの更新: 0.99 ms ソート済みの更新: 0.87 ms

32.

未ソート vs ソート済み 平均更新コストで 12% スピードアップ 最悪のフレームでは 34% 改善! 機能的な違いはなく、ただ update が呼ばれる順序だ け * パフォーマンスデータは Unity の内部テストに基づいています。 詳細については前の免責事項のスライドを参照してください。

33.

TransformAccessArray を 使用する

34.

問題 ゲームオブジェクトへのアクセスは メインスレッドでしか行えない Transform だけが必要な場合がある ワーカースレッドにそれを移動できたら どうなるか?

35.

メインスレッドの束縛 メインスレッド ワーカースレッド

36.

ワーカースレッドのジョブから Transform へのアクセスを許可する Transform Access Array を入力する バーストできる 使用できるタイミングに関するいくつかの 制約 基本的に、メインスレッドが Transform にアクセスしない時間のみをスケジュール する

37.

ケーススタディ: UI 要素 ゲーム内オブジェクトの上に配置 ゲームカメラのオーバーレイとしての キャンバス レベルごとに 20 - 50 の HUD 要素 Camera.WorldToViewportPoint で変換 要素をカリングまたは固定し、方向付けのために 回転 すべてのカメラ に対してこれを行う必要がある

39.

Camera.WorldToView portPoint 管理関数。Camera オブジェクトが必要 メインスレッドで呼び出す必要がある 基本的に十分に理解されている行列数学

40.

HUD 要素の変換は高コスト 総コスト: 0.9 ms HUD カリングパス: 0.15 ms HUD Transform: 0.75 ms

41.

必要な Transform を事前にすべて収集する 改善する バーストコンパイルされたジョブを使って 作業する ジョブはワーカースレッドで実行できる 結果には終了時にメインスレッドでアクセス できる

42.

発表タイトル セクションタイ トル コードサンプル System の設定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [UpdateInGroup(typeof(SimulationSystemGroup))] public partial class UpdateHUDElementsSystem :SystemBase { public struct HUDElement { public float3 Position; public bool IsElementOffscreen; } private NativeArray<HUDElement> m_Elements; } protected override void OnUpdate() { }

43.

発表タイトル セクションタイ トル コードサンプル TransformAccessArray を作成する 1 ... 2 public partial class UpdateHUDElementsSystem :SystemBase 3 { 4 ... 5 private TransformAccessArray m_Transforms; 6 7 protected override void OnUpdate() 8 { 9 var Transform[] collectedTransforms = CollectTransforms(); 10 if (m_Transforms.isCreated) 11 { 12 m_Transforms.SetTransforms(transforms); 13 } 14 else 15 { 16 m_Transforms = new TransformAccessArray(transforms); 17 } 18 } 19 20 protected override void OnDestroy() 21 { 22 if (m_Transforms.isCreated) 23 { 24 m_Transforms.Dispose(); 25 }

44.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
Transform にアクセスするためのジョブを作成する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

[BurstCompile]
struct CopyTransformPositionsJob :IJobParallelForTransform
{
[WriteOnly] public NativeArray<HUDElement> Elements;

}

public void Execute(int index, TransformAccess transform)
{
if (transform.isValid)
{
Elements[index] = new HUDElement
{
Position = transform.position
};
}
}

45.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
ジョブをスケジュールする
1 ...
2 public partial class UpdateHUDElementsSystem :SystemBase
3 {
4
private TransformAccessArray m_Transforms;
5
6
private NativeArray<HUDElement> m_Elements;
7
8
protected override void OnUpdate()
9
{
10
new CopyTransformPositionsJob
11
{
12
Elements = m_Elements
13
}
14
.ScheduleReadOnly(m_Transforms, 1000);
15
16
17
18
19
20
21
22
23
24
25

46.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
ジョブを拡張して HUD 要素を処理する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

[BurstCompile]
struct TransformHUDElementsJob :IJob
{
[ReadOnly] public Rect CameraViewport;
[ReadOnly] public float4x4 WorldToClip;
[ReadOnly] public float4x4 CameraToWorld;
public NativeArray<HUDElement> Elements;

public void Execute()
{
for(int i = 0; i < Elements.Length; i++)
{
var element = Elements[i];
element.Position = WorldToViewportPoint(element.Position, CameraToWorld, WorldToClip, CameraViewport);
element.IsElementOffscreen = !CameraViewport.Contains(element.Position);

}

}

}

// write the results back
Elements[i] = element;

47.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
ジョブをスケジュールする
1 ...
2 public partial class UpdateHUDElementsSystem:SystemBase
3 {
4
...
5
private NativeArray<HUDElement> m_Elements;
6
7
protected override void OnUpdate()
8
{
9
var copyHandle = new CopyTransformPositionsJob
10
{
11
m_Elements = m_Elements
12
}
13
.ScheduleReadOnly(m_Transforms, 1000);
14
15
var camera = Camera.main
16
var processHandle = new TransformHUDElementsJob
17
{
18
CameraViewport = camera.pixelRect,
19
WorldToClip = camera.projectionMatrix * camera.worldToCameraMatrix,
20
CameraToWorld = camera.cameraToWorldMatrix
21
}
22
.Schedule(copyHandle);
23
24
25

48.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
結果を使う
1 ...
2 public partial class UpdateHUDElementsSystem :SystemBase
3 {
4
...
5
private NativeArray<HUDElement> m_Elements;
6
7
private JobHandle m_TransformHUDElementsJobHandle;
8
9
protected override void OnUpdate()
10
{
11
...
12
m_TransformHUDElementsJobHandle = new TransformHUDElementsJob
13
{
14
...
15
}
16
.Schedule(copyHandle);
17
}
18
19
public NativeArray<HUDElement> GetProcessedHUDElements()
20
{
21
m_TransformHUDElementsJobHandle.Complete();
22
return m_Elements;
23
}
24
25

49.

発表タイトル セクションタイ トル コードサンプル 結果を公開する 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(OtherSystem))] public partial class ApplyHUDElementsSystem :SystemBase { protected override void OnUpdate() { var elements = UpdateHUDElementsSystem.Instance.GetProcessedHUDElements(); } } for(int i = 0; i < elements .Length; i++) { var element = elements [i]; m_CollectedHUDElements[i].transform.position = element.Position; }

50.

実践:500 の HUD 要素を変換する System:0.009 ms 位置コピージョブ (0.03 ms) 要素変換ジョブ (0.02 ms)

51.

.Schedule を使う 必要はありません バーストジョブはメインスレッドで実行 できる .Run を使って .Complete を呼び出す必要を なくす メインスレッドがアイドルのときに作業を 行うのに便利

52.

便利なテクニック: アイドルの隙間に作業を隠す WaitForJobGroupID:0.27 ms

53.

HUD 要素変換ジョブが完璧にフィット! リストの構築:0.04 ms 処理:0.01 ms 結果の適用:0.01 ms 総コスト:0.21 ms

54.

ゲームオブジェクトにネットワーク変換を書き込む その他の使用法 ネットワークに送信するためにゲームオブジェクトの Transform を読み取る 休止とレンダリングのためにプレイヤーの近接を チェックする 空間クエリのために KDTree を構築する

55.

KDTree を使って 空間クエリを最適化する

56.

KDTree マルチスレッド化およびバーストされた 空間ルックアップ MegaCity 2019 の一部として出荷 サバイバルキッズ (および他のいくつかの プロジェクトでも) 広く使用

57.

サンプルを作成 アイテムが範囲内にある場合に色が変わる シンプルなキューブ 典型的なゲームシナリオ AI ロジックやトリガープレートなど

58.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
標準的なアプローチ
1 private bool IsItemInRange(float radius)
2 {
3
List<GameObject> allItems = CollectAllItems();
4
var radiusSqrd = radius * radius;
5
6
foreach (var item in allItems)
7
{
8
var dist = (item.transform.position - transform.position).sqrMagnitude;
9
if (dist <= radiusSqrd)
10
{
11
return true;
12
}
13
}
14
15
return false;
16 }
17
18
19
20
21
22
23
24
25

59.

プロファイルしましょう。20 個のキューブ + 1,000 アイテム メインスレッド ジョブ (0.020 ms) 総コスト: 5.72 ms 個々のキューブ: システム (0.010.24 ms)ms ワーカースレッド

60.

同じシナリオに KDTree を使いましょう 個々のキューブ:0.009 ms 総コスト:0.08 ms

61.

何をどのように? KDTree はフレームの初めに構築される 空間的に整理された木 検索では早期に枝を排除する クエリはバーストされる

62.

発表タイトル セクションタイ トル コードサンプル 木を構築 1 [UpdateInGroup(typeof(InitializationSystemGroup))] 2 public partial class KDTreeSystem :SystemBase 3 { 4 private KDTree m_KDTree; 5 6 protected override void OnCreate() 7 { 8 m_KDTree = new KDTree(1000, Allocator.Persistent); 9 } 10 11 protected override void OnDestroy() 12 { 13 m_KDTree.Dispose(); 14 } 15 16 17 18 19 20 21 22 23 24 25

63.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
木を構築
1 ...
2 public partial class KDTreeSystem :SystemBase
3 {
4
private KDTree m_KDTree;
5
private NativeArray<float3> m_Points;
6
private JobHandle m_BuildKDTreeHandle;
7
8
protected override void OnUpdate()
9
{
10
// copy all the transforms into m_Points
11
// we can use a job!
12
...
13
14
m_BuildKDTreeHandle = m_KDTree.BuildTree(m_Points, m_CopyPositionsJobHandle);
15
16
17
18
19
20
21
22
23
24
25

64.

1,000 アイテムがワーカースレッドで 0.11 ms でビルド 位置のコピー: 0.05 ms ビルド: 0.06 ms

65.
[beta]
発表タイトル

セクションタイ
トル

コードサンプル
KDTree でのクエリ
1 private bool IsItemInRange(float radius)
2 {
3
var tree = KDTreeBuildSystem.Instance.KDTree;
4
var results = new NativeArray<KDTree.Neighbour>(10, Allocator.Temp);
5
6
int numFound = tree.GetEntriesInRange(transform.position, radius, ref results);
7
8
return numFound > 0;
9 }
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

66.

ご自由にお使いください! GitHub の MegaCity 2019 サンプルの一部 として公開されています 内部バージョンには完全なユニットテスト スイートとサンプルコードがあります

67.

簡単な要約

68.

System を使って Update 関数を呼び出す まとめ Transform アクセスはメインスレッドから 移すことができる 遅い管理機能をバースト化したジョブで置 き換えることを恐れない KDTree は空間クエリの最適化に役立つ

69.

Thank you