學習Unity 2019 ECS 框架(一)

HelloCube

ForEach

ForEach是一個合體的Cubes共同旋轉的簡單場景。數組

 

 RotatingCube掛載了RotationSpeed Convert to Entity,將該GameObject轉換爲Entity,該物體的GameEngine Component是Transform,做爲一個旋轉單位,保存爲實體貌似也是正確的,ECS中C做爲數據集合,做爲rotation component我認爲也是能夠的(更正:RotationSpeedAuthoring沒有處理旋轉,只是單純記了個數據,所以它不是Component)。緩存

public class RotationSpeedSystem_ForEach : ComponentSystem安全

對應ECS中的Systsem,也是Component,Unity ECS居然能夠一個腳本同時做爲System和Component,此到處理的RotationCube的旋轉,是個Component,同時這個場景中只有單個旋轉須要處理,同時做爲System專門處理物體旋轉也行。框架

 

RotationSpeedAuthoring_ForEach繼承了MonoBehaviour,所以須要在場景內的GameObject上掛載,接着它又實現了IConvertGameObjectToEntity接口,裏面只有一個接口方法void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem);dom

這個接口是轉化爲實體,將GameObject的Behaviour轉化爲實體?不該該是Component麼,Convert方法裏new RotationSpeed_ForEach,這應該是個真正的實體了。ide

public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { var data = new RotationSpeed_ForEach { RadiansPerSecond = math.radians(DegreesPerSecond) }; dstManager.AddComponentData(entity, data); }

 EntityManager在Unity.Entities命名空間下,裏面有各類AddComponent/AddComponentData方法,Convert實例化了RotationSpeed_ForEach,關於RotationSpeed_ForEach它屬於Component,實現了IComponentData接口,這個接口很是簡單==過於簡單,啥都沒有隻是說了這個接口的含義,嗯,長成這樣函數

namespace Unity.Entities { public interface IComponentData { } }

這樣看來Unity的ECS框架和普通ECS很不同,從Component能夠直接建立Entity,我認爲繼承自MonoBehavious的類是組件,這個組件實現了ConvertGameObjectToEntity並在Convert方法中將自身做爲數據加入EntityManager。url

 

public class RotationSpeedSystem_ForEach : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) => { var deltaTime = Time.deltaTime; rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime)); }); }

繼承自ComponentSystem集中處理OnUpdate的每幀刷新,Unity說如今這種作法不是最優解,但足以將ComponentSystem Update (logic) and ComponentData (data)解耦合,MonoBehavious已經再也不處理OnUpdate的刷新了。spa

 

ForEachWithEntityChanges

Spawner有個厲害的操做,將Hierarchy的預設轉換爲了Entity,並且EntityManager提供接口entityManager.Instantiate(prefab),直接建立prefab實例,細節不說,直接看代碼。.net

Entity prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, World.Active); var entityManager = World.Active.EntityManager;

 

SpawnFromMonoBehaviour

這個示例我以爲能體現一些ECS的優點,將數據和邏輯行爲解耦,Spawner_FromMonoBehaviour只須要管理行爲,而旋轉數據由自身RotationSpeedAuthoring_IJobForEach,添加進EntityManager.AddComponentData(entity, data);統一處理。

 

 

Advanced/Boids

這個示例是大量計算海洋魚羣路徑,模擬2個魚羣被鯊魚追逐的軌跡運動。

基本思想是生成兩個巨量魚羣,每幀刷新每條魚的移動方位,先計算當前位置到targets[0]點的距離,變例所有的目標點取得最短距離。

NearestPosition方法並無返回值,這是Convert to Entity的好處,數值由Entity統一管理,提交的一方只要確保數據正確。

void NearestPosition(NativeArray<float3> targets, float3 position, out int nearestPositionIndex, out float nearestDistance ) { nearestPositionIndex = 0; nearestDistance = math.lengthsq(position-targets[0]); for (int i = 1; i < targets.Length; i++) { var targetPosition = targets[i]; var distance       = math.lengthsq(position-targetPosition); var nearest        = distance < nearestDistance; nearestDistance = math.select(nearestDistance, distance, nearest); nearestPositionIndex = math.select(nearestPositionIndex, i, nearest); } nearestDistance = math.sqrt(nearestDistance); }
// Resolves the distance of the nearest obstacle and target and stores the cell index. public void ExecuteFirst(int index) { var position = cellSeparation[index] / cellCount[index]; int obstaclePositionIndex; float obstacleDistance; NearestPosition(obstaclePositions, position, out obstaclePositionIndex, out obstacleDistance); cellObstaclePositionIndex[index] = obstaclePositionIndex; cellObstacleDistance[index] = obstacleDistance; int targetPositionIndex; float targetDistance; NearestPosition(targetPositions, position, out targetPositionIndex, out targetDistance); cellTargetPositionIndex[index] = targetPositionIndex; cellIndices[index] = index; }

 

Shark上掛載了GameObjectEntity

 

 

 

GameObjectEntity感受很是方便,在該物體知足enabled && gameObject.activeInHierarchy時,會自動AddToEntityManager(m_EntityManager, gameObject);加入到EntityManager的組件管理中,CreateEntity(entityManager, archetype, components, types);生成一個實體返回。

public static void CopyAllComponentsToEntity(GameObject gameObject, EntityManager entityManager, Entity entity) { foreach (var proxy in gameObject.GetComponents<ComponentDataProxyBase>()) { // TODO: handle shared components and tag components
        var type = proxy.GetComponentType(); entityManager.AddComponent(entity, type); proxy.UpdateComponentData(entityManager, entity); } }

它能夠將這個GameObject上所有的Component轉成ComponentData,並且在OnEnable/OnDisable都有處理。

 

來看下SpawnRandomInSphere,雖然類的命名是在球體半徑中隨機生成魚,但這個類裏面乾的事情全是提交數據,它是和Entity打交道,沒有處理生成魚的行爲。

它繼承自MonoBehaviour,也就是說這個類會直接掛在場景內的GameObject上,後面兩個接口分別是聲明須要建立的prefab和將這個GameObject的數據轉換爲Entity。

public class SpawnRandomInSphere : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity { public GameObject Prefab; public float Radius; public int Count; // Lets you convert the editor data representation to the entity optimal runtime representation
            public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { var spawnerData = new Samples.Boids.SpawnRandomInSphere // 自定義的數據格式,是個繼承ISharedComponentData的結構體 { // The referenced prefab will be converted due to DeclareReferencedPrefabs. // So here we simply map the game object to an entity reference to that prefab.
                    Prefab = conversionSystem.GetPrimaryEntity(Prefab),// 將GameObject Prefab轉變爲一個實體返回 Radius = Radius, Count = Count }; dstManager.AddSharedComponentData(entity, spawnerData); } // Referenced prefabs have to be declared so that the conversion system knows about them ahead of time
            public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs) { referencedPrefabs.Add(Prefab); } }

 

SpawnRandomInSphereSystem並非World,而是ComponentSystem它處理全部提交的ComponentData.

var spawnPositions = new NativeArray<float3>(toSpawnCount, Allocator.TempJob); GeneratePoints.RandomPointsInUnitSphere(spawnPositions);

NativeArray<T>只能容納值對象。

在建立的時候除了指定length外,還須要指定allocator模式:Temp(臨時),TempJob(Job內臨時),Persistent(持久)。

這是Unity官方提供的容器類,它所指定的allocator模式多是相似Temp對應棧內存分配,Persistent對應堆內存分配的方式。
它只是簡單的封裝一下數組,本質和普通的struct數組彷佛沒什麼區別,都能內存連續使cpu更容易命中緩存。

var entities = new NativeArray<Entity>(toSpawnCount, Allocator.Temp); for (int i = 0; i < toSpawnCount; ++i) { entities[i] = PostUpdateCommands.Instantiate(spawner.Prefab); }

生成隨機點是由Unity.Mathematics提供的接口,沒有看到生成隨機點後的返回,這個方法是void.Instantiate是EntityCommandBuffer的建立函數,Unity註釋說This code is placeholder until we add the ability to bulk-instantiate many entities from an ECB,這句生成只是個佔位符,用於理解邏輯,真正的生成在其餘地方。

 

for (int i = 0; i < toSpawnCount; i++) { PostUpdateCommands.SetComponent(entities[i], new LocalToWorld { Value = float4x4.TRS( localToWorld.Position + (spawnPositions[i] * spawner.Radius), quaternion.LookRotationSafe(spawnPositions[i], math.up()), new float3(1.0f, 1.0f, 1.0f)) }); }

隨後計算每一個實體看下目標點的下一個位置,魚的行進路線是看向目標點的,這個計算是當前點的世界座標+生成點(遊戲初始化時)*生成魚羣半徑+轉向目標的轉動偏移。

 

最後計算完了這些數據,SpawnRandomInSphereSystem將這個節點的數據刪除。

 

Boid : MonoBehaviour

本實例中有3個Boid變體,Boid類只是ComponentData,並轉換爲Entity.

 

BoidSystem : JobComponentSystem

能夠直接看BoidSystem,發現行爲代碼在MergeCells中,有3個方法ExecuteFirst、ExecuteNext、NearestPosition。

MergeCells使用的[BurstCompile]編譯器,也許這是我找不到ExecuteFirst、ExecuteNext調用地方的緣由,在ExecuteFirst中分別計算了到障礙物和到目標點的最近點,計算最近點是當前點到全部障礙、目標的窮舉運算。


 

看了一篇文章https://gameinstitute.qq.com/community/detail/126083,上面說Unity的ECS和JobSystem很類似,C# JobSystem 只支持structs和NativeContainers,並不支持託管數據類型。因此,在C# JobSystem中,只有IComponentData數據能夠被安全的訪問。

相關文章
相關標籤/搜索