たのしいDOTS
〜初級から上級まで〜
ユニティ・テクノロジーズ・ジャパン
安原 祐二
DOTSとは
Data Oriented Technology Stack
Unityの新しい基盤
DOTSとは
Data Oriented Technology Stack
Unityの新しい基盤
なぜ今なのか? データ指向って?
DOTSとは
Data Oriented Technology Stack
Unityの新しい基盤
現在はプレビュー版 これから使いやすくなる
なぜ今なのか? データ指向って?
DOTSとは
Data Oriented Technology Stack
Unityの新しい基盤
現在はプレビュー版
いますぐ始めなくていい
これから使いやすくなる
なぜ今なのか? データ指向って?
あとで見返そう
DOTSとは
Data Oriented Technology Stack
Unityの新しい基盤
現在はプレビュー版
いますぐ始めなくていい
これから使いやすくなる
この技術は
おもしろい!
なぜ今なのか? データ指向って?
あとで見返そう
前半: メモリの戦い
後半: 驚異のUnity Physics
メモリの戦い
〜その歴史〜
限られた土地を使いまわそう
役所
役所は確保状況を把握 ただし中身は関知しない
確保(allocation)
確保(allocation)役所
解放(free)
解放を報告
役所
再利用
役所
メモリも ストレージも
事情は同じ
メモリも ストレージも
事情は同じ
メモリは格段に複雑な事情を抱えている
利用者にアドレスを渡してしまっているので・・・
アドレスが埋まっている
問題は解放
解放!
!?
歴史上、さまざまな取り組みが
事故発生
解放!
C#「GCにお任せする」
解放!
C#「GCにお任せする」
マネージドメモリ
(managed memory)
解放を呼ばせない
ポインタとはこれ  のこと
マネージドメモリ
(managed memory)
ポインタとはこれ  のこと
int
マネージドメモリ
(managed memory)
int a;*
マネージドメモリ
(managed memory)
ゲームでこんな複雑な必要あるだろうか?
Unmanaged Memoryが要求される
マネージドメモリ
(managed memory)
Unmanaged
Memory
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
ネイティブコンテナのトリック
〜Unmanagedに挑戦〜
〜ネイティブコンテナ(NativeContainer)とは〜
Unmanaged
Memory
Chunk NativeContainer Unmanagedメモリを保持する機構
ただし自分で解放する必要
〜ネイティブコンテナ(NativeContainer)とは〜
Unmanaged
Memory
Chunk NativeContainer
ただし自分で解放する必要
オブジェクトではなく、アルゴリズムで確保する
(生存期間をアルゴリズムで決める)
秩序は守られる!
Unmanagedメモリを保持する機構
代表例:NativeArray
NativeArray
structの配列
必ずstruct(値型)!
class(参照型) はC#の機構により
マネージドメモリになってしまう
代表例:NativeArray
NativeArray
structの配列
必ずstruct(値型)!
class(参照型) はC#の機構により
マネージドメモリになってしまう
var a = NativeArray<float>(8, Allocator.Persistent)
C#の特徴を利用
structならメモリがひとかたまりに!
ネイティブコンテナもstruct
リファレンスマニュアルより
ところで struct って恐くないですか?
structの罠
aはコピーされてfuncに渡される
var a = new Vector3(1, 2, 3);
func(a);
funcの中で変更してもaに影響なし
structの罠
aはコピーされてfuncに渡される
var a = new Vector3(1, 2, 3);
func(a);
var a = new NativeArray<float>(32);
func(a);
aはコピーされてfuncに渡される あれ?
これは大丈夫?
funcの中で変更してもaに影響なし
ネイティブコンテナの実装は必ずポインタ!
unsafe struct NativeArray<T> {
void ptr;
}
ポインタをコピーしても参照先は同じ
Unmanaged
Memory
var a = new NativeArray<float>(32);
func(a);
funcの変更が伝わる!
*
ベテランでも引っかかるstructの罠
struct MyNativeArray<T> {
NativeArray<T> na;
int Length;
}
よーし
ネイティブコンテナ
自作しちゃうぞー
ベテランでも引っかかるstructの罠
struct MyNativeArray<T> {
NativeArray<T> na;
int Length;
}
これは大失敗!
よーし
ネイティブコンテナ
自作しちゃうぞー
なぜでしょう?
struct MyNativeArray<T> {
NativeArray<T> na;
int Length;
}
Unmanaged
Memory
Length
Length
var a = new MyNativeArray<float>(32);
func(a);
struct MyNativeArray<T> {
NativeArray<T> na;
int Length;
}
Unmanaged
Memory
Length
Length
Lengthが
共有されてない!!
var a = new MyNativeArray<float>(32);
func(a);
コンパイルエラーも出ないし
プログラムは正しいけれど
100%バグになる
struct MyNativeArray<T> {
NativeArray<T> na;
int Length;
}
Unmanaged
Memory
Lengthも
ポインタの先に置く
Length
ネイティブコンテナ
の中は必ずポインタ!
unsafe struct MyNativeArray<T> {
void ptr;
}
*
最重要!チャンクの設計
〜ゲームのためのメモリ〜
ゲームのオブジェクトを     で扱いたいUnmanaged
Memory
マネージドメモリ
(managed memory)
Unmanaged
Memory
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
現代はメモリが広い!
マネージドメモリ
(managed memory)
Unmanaged
Memory
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
Unityの登場時(2005)とは事情が異なる
現代はメモリが広い!
メモリの再利用をギリギリまで追求する必要はない
マネージドメモリ
(managed memory)
Unmanaged
Memory
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
Unityの登場時(2005)とは事情が異なる
増えたメモリは主にアセットに利用される
ゲームロジックのメモリは大きく変わっていない
もし、確保するサイズが一律で同じだったら
確保・解放にまつわる
多くの問題が解決
ところで・・・
ある物体は3つのコンポーネントで構成されているとする
CBA ABCはサイズが異なる
ある物体は3つのコンポーネントで構成されているとする
CBA ABCはサイズが異なる
この物体専用の領域を用意できれば・・・
ある物体は3つのコンポーネントで構成されているとする
CBA
CBA
ある程度のメモリの無駄は許容する
ABCはサイズが異なる
確保単位が一定に!
この物体専用の領域を用意できれば・・・
A
必ず16KiB
CB
これがチャンクの姿
struct Entity
CBA
コンポーネント種別に並べる
別の物体は4つのコンポーネントで構成
CB
チャンクサイズ16KiBは変わらない
A
割り振りがさっきと異なる
このチャンクには3つ格納できる
D
格納できる数が変わる
CB アーキタイプABCA
組み合わせをアーキタイプ(Archetype)と呼ぶ
CBA D アーキタイプABCD
アーキタイプABC用チャンク
アーキタイプABCD用チャンク
生成
A CB
このチャンクはABCD用なので無関係!
CBA
専用チャンクに格納(満杯ならもう一本追加)
あるオブジェクトを削除する場合
たったの2ステップ
削除 削除 削除 削除
チャンクに格納されているオブジェクト数を減らす
3→2
ステップ1
最後の要素を消したい場所に上書きコピー
削除完了!
ステップ2
最後の要素を消したい場所に上書きコピー
削除完了!
ステップ2
順序は保たれていない
決して歯抜けにならない
他のオブジェクトに直接参照させない
ねえねえ
マネージャ
通して
他のオブジェクトに直接参照させない
マネージド Unmanaged
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
ねえねえ
マネージャ
通して
Systemの概念
〜データ指向とは〜
データは用意できた
System
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
どうやって処理させよう?
“System”を記述する
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
Chunk
System
• チャンクを選ぶ
• 入力と出力を決める
• 処理する
“System”の3つの働き
使用コンポーネントを決める
アーキタイプABC用チャンク
アーキタイプABCD用チャンク
アーキタイプBD用チャンク
条件:BDをもつチャンク
query = GetEntityQuery(new EntityQueryDesc() {
All = new ComponentType[] {
ComponentType.ReadOnly<B>(),
ComponentType.ReadWrite<D>(),
}, });
使用コンポーネントを決める
アーキタイプABC用チャンク
アーキタイプABCD用チャンク
アーキタイプBD用チャンク
条件:BDをもつチャンク
query = GetEntityQuery(new EntityQueryDesc() {
All = new ComponentType[] {
ComponentType.ReadOnly<B>(),
ComponentType.ReadWrite<D>(),
}, });
使用コンポーネントを決める
アーキタイプABC用チャンク
アーキタイプABCD用チャンク
アーキタイプBD用チャンク
条件:BDをもつチャンク
query = GetEntityQuery(new EntityQueryDesc() {
All = new ComponentType[] {
ComponentType.ReadOnly<B>(),
ComponentType.ReadWrite<D>(),
}, });
アーキタイプBD用チャンク
処理する
System
Bと Dを
処理する
System
Bと Dを
処理する
バラバラに処理しているように見えるが
System
きっちりオブジェクト単位で渡っている
Jobを発行
メモリの連続が考慮されている
Job
Job
Job
System
入出力を明示
Job
Job
Job
B:入力(書き込まない)
D:出力(書き込む)
System
DSystem
データの流れ
B
DSystem
データの流れ
System
B
A
Native
Container
ネイティブコンテナは
アルゴリズムで使う
DSystem
データの流れ
System
B
A
System Rotation
Native
Container
ネイティブコンテナは
アルゴリズムで使う
データに注目
Update
{
if (position.y > 0f)
{
rotation = objA.func(1f);
return;
}
else if (position.y > 4f)
{
if (position.z > 0f)
{
m_C = objB.func(2f);
}
else
m_C = objB.func(-1f);
}
rotation = m_C;
}
D
System
System
B
A
System
Rotation
いつもの書き方
Native
Container
Update
{
if (position.y > 0f)
{
rotation = objA.func(1f);
return;
}
else if (position.y > 4f)
{
if (position.z > 0f)
{
m_C = objB.func(2f);
}
else
m_C = objB.func(-1f);
}
rotation = m_C;
}
D
System
System
B
A
System
Rotation
いつもの書き方
Native
Container
テストが
容易!
Update
{
if (position.y > 0f)
{
rotation = objA.func(1f);
return;
}
else if (position.y > 4f)
{
if (position.z > 0f)
{
m_C = objB.func(2f);
}
else
m_C = objB.func(-1f);
}
rotation = m_C;
}
D
System
System
B
A
System
Rotation
いつもの書き方
Native
Container
テストが
容易!
データに注目
Update
{
if (position.y > 0f)
{
rotation = objA.func(1f);
return;
}
else if (position.y > 4f)
{
if (position.z > 0f)
{
m_C = objB.func(2f);
}
else
m_C = objB.func(-1f);
}
rotation = m_C;
}
D
System
System
B
A
System
Rotation
いつもの書き方
Native
Container
参照はエンティティ
〜ポインタの代わりに〜
マネージドメモリ
(managed memory)
参照の問題をいかにして解決するか
オブジェクトには必ずEntityがある
チャンクにも入っている
Entity
Entity
他のオブジェクトはEntityで参照する
でも、Entityはポインタではない
Entityの正体はインデクス(ある配列中の位置)
4 5
5 8
2 5
8
Entityの正体はインデクス(ある配列中の位置)
4 5
5 8
2 5
8
1 2 3 4 5
チャンクポインタ
チャンク内インデクス
バージョン
Entity 5 の情報
EntityManagerが持つテーブル
Entityの正体はインデクス(ある配列中の位置)
4 5
5 8
2 5
8
1 2 3 4 5
チャンクポインタ
チャンク内インデクス
バージョン
Entity 5 の情報
EntityManagerが持つテーブル
→ポインタ同様に高速
チャンクポインタ
チャンク内インデクス
バージョン
4 5
5 8
2 5
8
Entity 5 の情報
削除は?
削除されたインデクスが次の生成で使いまわされたらアウトだよね
1 2 3 4 5
チャンクポインタ
チャンク内インデクス
バージョン+1
4 5
5 8
2 5
8
Entity 5 の情報
削除は?
削除!
1 2 3 4 5
5
1
実はEntityはインデクスとバージョン(計64bit)
インデクス(Index)
バージョン(Version)
4
0
5
1
5
1
8
1
2
4
5
1
8
0
チャンクポインタ
チャンク内インデクス
バージョン+1
Entity 5 の情報
生成時に与えられた
バージョン1が・・・
削除時に増加して2になっているので・・
1 2 3 4 5
チャンクポインタ
チャンク内インデクス
バージョン+1=2
4
1
5
1
5
1
8
1
2
1
5
1
8
1
Entity 5 の情報
あなたの知っている5番はもういません!
バージョン1?
このEntityの
情報ください
1 2 3 4 5
マネージドメモリ
(managed memory)
Unmanaged
Memory
Chunk NativeContainer
Chunk
Chunk
Chunk
NativeContainer
NativeContainer
Unmanagedな世界でも・・・
速度を兼ね備えた見事な管理システムを実現
驚異のUnity Physics
C#で物理シミュレーションを
フル実装しちゃおうぜ
正気かしら
Unity Physics 誕生
・決定的(deterministic)
・シミュレーションループ独立
こういうのを
待っていた!
・ステートレス
通常の物理エンジン
約15fps
Unity Physics
約60fps
キューブ13824個 on high-end PC
ワールドを分離して自在にループ実行
https://github.com/Unity-Technologies/DOTS-Shmup3D-sample
Unity Physicsは素晴らしい!
が、現時点(Ver.0.2.4)では気配りが足りない
けっこう突き放してくる!
これから使いやすくなっていきます
注意点その1
静的なコライダーをスクリプトから動かす場合
コリジョンの更新は
PhysicsVelocity
があることが条件
AddComponentでPhysicsVelocityを付加しよう
外力(AddForce)がない
その代わり力積(ApplyLinearImpulse)がある
= m
dv
dt
dt = mdv
外力にdtを掛けて力積(ApplyLinearImpulse)を呼べばいい
外力 力積
注意点その2
F F
public static void ApplyLinearImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Linear += impulse * massData.InverseMass;
}
実装の確認は避けられない(現時点では)
dv = Fdt
1
m
つまり
public static void ApplyLinearImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Linear += impulse * massData.InverseMass;
}
実装の確認は避けられない(現時点では)
Force velocityData.ApplyLinearImpulse(force*deltaTime);
Impulse velocityData.ApplyLinearImpulse(impulse);
Acceleration
VelocityChange
velocityData.Linear += acceleration*deltaTime;
velocityData.Linear += velocity;
ForceModeの対応:
dv = Fdt
1
m
つまり
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
注意点その3
massData.InverseInertiaは行列ではなくベクトル
AddTorqueは?
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
注意点その3
massData.InverseInertiaは行列ではなくベクトル
直交座標系の回転要素がない慣性テンソル
AddTorqueは?
(対角化された状態)
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
注意点その3
慣性テンソル空間でトルクを与える必要
massData.InverseInertiaは行列ではなくベクトル
AddTorqueは?
(対角化された状態)
直交座標系の回転要素がない慣性テンソル
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
注意点その3
慣性テンソル空間でトルクを与える必要
特殊な形状でなければAddRelativeTorqueと考えて良い
massData.InverseInertiaは行列ではなくベクトル
AddTorqueは?
(対角化された状態)
直交座標系の回転要素がない慣性テンソル
参考:慣性テンソルについての解説動画
https://learning.unity3d.jp/1167/
〜Unity道場〜
物理シミュレーション完全マスター
34:20から
注意点その4
軸単位の拘束(Constraint)の実現方法
拘束しないとこうなるよね
物体の位置・姿勢の更新は
すべてApply*Impulse経由
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
という実装になってるので
物体の位置・姿勢の更新は
すべてApply*Impulse経由
InverseInertia
public static void ApplyAngularImpulse(ref this PhysicsVelocity velocityData, PhysicsMass massData, float3 impulse)
{
velocityData.Angular += impulse * massData.InverseInertia;
}
の設定で回転拘束を実現
例: new PhysicsMass {
…
InverseInertia = new float3(0, 1, 0);
…
}
Y軸以外の角速度を
発生させない
という実装になってるので
慣性テンソルに回転がある場合は注意
まとめ
• ゲーム実装を強く意識
  汎用的だったメモリ管理をゲームの特徴に合わせる
DOTSの必然性
• ゲーム実装を強く意識
  汎用的だったメモリ管理をゲームの特徴に合わせる
DOTSの必然性
• 現代のCPUを強く意識
  64bit CPUを前提にできつつある
• ゲーム実装を強く意識
  汎用的だったメモリ管理をゲームの特徴に合わせる
DOTSの必然性
• 現代のCPUを強く意識
  64bit CPUを前提にできつつある
• テスタビリティ・スケーラビリティを強く意識
  データの一意性+マルチコア実行
• ゲーム実装を強く意識
  汎用的だったメモリ管理をゲームの特徴に合わせる
DOTSの必然性
DOTSはどんどん使いやすくなっていく
そのときに始めれば十分 ご期待ください!
• 現代のCPUを強く意識
  64bit CPUを前提にできつつある
• テスタビリティ・スケーラビリティを強く意識
  データの一意性+マルチコア実行
おしまい

【Unite Tokyo 2019】たのしいDOTS〜初級から上級まで〜