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