Unity2018新功能之Entity Component System(ECS)二

版权声明:By XM肖牧 https://blog.csdn.net/yye4520/article/details/82804179

在上一篇文章里,我们简单的介绍了ECS的概念和Entities的使用方法,我将在这篇文章中继续深入讲解ECS的框架。

Unity2018新功能之Entity Component System(ECS)一

首先我们先来看下面这一张图。这是我通过ECS框架创建出来的5万个Cube,FPS基本稳定在30左右,因为默认是实时光照,所以渲染压力有点大,这不是我们这篇文章的讨论范围。

为什么ECS那么牛逼呢?

我们来看看我们传统的开发方式,打个比方GameObject组件,GameObject里面既有Transform,renderer,rigidbody,collider,又有GetComponent等属性和方法,当我们在实例化GameObject的时候,是把里面所有的东西全部都加到内存里面的,而且是离散式的!即物体和它的组件(Component)并非在同一个内存区段,每次存取都非常耗时。而ECS会确保所有的组件资料(Component Data)都紧密的连接再一起,这样就能确保存取内存资料时以最快的速度存取。真正做到需要什么就用什么,不带一点浪费的。

 

我们一起来创建ECS代码

 1.创建实体管理器和实体。

由于上一篇文章没有讲如何创建实体管理器(EntityManager)和创建实体(Entity),那么我们接下来就讲一下如何用代码创建实体管理器(EntityManager)和创建实体(Entity)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using Unity.Entities;
using Unity.Transforms;
using Unity.Collections;
using Unity.Rendering;

public class Main : MonoBehaviour {

    public EntityManager entityManager;
    public EntityArchetype entityArchetype;

    public int createPrefabCout;
    public GameObject prefab;
    public Mesh mesh;
    public Material material;

    // Use this for initialization
    void Start ()
    {
        //创建实体管理器
        entityManager = World.Active.GetOrCreateManager<EntityManager>();

        //创建基础组件的原型
        entityArchetype = entityManager.CreateArchetype(typeof(Position), typeof(Rotation), typeof(RotationSpeed));

        if (prefab)
        {
            for (int i = 0; i < createPrefabCout; i++)
            {
                //创建实体
                Entity entities = entityManager.CreateEntity(entityArchetype);

                //设置组件
                entityManager.SetComponentData(entities, new Position { Value = UnityEngine.Random.insideUnitSphere * 100 });
                entityManager.SetComponentData(entities, new Rotation { Value = quaternion.identity });
                entityManager.SetComponentData(entities, new RotationSpeed { Value = 100 });

                //添加并设置组件
                entityManager.AddSharedComponentData(entities, new MeshInstanceRenderer
                {
                    mesh = this.mesh,
                    material = this.material,
                });
            }


            //NativeArray<Entity> entityArray = new NativeArray<Entity>(createPrefabCout, Allocator.Temp);

            //for (int i = 0; i < createPrefabCout; i++)
            //{
            //    entityManager.Instantiate(prefab, entityArray);
            //    entityManager.SetComponentData(entityArray[i], new Position { Value = UnityEngine.Random.insideUnitSphere*10 });
            //    entityManager.AddSharedComponentData(entityArray[i], new MeshInstanceRenderer
            //    {
            //        mesh = this.mesh,
            //        material = this.material,
            //    });
            //}

            //entityArray.Dispose();
        }
    }
}

我先创建了一个EntityManager管理器,然后在它下面生成了一个Archetype,这种EntityArchetype能确保存放在里面的Component Data都紧密的相连,当一次性产生大量的Entities并给予各种逻辑运算时,这种结构在内存与快取之间的移动性能直逼memcpy。

虽然Prefab是传统Game Object,但在实例化成Entity时,Unity会自动把跟ECS无关的Component拆离,只留下Component Data生成Entity物体。这种跟Unity整合的流程,可以称为Hybrid ECS,也是Unity在集成ECS的主要路线。

注释的地方本来是用NativeArray<T>,这是Unity新的集合而非C#的,需要引用Unity.Collections,因为我要创建很多个实体,超过了它的范围就没用了。

2.创建旋转速度组件

using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;

[System.Serializable]
public struct RotationSpeed : IComponentData
{
    public float Value;
}
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { };

在上一篇文章中的组件我是直接用传统的方式挂载的,这次我改成用ECS的组件。

所有的组件都需要继承SharedComponentDataWrapperComponentDataWrapper,数据(struct)需要继承ISharedComponentDataIComponentData。使用SharedComponentDataWrapperISharedComponentData可显著降低内存,创建100个cube和一个cube的消耗内存的差异几乎为零。

3.创建旋转速度系统

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
    {
        public float dt;

        public void Execute(ref Rotation rotation, ref RotationSpeed speed)
        {
            rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.Value * dt));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, inputDeps);
    }
}

RotationSpeedRotation里面的Execute,可以理解为传统模式中的Update,不过是并行执行的。相关逻辑就是计算运行时间、运算位置并赋值。

想要把RotationSpeedRotation中的变量和我们找到的物体联系起来,且在Job系统中并行执行就需要JobHandle OnUpdate。他的作用是把我们包装起来的业务逻辑【就是Execute】放到Job系统执行【多核心并行计算】,并且把找到的物体和RotationSpeedRotation中的变量关联起来。

我们一起来运行ECS框架

代码写完之后我们创建一个Cube的预设,挂载上GameObjectEntity实体,PositionComponent,RotationComponent,RotationSpeedComponent组件,再创建一个材质,最后在场景挂上Main脚本给变量赋值,运行Unity3d,我们将看到文章开头的那张图片,Good!

其中PositionComponent,RotationComponent和MeshInstanceRenderer是Entities自带的组件,前两个比较好了解,MeshInstanceRenderer是为了创建网格模型和材质,使我们创建的实体能够被可视化,不然我们的实体将是透明的,我们也可以自己写,这里就不再做解释了。

由于我们的创建的对象已经脱离了GameObject,所以我们在Hierarchy面板看不到创建出来的对象。

我们打开 Window>Analysis>Entity Debugger,我们就可以看到我们创建出来的对象的信息了,左边是系统的信息,显示我们整个项目里面所有的系统,没有被使用的系统会显示为not run。右边是实体的信息,显示我们创建的所有的实体,我们可以看见我们总的创建了5万个实体。

我们随便点击一个实体,便可以看到该实体所包含的所有组件信息。

我们展开所有组件,发现组件的数据是不能修改的,听Unity中国的技术总监杨栋老师说,Entities的开发团队正在完善这个框架,使它能够像传统的开发模式一样编辑。

注:由于这个ECS框架是预览版(目前使用的是0.0.12-preview.14)的还不是很完善,在使用上并不是那么友好,而且没有做异常处理经常导致Unity奔溃。比如CreateArchetype加了ComponentType之后,如果没有SetComponentData该ComponentType,Unity3d就会奔溃。

如果用ECS框架开发一个大型游戏项目,那这个项目到底需要多少个ComponentData和ComponentSystem(此处隐藏着一个破涕为笑的表情),我真的为我们Entities开发团队感到骄傲(此处隐藏着一个捂脸的表情)。

希望Unity3d的Entities开发团队能够更好的完善ECS这个框架,我对这个框架抱有很大的希望,因为它的出现改变了Unity3d引擎做出来的游戏性能差的标签。加油!

猜你喜欢

转载自blog.csdn.net/yye4520/article/details/82804179