学习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数据能够被安全的访问。