DOTS how to work

本文最后编辑于2019.12.31, DOTS随时可能修改接口,修改结构。

创建一个entity

如何把一个场景里的GO变成不存在场景里的Entity呢。一个典型的物体,有Transform, MeshFilter, MeshRenderer。给它挂上ConvertToEntity,点播放就会自动把它变成Entity了。 其中Unity自动做了几件事: 根据Transform来给这个新创建的Entity添加了Translation, Rotation。根据MeshFilter, MeshRenderer来给Entity添加渲染相关的Components。比如渲染转换这一步,是通过MeshRendererConversion : GameObjectConversionSystem来完成的。详细代码项目里直接看。 而我们自定义了一些的IComponentData,要怎么样在Editor里编辑好,然后让ConvertToEntity把它改成内部的数据呢?

方法1:定义IComponentData, 加上attribute [GenerateAuthoringComponent]. 在Unity约定俗成中,对于某个componentData, 它对应转化MonoBehavior会取名Authoring,就是作者的意思。而这个attribute会自动创建authoring代码。

[GenerateAuthoringComponent]
public struct RotationSpeed_ForEach : IComponentData
{
    public float RadiansPerSecond;
}

方法2:手动定义Authoring脚本。他需要定义和组件同样的public数据,RequiresEntityConversion必有,AddComponentMenu可有可无,ConverterVersion应该是可有可无的。 IConvertGameObjectToEntity接口是必须的,实现这个接口来convert我们的数据。

[Serializable]
public struct RotationSpeed_IJobChunk : IComponentData
{
    public float RadiansPerSecond;
}

[RequiresEntityConversion]
[AddComponentMenu("DOTS Samples/IJobChunk/Rotation Speed")]
[ConverterVersion("joe", 1)]
public class RotationSpeedAuthoring_IJobChunk : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond = 360.0F;

    // The MonoBehaviour data is converted to ComponentData on the entity.
    // We are specifically transforming from a good editor representation of the data (Represented in degrees)
    // To a good runtime representation (Represented in radians)
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationSpeed_IJobChunk { RadiansPerSecond = math.radians(DegreesPerSecond) };
        dstManager.AddComponentData(entity, data);
    }
}

最后要构建一个System来使用数据。System里创建Job有两种方式。第一种是用lambda表达式,第二种是定义一个Job struct。两种方式都可以利用BurstComplier来提高性能。

方法1:定义JobComponentSystem, 在OnUpdate时返回JobHandle。这里没有用Burst,使用方法是在foreach之前.WithBurst(FloatMode.Default, FloatPrecision.Standard, true)

ref in out是比较重要的,对于编译器来说,它可以决定job的依赖和是否并行,串行。

public class RotationSpeedSystem_ForEach : JobComponentSystem
{
    // OnUpdate runs on the main thread.
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        float deltaTime = Time.DeltaTime;

        // Schedule job to rotate around up vector
        var jobHandle = Entities
            .WithName("RotationSpeedSystem_ForEach")
            .ForEach((ref Rotation rotation, in RotationSpeed_ForEach rotationSpeed) =>
             {
                 rotation.Value = math.mul(
                     math.normalize(rotation.Value), 
                     quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
             })
            .Schedule(inputDependencies);

        // Return job handle as the dependency for this system
        return jobHandle;
    }
}

方法2: 定义IJobChunk。这里是把所有数据chunk一起处理了,和IJobForEach相对。具体区别。 EntityQuery是查询定义。

public class RotationSpeedSystem_IJobChunk : JobComponentSystem
{
    EntityQuery m_Group;

    protected override void OnCreate()
    {
        // Cached access to a set of ComponentData based on a specific query
        m_Group = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<RotationSpeed_IJobChunk>());
    }

    // Use the [BurstCompile] attribute to compile a job with Burst. You may see significant speed ups, so try it!
    [BurstCompile]
    struct RotationSpeedJob : IJobChunk
    {
        public float DeltaTime;
        public ArchetypeChunkComponentType<Rotation> RotationType;
        [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed_IJobChunk> RotationSpeedType;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var chunkRotations = chunk.GetNativeArray(RotationType);
            var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
            for (var i = 0; i < chunk.Count; i++)
            {
                var rotation = chunkRotations[i];
                var rotationSpeed = chunkRotationSpeeds[i];

                // Rotate something about its up vector at the speed given by RotationSpeed_IJobChunk.
                chunkRotations[i] = new Rotation
                {
                    Value = math.mul(math.normalize(rotation.Value),
                        quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
                };
            }
        }
    }

    // OnUpdate runs on the main thread.
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        // Explicitly declare:
        // - Read-Write access to Rotation
        // - Read-Only access to RotationSpeed_IJobChunk
        var rotationType = GetArchetypeChunkComponentType<Rotation>();
        var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed_IJobChunk>(true);

        var job = new RotationSpeedJob()
        {
            RotationType = rotationType,
            RotationSpeedType = rotationSpeedType,
            DeltaTime = Time.DeltaTime
        };

        return job.Schedule(m_Group, inputDependencies);
    }
}

IDeclareReferencedPrefabs

如果需要传入一个prefab给System用,Authoring需要实现这个接口。

public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
    referencedPrefabs.Add(Prefab);
}

//Convert中
Prefab = conversionSystem.GetPrimaryEntity(Prefab)

长度不定的数组

对于数组,DOTS需要声明一个struct继承IBufferElementData,然后当作一个单独的Component,贴到Entity上。注意,这不是类型定义,而是Component定义。组合起来成为Archetype。理解这件事需要自己写一下代码。

public struct float3BufferElement : IBufferElementData
{
    public float3 Pos;

    public static implicit operator float3(float3BufferElement e)
    {
        return e.Pos;
    }

    public static implicit operator float3BufferElement(float3 e)
    {
        return new float3BufferElement { Pos = e };
    }

    public static implicit operator float3BufferElement(Vector3 e)
    {
        return new float3BufferElement { Pos = e };
    }
}

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var buffer = dstManager.AddBuffer<float3BufferElement>(entity);
        for (int i = 0; i < Points.Length; i++)
        {
            buffer.Add(Points[i]);
        }
    }

ForEach((Entity entity, int entityInQueryIndex, in ListSpawner spawner, in DynamicBuffer<float3BufferElement> buffer)=>{
    ...
})

长度一定,不会改变的数组或数据 Blob Data

blob asset是不变的数据,存储在unmaganaged memory里。它可以包含原始类型,字符串,结构体,数组,数组的数组。数组和结构体只能包含blittable types.字符串是BlobString。一旦创建,就不能更改。你可以新建一个,然后引用新的。 这个功能比较适合保存一些配置信息,在authoring时创建Component,将引用存在Component里。多个Component可以引用同一个blob asset。


public struct Blobfloat3
{
    public BlobArray<float3> Positions;
}

public struct ListSpawner : IComponentData
{
    public BlobAssetReference<Blobfloat3> Pos;
    public Entity Prefab;
}

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var builder = new BlobBuilder(Unity.Collections.Allocator.Temp);
        ref var root = ref builder.ConstructRoot<Blobfloat3>();

        var float3Array = builder.Allocate(ref root.Positions, Points.Length);
        for (int i = 0; i < Points.Length; i++)
        {
            float3Array[i] = Points[i];
        }
        var blobAsset = builder.CreateBlobAssetReference<Blobfloat3>(Unity.Collections.Allocator.Persistent);

        builder.Dispose();

        var data = new ListSpawner
        {
            Prefab = conversionSystem.GetPrimaryEntity(Prefab),
            Pos = blobAsset
        };

        dstManager.AddComponentData(entity, data);
    }

ForEach((Entity entity, int entityInQueryIndex, in ListSpawner spawner)=>{
    ...
})
Written on December 5, 2019