Unity DOTS を使う
始めに
先日の UI Toolkit に続いて DOTS(Data-Oriented Technology Stack) も今後標準になっていくとの事。 であればどうやって使うのかを理解しておきたいという話。
- Unity 2022.3.13f1
- com.unity.entities 1.0.16
- com.unity.brust 1.8.2
参考
公式 PR https://unity.com/ja/dots
公式 ECS マニュアル https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/index.html
公式 Brust マニュアル https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/index.html
公式 Job System マニュアル https://docs.unity3d.com/2022.3/Documentation/Manual/JobSystem.html
公式 Learning DOTS https://github.com/Unity-Technologies/EntityComponentSystemSamples/#learning-dots
基礎知識
- DOTS は3つの技術の集合を指す言葉。それぞれを個別に使う事は出来るが、連携前提でもあるようだ。
- 2D サポートは現状無い。
- 物理演算については今ある Physics 2D システムで十分という話があった。
- スプライト描画についてはライブラリを公開している人がいるが、PC以外のプラットフォーム検証が十分でないようだ。
2D Support in DOTs? https://discussions.unity.com/t/2d-support-in-dots/900317
プロジェクトセットアップ
New Entities project setup https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/EntitiesSamples/Docs/project_setup.md
- HDRP か URP を入れる、とある。
- HDRP はモバイル対応していないとの事。モバイル向けしか作らない予定でもあるので URP を選択。
HDRP for mobile https://discussions.unity.com/t/hdrp-for-mobile/918839
- 新規プロジェクト作成
- テンプレートは「3D Mobile」を選択。始めなので「3D(URP)」でも良いが、今後を踏まえて敢えて。
- URP を後付け追加
Installing the Universal Render Pipeline into an existing Project https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.0/manual/InstallURPIntoAProject.html
- パッケージマネージャでの項目名は「Universal RP」
- URP アセットの追加先として Settings というフォルダを作って入れた。
- Editor実行時に警告が出たので設定変更
- 追加したアセットの内 「Universal Render Pipeline Asset_Renderer」の方を選択
- インスペクターから「レンダリング - レンダリングパス」を「Forward+」に変更
- パッケージマネージャから以下を入れる。
- Entities
- Entities Graphics
- Unity Physics
- 設定変更
- Editor メニュー - 環境設定。Entities を選んで、Baking - Scene View Mode を Runtime Data にする。
- Editor メニュー - プロジェクト設定。エディター - 再生モードの開始時設定 で「再生モードの開始時オプション」にチェックを入れて「ドメインを再ロード」「シーンを再ロード」のチェックはOFFであることを確認。
お試し実装
- サンプル HelloCube からコードをコピーするなどして進める。
- サブシーンを追加
- ヒエラルキー内で右クリック - New Sub Scene - Empty Scene
- ECS関連オブジェクトが置かれる。
- サブシーンを選択して、メニュー - ゲームオブジェクト - 空のオブジェクト。
- そのオブジェクトにスクリプト追加。名前を MainExecuteScript とした。
- Visual Studio を開いて MainExecuteScript を以下のようにした。
using UnityEngine;
using Unity.Entities;
public class MainExecuteScript : MonoBehaviour
{
class Baker : Baker<MainExecuteScript>
{
public override void Bake(MainExecuteScript mainExecuteScript)
{
var entity = GetEntity(TransformUsageFlags.None);
AddComponent<ComponentData>(entity);
}
}
public struct ComponentData : IComponentData
{
}
}
- Editor に戻って、サブシーンの下にキューブを置いた。
- キューブにスクリプトを追加。名前を CubeScript とした。
- Visual Studio で CubeScript の中身を以下のようにした。
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
public class CubeScript : MonoBehaviour
{
public float DegreesPerSecond = 360.0f;
class Baker : Baker<CubeScript>
{
public override void Bake(CubeScript cubeScript)
{
var entity = GetEntity(TransformUsageFlags.Dynamic | TransformUsageFlags.NonUniformScale);
AddComponent(entity, new ComponetData
{
RadiansPerSecond = math.radians(cubeScript.DegreesPerSecond)
});
}
}
public struct ComponetData : IComponentData
{
public float RadiansPerSecond;
}
}
- Visual Studio でコードファイルを追加。名前を MainSystem とした。
- 中身を以下のようにした。
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;
namespace Assets.Scenes
{
public partial struct MainSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<MainExecuteScript.ComponentData>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<CubeScript.ComponetData>>())
{
transform.ValueRW = transform.ValueRO.RotateY(
speed.ValueRO.RadiansPerSecond * deltaTime);
}
}
}
}
- Editor で実行。キューブが回る事を確認した。
メモ
MainExecuteScript と CubeScript は構造だけ見ると似ている。分かりづらいがMainExecuteScript は一つだけ作られ、後はオブジェクト毎に CubeScript と MainSystem が作られるので、数が増えてくれば分かってくるはず。ISystem だけ Visual Studio 上で作る。何かに割り当てたのでは無いのが分かりづらいが、そういうものとしておく。
(2023/11/18 更新)
-
MainExecuteScript と CubeScript は構造が似ている。
-
ISystem だけ Visual Studio 上で作る事になる。分かりづらいがそういうものとしておく。
-
値の更新処理は ISystem。これが Brust によって最適化される。
- 更新対象は LocalTransform と CubeScript.ComponetData の両方を持っているオブジェクトとという実装
- CubeScript.ComponetData の登録は、CubeScript を登録したオブジェクトに対して Baker_CubeScript_ で行われる。
-
ISystem.OnCreate で RequireForUpdate を使い MainExecuteScript.ComponentData に紐づけているが、コメントアウトしても動く。何のためにこれをするのか。
- 無い場合は全てのシーンで動作する ISystem になる。
- 追加すると、そのコンポーネントが存在するシーンでのみ動作する ISystem になる。
-
つまりは検証目的としては MainExecuteScript は不要だった。
- サンプルコードの元の実装を見返してみると、更新対象のON/OFFフラグとしての使用が目的だった。
Method ShouldRunSystem https://docs.unity3d.com/Packages/com.unity.entities@1.0/api/Unity.Entities.ComponentSystemBase.ShouldRunSystem.html
- AddComponent で GetEntity(TransformUsageFlags) の戻り値を与えているが、これは何か。
- Transform から更新予定の無い項目を削る事で軽量化するために指定する。
TransformUsageFlags https://docs.unity3d.com/Packages/com.unity.entities@1.0/api/Unity.Entities.TransformUsageFlags.html
最後に感想など
ECS と Brust は触れたが、Job System やグラフィック関連は今後としておく。
2D サポートがないと知った際は正直言って萎えたが、 内容を見る限り部分的には使えそう。
後は、パフォーマンスは確かに大事だが、仕組みが過剰ではないかと思った。 ゲームを設計する時点で多数のオブジェクトがあるというなら良いが、 パフォーマンスが悪いからと後から導入するのは困難。 逆に導入したはいいがオブジェクトが減って苦労が無駄になるかも。
冒頭にも書いたが標準になっていくらしい。 しかしスクリプトを沢山作る必要がある点で、このままだと無理だと思った。