Unity - DOTS(多线程数据导向型技术堆栈)

简介


DOTS(Data-Oriented Technology Stack),中文名:多线程数据导向型技术堆栈,它主要由三个部分组成。

  • C# Job System:充分利用多核CPU的内核,高效安全的运行多线程代码。
  • ECS(Entity Component System):将数据与行为分离,并让数据紧密的排列在内存中。
  • Burst Complier:编译生成高效的代码。

Job System和ECS是两种不同的概念,均可以分开始使用。当它们一起使用的时候会发挥出更大的优势。而同时使用这三种技术,就是我们的DOTS技术。

C# Job System


Unity内部是多线程处理,但对外是不允许开发人员使用多线程的。因为Unity害怕多线程会被开发人员滥用,而引发许多莫名其妙的BUG。但实际上我们仍然可以使用C#多线程来对数据进行运算,当然这里的多线程中不能包含Unity相关的API。

而C# Job System的出现使得开发人员能够通过它来进行多线程的安全使用。

如今的CPU大多都是多核的。以往的游戏开发中,很多时候只用到了其中一个核心,而其余的核心大多时候都处于空闲状态。这使得CPU的性能并没有被完全激发出来。而C# Job System内部的多线程机制,能够使得这些原本处于空闲状态的CPU内核都运转起来。这就使得游戏运行的效率大为提升。

但在使用C# Job System的时候,我们只能在Job(一个线程)中使用值类型的数据。如果想使用数组(引用类型)的话,必须使用NativeArray来代替,这便引入了一个新的概念HPC#。

HPC#


HPC#(High Performance C#),中文名:高性能C#。

在C#中引用类型的数据是被分配在堆上的,程序员无法手动的去释放它,它的内存释放是由GC(垃圾回收机制)所控制的。又因为GC是每隔一段时间去清理一次,所以这使得在没有到达清理之前,即使我们去主动的把数据设置成null也是没用的。但HPC#提供了一个数据结构NativeArray,使得我们可以主动的去释放内存。

NativeArray类似于数组,它是实际上是一个在C#上分配的C++对象,且没有被分配到堆上,因此它是不归GC管,所以

我们需要通过Dispose()去手动去释放NativeArray。但有个不太好的地方就是,NativeArray里面只能放值类型数据。

如何使用C# Job System?参考代码如下:

public class Sample : MonoBehaviour
{
    struct MyJob : IJob
    {
        public NativeArray m_NativeArray;

        public void Execute()
        {
            for (int i = 0; i < m_NativeArray.Length; i++)
            {
                m_NativeArray[i] = i;
            }
        }
    }

    struct MyJobParallelFor : IJobParallelFor
    {
        public NativeArray m_NativeArray;

        public void Execute(int index)
        {
            m_NativeArray[index] = index;
        }
    }


    private void Update()
    {
        NativeArray nativeArray = new NativeArray(1000, Allocator.Persistent);
        for(int i=0;i

Burst Complier


实际上就是一个编译器。它从抽象出来了一个中间语言IR。在对C#代码进行编译的时候,会先将C#语言转换成IR语言,然后再对IR语言进行优化最终转换成机器码。 (C#==>转换==>IR==>优化==>机器码)

多指针指向同一内存


对于一般的编译器而言,当它在处理两个指针的时候,是不知道两个指针是否指向同一块内存的,这时在处理这两个指针的时候,就会以一种安全但不高效的方式来处理。而对于Burst编译器,它内部是禁止了多个指针指向同一块内存的情况,因此它可以才用更高效的方式来处理。

如何启用BurstComplier?参考代码如下:


//[BurstCompile]只有修饰了Job System的Job相关接口时才有用
[BurstCompile]
struct Job : IJob
{
    //......
}

ECS(Entity Cmponent System)


中文名是:实体组件系统。它实际上是将原本属于游戏对象以及它上面的数据和行为相互分离,分离成Entity、Component和System。

Entity


Entity是就相对于GameObject,但它实际上是一个标识,是一个key。它标识了它的数据到底对应着哪个Archetype。

Archetype是一个容器,一个Archetype中由多个chunk(一个16k内存块)组成。

Archetype的容量是动态变化的,不够就加一个chunk。

Archetype里面存放什么数据类型取决于游戏对象上有哪些Component(ECS),这其实是一种组合。当然也可以通过代码自行组合Component产生新的Archetype。

一个Archetype存放了这种组合下的所有数据。

Component


对于以往的开发方式,一个Component(以往)必须要继承MonoBehaviour才能被挂到游戏对象上。而对于MonoBehaviour它是多次继承的产物,它里面有很多很多的数据。而这里面的大多数数据,我们实际上是根本用不到的,但它却必须存在。这就使得有大量的内存我们白白浪费了。

还有一个问题就是,对于Component(以往)来说。每个Component在内存中是不连续的,它是分散在内存中的。这就使得缓存的命中率非常的低,自然效率就很低。

而ECS下的Component(ECS)里面只包含所需要用到的数据,不需要继承自MonoBehaviour,内存大为节省。同时由Component(ECS)类型组合而成的Archetype,会在内存中高度紧密的排列。这就使得缓存的命中率非常的高,效率大幅提升。

  • Component(ECS)写法如下:
using Unity.Entities;

//继承IComponentData,再被[GenerateAuthoringComponent]修饰
//如果继承的是ISharedComponentData则内存中只有一份实例
[GenerateAuthoringComponent]
public struct MoveSpeed : IComponentData
{
    public float Value;
}

System


在以往的开发中,行为和数据是绑定在游戏对象上的。这就使得行为的复用性非常的低,因为我们需要为很多游戏对象编写它对应的行为和数据。虽然我们可以通过许多设计模式来提高行为的复用性,但这也会使得系统越加复杂,难以理解。并且大量虚函数的调用也会是一笔不小的开销。

而对于ECS下的System就能以非常高的复用性来复用游戏对象原本的行为。因为在ECS体系下,System能看见的就只有数据。也就是说只要一个游戏对象上拥有这个系统所需要处理的数据,那么这个游戏对象就能拥有这个System所对应的行为,和这个游戏对象是人、是NPC、是房子、是石头......没有一点关系。

这就能使得不同的游戏对象可以复用相同的行为,甚至这些游戏对象可以是不同类别。(可以不派生自同一个超类)

针对DOTS做了一个案例,代码如下:


比较了10w个Cube旋转+移动时,以往开发和DOTS开发的差别。

以往开发

MoveNormal.cs

public class MoveNormal : MonoBehaviour
{
    public float m_Speed;

    void Update()
    {
        transform.position += Vector3.forward * m_Speed * Time.deltaTime;
    }
}

RotateNormal.cs

public class RotateNormal : MonoBehaviour
{
    public float m_Speed;

    void Update()
    {
        transform.rotation *= Quaternion.AngleAxis(Mathf.Rad2Deg * m_Speed * Time.deltaTime, Vector3.up);
    }
}

DOTS开发

MoveSpeed.cs

[GenerateAuthoringComponent]
public struct MoveSpeed : IComponentData
{
    public float Value;
}

RotateSpeed .cs

[GenerateAuthoringComponent]
public struct RotateSpeed : IComponentData
{
    public float Value;
}

MoveSystem.cs

public class MoveSystem : JobComponentSystem
{
    EntityQuery m_Group;

    protected override void OnCreate()
    {
        m_Group = GetEntityQuery(typeof(Translation),
            ComponentType.ReadOnly());
    }
    
    [BurstCompile]
    struct MoveJob : IJobChunk
    {
        public float dt;
        public float3 direction;
        public ArchetypeChunkComponentType translationType;
        [ReadOnly] public ArchetypeChunkComponentType moveSpeedType;


        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var translationArray = chunk.GetNativeArray(translationType);
            var moveSpeedArray = chunk.GetNativeArray(moveSpeedType);
            

            for (int i = 0; i < chunk.Count; i++)
            {
                float3 offset = dt * moveSpeedArray[i].Value * math.normalize(direction);
                translationArray[i] = new Translation()
                {
                    Value = translationArray[i].Value + offset
                };
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        MoveJob job = new MoveJob()
        {
            dt = UnityEngine.Time.deltaTime,
            direction = new float3(0, 0, 1),
            translationType = GetArchetypeChunkComponentType(),
            moveSpeedType = GetArchetypeChunkComponentType(true)
        };

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

RotateSystem.cs

public class RotateSystem : JobComponentSystem
{
    EntityQuery m_Group;

    protected override void OnCreate()
    {
        base.OnCreate();
        m_Group = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly());
    }

    [BurstCompile]
    struct RotateJob : IJobChunk
    {
        public float dt;
        public ArchetypeChunkComponentType rotationType;
        [ReadOnly] public ArchetypeChunkComponentType rotateSpeedType;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var rotationArray = chunk.GetNativeArray(rotationType);
            var rotationSpeedArray = chunk.GetNativeArray(rotateSpeedType);

            for(int i=0;i(),
            rotateSpeedType = GetArchetypeChunkComponentType()
        };
        return job.Schedule(m_Group, inputDeps);
    }
}

产生Cube

Spawn.cs

public class Spawn : MonoBehaviour
{
    public GameObject m_PrefabECS;
    public GameObject m_PrefabNormal;
    [Header("长")]
    public int m_CountX;
    [Header("宽")]
    public int m_CountZ;
    [Header("高")]
    public int m_CountY;
    private int m_Amount = 0;
    Entity m_EntityPrefab;
    private void Start()
    {
        m_EntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(m_PrefabECS,
            GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null));
    }

    private void OnGUI()
    {
        if(GUILayout.Button("产生CubeDOTS"))
        {
            InstantiateCubes(m_EntityPrefab);
        }
        if(GUILayout.Button("产生Cube(Normal)"))
        {
            InstantiateCubes(m_PrefabNormal);
        }
        GUILayout.Label("Amount:" + m_Amount);
    }

    List InstantiateCubes(T1 prefab) where T2:IInstantiateCubes,new()
    {
        m_Amount += m_CountX * m_CountY * m_CountZ;
        return new T2().InstantiateCubes(GetPositions(),prefab);

        //=====================================
        List GetPositions()
        {
            List result = new List(m_Amount);

            float3 pos = float3.zero;
            for (int i = 1; i <= m_CountX; i++)
            {
                pos.y = 0;
                for (int j = 1; j <= m_CountY; j++)
                {
                    pos.x = 0;
                    for (int k = 1; k <= m_CountX; k++)
                    {
                        result.Add(pos);
                        pos.x += 1.5f;
                    }
                    pos.y += 1.5f;
                }
                pos.z += 1.5f;
            }
            return result;
        }
    }
}



public interface IInstantiateCubes
{
    List InstantiateCubes(List positions, T prefab);
}

public class SpawnCubeDOTS : IInstantiateCubes
{

    public List InstantiateCubes(List positions,Entity prefab)
    {
        EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        
        var result=positions.Select(pos => entityManager.Instantiate(prefab))
                            .Select((entity, index) =>
                            {
                                entityManager.SetComponentData(
                                    entity,
                                    new Translation { Value = positions[index] });
                                return entity;
                            });
        return new List(result);
    }
}

public class SpawnCubeNormal : IInstantiateCubes
{
    public List InstantiateCubes(List positions, GameObject prefab)
    {
        var result = positions.Select(pos => GameObject.Instantiate(prefab,pos,Quaternion.identity));
        return new List(result);
    }
}

对比结果

  • 以往开发
Unity - DOTS(多线程数据导向型技术堆栈)_第1张图片
Normal.png
  • DOTS
Unity - DOTS(多线程数据导向型技术堆栈)_第2张图片
DOTS.png
  • 提示
    • Unity.Mathematics一定要用上。这是Unity新的数学库,对这里面的数据类型进行运算时,运算指令能映射到硬件的SIMD寄存器上。这就使得一条指令可以进行多项运算,效率大幅度提升。
    • 还要就是在Material中,一定要把 Enable GPU Instancing勾上,能很大程度的降低Draw Call。

你可能感兴趣的:(Unity - DOTS(多线程数据导向型技术堆栈))