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

 
posted @ 2019-12-11 00:05  bylle  阅读(6800)  评论(0编辑  收藏  举报