學習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);

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

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。

 

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的刷新了。

 

ForEachWithEntityChanges

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

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數據可以被安全的訪問。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章