前言
? ? ? ? 之前使用過ECS面向組件開發,一直想試一下Unity的ECS DOTS技術,但是苦于入門門檻太高,下載官方的Demo,發現代碼哪哪兒都看不懂,一大堆API聞所未聞,而且沒有一個入門的流程,導致無法進行下去。也嘗試過幾次讓AI引導,創建一個小的Demo,但是由于Entities和Graphics各個版本API差距很大,單純使用AI,也難以實現。今天突然奇想,結合官方Demo和AI,再次嘗試了,成功的實現了一個小Demo。
? ? ? ? 本文不做原理講解,因為還沒琢磨透每行代碼,只是做一個流程示范,可以讓每個入門的讀者按照流程體驗一下萬人同屏的情況下,FPS穩定在60+的“爽感”,Demo實現之后,再仔細閱讀代碼,慢慢拓展功能。
? ? ? ? 本文以下內容,均是個人理解+AI解釋,并不能保證完全正確,如有問題,歡迎指出
版本
? ? ? ? Unity版本6000.0.40(可改,但是一定要是6000,官方Demo用的例子是6000.0.23),Entities版本1.3.5(不要改,避免API失效,如果Unity版本太低導致不支持,建議升級Unity版本),Entities Graphics版本1.4.2(不要改,原因同上)。URP版本17.0.4(這個理論上無所謂)。
步驟
1.創建工程
????????選擇URP項目(必須,只支持URP,如果會在項目中切換渲染管線則隨意)
2.PackageManager導入插件
? ? ? ? 分別導入以上插件Entities1.3.5,Entities Graphics1.4.2,PackageManager里面版本太高的話,在Version History里面切換
3.新建cube生成器
using Unity.Entities;
using UnityEngine;public class RandomMoveAuthoring : MonoBehaviour
{public GameObject prefab;public int count = 50000;public float radius = 100f;class Baker : Baker<RandomMoveAuthoring>{public override void Bake(RandomMoveAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);AddComponent(entity, new RandomMoveSpawner{Prefab = GetEntity(authoring.prefab, TransformUsageFlags.Renderable),Count = authoring.count,Radius = authoring.radius});}}
}public struct RandomMoveSpawner : IComponentData
{public Entity Prefab;public int Count;public float Radius;
}
? ? ? ? 這個腳本的作用主要是把Mono的GameObject(prefab)對象,烘焙成ECS系統里面的Entity實例,把Mono腳本上面的值(count,radius)烘焙到ECS里的IComponentData上。這個Bake函數是在編輯器模式下執行的,不是在運行時執行的。
4.新建cube移動組件數據
using Unity.Entities;
using Unity.Mathematics;public struct RandomMoveComponent : IComponentData
{public float3 StartPosition;public float3 Direction;public float Speed;public float Radius;
}
? ? ? ? 記錄cube的各個屬性,起始位置,方向,速度,移動半徑
5.新建cube生成系統
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;[BurstCompile]
[UpdateInGroup(typeof(InitializationSystemGroup))]
public partial struct RandomMoveSpawnerSystem : ISystem
{public void OnCreate(ref SystemState state){}public void OnUpdate(ref SystemState state){var ecb = new EntityCommandBuffer(Allocator.Temp);foreach (var (spawner, entity) in SystemAPI.Query<RefRO<RandomMoveSpawner>>().WithEntityAccess()){var random = new Unity.Mathematics.Random((uint)UnityEngine.Random.Range(1, int.MaxValue));// 創建 NativeArray 存儲實例var instances = new NativeArray<Entity>(spawner.ValueRO.Count, Allocator.Temp);// 實例化 prefab ,這里先用 EntityManager,因為 ECB 不能實例化實體,之后可以改成別的方案state.EntityManager.Instantiate(spawner.ValueRO.Prefab, instances);for (int i = 0; i < instances.Length; i++){float3 start = new float3(random.NextFloat(-spawner.ValueRO.Radius, spawner.ValueRO.Radius),0,random.NextFloat(-spawner.ValueRO.Radius, spawner.ValueRO.Radius));float3 dir = math.normalize(random.NextFloat3Direction());dir.y = 0;// 通過ECB設置組件數據(延遲生效)ecb.SetComponent(instances[i], LocalTransform.FromPosition(start));ecb.AddComponent(instances[i], new RandomMoveComponent{StartPosition = start,Direction = dir,Speed = UnityEngine.Random.Range(1f, 3f),Radius = 10f});}instances.Dispose();// 延遲刪除 spawner 實體ecb.DestroyEntity(entity);}ecb.Playback(state.EntityManager);ecb.Dispose();}
}
? ? ? ? 根據RandomMoveSpawner的Count數量,生成對應的實例。可以看到,很多新的API,還沒理清,就不解釋誤導人了。
6.新建cube移動系統
原版代碼
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;[BurstCompile]
public partial struct RandomMoveSystem : ISystem
{public void OnUpdate(ref SystemState state){float time = (float)SystemAPI.Time.ElapsedTime;foreach (var (move, transform) in SystemAPI.Query<RefRO<RandomMoveComponent>, RefRW<LocalTransform>>()){float3 pos = move.ValueRO.StartPosition +move.ValueRO.Direction *math.sin(time * move.ValueRO.Speed) *move.ValueRO.Radius;transform.ValueRW.Position = pos;}}
}
? ? ? ? 這個代碼運行之后,fps只有30,明顯不正常,讓AI又重新生成了一版
新版代碼
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;[BurstCompile]
public partial struct RandomMoveSystem : ISystem
{[BurstCompile]public partial struct RandomMoveJob : IJobEntity{public float time;public void Execute(in RandomMoveComponent move, ref LocalTransform transform){float3 pos = move.StartPosition +move.Direction *math.sin(time * move.Speed) *move.Radius;transform.Position = pos;}}public void OnUpdate(ref SystemState state){var job = new RandomMoveJob{time = (float)SystemAPI.Time.ElapsedTime};job.ScheduleParallel(); // 并行調度}
}
????????使用 IJobEntity + Burst 多線程并行,FPS達到了90左右,完美!
7.代碼結束,接下來是編輯器的操作
8.新建cube預制體
? ? ? ? 新建一個cube,拖成預制體。新建一個材質,shader選擇URP默認的Lit即可。勾選材質上的Enable GPU Instancing(這個之前沒勾選的時候,渲染不出來,但是后面又測試的時候,發現不勾選也可以正常渲染,而且可以正常合批,沒搞清楚什么情況)
9.烘焙!!!重要
? ? ? ? 場景內新建對象,命名Spawner(隨意命名)
????????Spawner掛載腳本RandomMoveAuthoring
????????把剛才新建的cube預制體拖到RandomMoveAuthoring的Prefab欄
? ? ? ? 選中Spawner,右鍵,點擊New Sub Scene->From Selection,選擇路徑保存場景,此時會自動觸發RandomMoveAuthoring腳本內的Bake函數,烘焙Mono的數據到ECS系統內,不進行烘焙,ECS是無法獲取到Mono內定義的數據的
10.運行
? ? ? ? 查看效果
問題記錄
1.運行時看不到物體
a.檢查渲染管線是否使用的是URP
b.烘焙是否正常,可以在Bake函數加日志,在點擊子場景Inspector視圖的Open按鈕時,會觸發Bake函數
c.確定編譯過程中,沒有報錯,有些報錯,不影響程序啟動,但是會影響程序邏輯
2.FPS比預期的低
查看下Profiler,耗時主要在哪里,如果是EditorLoop,則關閉Scene視圖
3.Scene視圖不顯示物體
關閉Sub Scene,取消圖中的 復選框
4.Hierarchy視圖,勿刪了Sub Scene怎么辦
在Hierarchy視圖,新建空對象,添加腳本Sub Scene,把之前創建的場景,拖到Sub Scene腳本的Scene Asset欄即可,重置下對象的Transform數據清除警告
總結
? ? ? ? ECS DOTS的入門還是太難了,充斥著大量的新的API,寫邏輯還要脫離Mono,跟之前的開發習慣差距巨大,需要慢慢深入,本文內容可能有錯的地方,后續如果有理解,再做更新