美文网首页Unity技术分享Unity3d进阶unity
Unity下一轮最大的变革-Entity Component S

Unity下一轮最大的变革-Entity Component S

作者: Angeladaddy | 来源:发表于2018-05-22 01:03 被阅读79次
    ECS+jobs实现的酷炫效果

    新一代Entity Component System(ECS)将会彻底改变Unity的底层概念(GameObject-Component 系统)和现有工作方式。MonoBehavious、Update、GameObject....这些概念已经过时了!

    1. 什么是ECS?

    ECS,中文:实体-组件系统。并不是什么新鲜玩意,它是在游戏架构中广泛采用的一种架构。在游戏中,每个物体是一个Entity(实体),比如,敌人、子弹、车辆等。每个实体具有一个或多个组件,赋予这个实体不同的行为或功能。所以,一个实体的行为可以在游戏运行时通过加减组件进行改变。ECS经常和数据驱动模式一起使用。 wiki链接

    从上面介绍可以看到,Unity中的GameObject好像扮演着实体的角色,但,它不够纯粹!

    • 下一代ECS全新架构中,将不会再有GameObject的概念,取而代之的是真正的轻量化的Entity,一个Entity就只是Entity,可以把它看成超级轻量化的GameObject,实际上,一个Entity什么都做不了。它不存储任何数据,甚至连名字都没有!
    • 你可以给Entity增加或移去Component,但,旧的Component不复存在,其被重新定义为ComponentData,这是一个继承自IComponentData接口的,高效的结构体:
    struct MyComponent: IComponentData
    {} 
    

    和旧的组件系统不同的是,IComponentData只存储数据,可随时添加到Entity或者移去。

    • EntityManager将会管理所有的Entity和其上的ComponentData,它将会保证内存的线性访问。
    • 新定义的ComponentSystem管理游戏逻辑(类比以前的MonoBehavior),可以操作旧的GameObjects/Components, 或者新的 ECS ComponentData/ Entity。

    2.为什么要引入 ECS

    传统的 GameObject/MonoBehaviour 系统七宗罪:(深有体会)

    • 面向对象的编程方式(曾经的圣典,已经跟不上时代,OOP最大的问题是数据和逻辑混在一起...现在我们要数据驱动模型)
    • Mono编译的未经优化的机器码
    • 糟糕的垃圾回收
    • 不能忍的单线程

    Entity-component-system的出现,就是解决这些问题:

    • 简单的思想:数据和逻辑构成你的游戏。
    • 有了ECS,就可以使用Unity(c#) Job System 和 Burst 编译器,充分发挥多核CPU的潜力

    3. Hello world

    说了半天,我到底怎么用ECS?
    unity世界的helloworld莫过于沿着y轴旋转一个方块,先来看看我们的老朋友MonoBehavior :

    using UnityEngine;
    
    class Rotator : MonoBehaviour
    {
        void Update()
        {
            transform.rotation *= Quaternion.AngleAxis(Time.deltaTime * speed, Vector3.up);
        }
    }
    

    多么熟悉的代码啊!伴随了我们将近10年的时间,一想到就要老去,竟然有些伤感...
    伤感归伤感,但是上面的代码,一直就有巨大的问题,十几年来我们只不过视而不见罢了。
    我们在unity中新建一个脚本,要挂在GameObject上,就必须继承MonoBehaviour,而MonoBehaviour 本身就继承自很多父类,父类中定义的很多字段、属性,在我们的小脚本中根本用不到,可还是不得不继承。白白的浪费了内存。

    下面我们来试试ECS:

    1. 目前ECS尚处于开发阶段,我们需要以下前提条件才能开启:
      • Unity2018.1版本以上
      • Build Settings - Player Settings ,设置c# runtime:


        image.png
      • 打开项目目录下packages/ manifest.json 文件,加入以下内容:
    {
        "dependencies": {
            "com.unity.entities":"0.0.11"
        },
        "testables": [
            "com.unity.collections",
            "com.unity.entities",
            "com.unity.jobs"
        ],
        "registry": "https://staging-packages.unity.com"
    }
    
    1. 场景中建立一个cube,新建Rotator.cs脚本,并拖给cube:
    public class Rotator : MonoBehaviour {
            //是的,没看错,只有数据
         public float speed;
    }
    
    1. 新建RotatorSystem.cs脚本:
    using System.Collections;
    using System.Collections.Generic;
    using Unity.Entities;
    using UnityEngine;
    
    /// <summary>
    /// ComponentSystem管理游戏逻辑(类比以前的MonoBehavior)
    /// 该类只有一个OnUpdate方法需要复写
    /// </summary>
    class RotatorSystem : ComponentSystem
    {
        /// <summary>
        /// 简单的Group结构体,规定Entity必须包含哪些ComponentData
        /// </summary>
        struct Group
        {
            public Transform transform;
            public Rotator rotator;
        }
        protected override void OnUpdate()
        {
            //遍历场景中同时包含transform和Rotator的Entity,执行操作
            foreach (var item in GetEntities<Group>())
            {
                item.transform.rotation *= Quaternion.AngleAxis(item.rotator.speed * Time.deltaTime, Vector3.up);
            }
        }
    }
    
    

    这个脚本不用拖拽给任何场景中的物体,运行时它会自动遍历场景中符合条件的Entity。
    但此时执行游戏,不会有任何变化,下一步,需要在cube上再挂一个GameObjectEntity组件,告诉ComponentSystem这是一个GameObject类型的实体。

    ECS ships with the GameObjectEntity component. On OnEnable, the GameObjectEntity component creates an entity with all components on the GameObject. As a result the full GameObject and all its components are now iterable by ComponentSystems.

    Thus for the time being you must add a GameObjectEntity component on each GameObject that you want to be visible / iterable from the ComponentSystem.

    运行游戏,voila!cube开始旋转。

    4. 好吧,看起来很炫,但这对我的游戏开发有什么意义?

    仔细想想:切换到ECS,我们需要做的只是:从MonoBehavior中把逻辑剥离出来放到ComponentSystem的OnUpdate里。实际上,以上有关ECS的代码示例只是‘Hybird’模式,对于大量已经开发的工程,这是一种无痛解决方案。unity这次的变化太大了,所以必须要有这么一种过渡阶段。

    那这样做有什么好处吗?

    • 分离数据和逻辑,麻麻再也不用担心我的代码难看了。
    • 系统批量处理物体(Entity),而不是单个处理,执行效率大大优化。
    • hybird模式允许你继续使用熟悉的模式,inspectors、editor tools等的同时,享受到ECS带来的效率提升。

    ok,那在Hybird模式下使用ECS有什么损失呢?

    • 初始化时间(遍历寻找Entity的过程)无法优化
    • 载入时间无法优化
    • 数据在内存中是随机获取的,非线性,执行效率下降
    • 无法利用多核处理器
    • 没有SIMD

    SIMD,Single instruction, multiple data,计算机在多核处理器上同时进行同种运算的能力。数据处理是并行的,但不是并发的。也就是,CPU单进程的并发计算。

    5. 纯ECS解决方式:

    欢迎进入Unity的未来:ECS+IComponentData+c# jobs

    • 我们使用ECS的本意是为了提高执行效率(performance),为了获得这种高效率,你必须使用 SIMD 方式编写代码(custom data layouts for each loop)。
    • c# job system 只能够管理结构体和NativeContainers,因此,IComponentData是最好的解决方案。
    • EntityManager 保证了线性内存模型下的访问(linear memory layout:https://en.wikipedia.org/wiki/Flat_memory_model)。
      三者搭配使用,如虎添翼。

    让我们再次转动这个cube:

    1. 场景中再建一个cube,写以下代码:
    using System;
    using Unity.Entities;
    
    /// <summary>
    /// 一个简单的结构体(ComponentData)
    /// </summary>
    [Serializable]
    public struct RotationSpeed : IComponentData
    {
        public float value;
    }
    

    可以看到,和我们一开始说的一样,这就是一个继承自IComponentData的简单结构体,对应过去的Component,此结构体可以从Entity上增加、删除。
    但此时直接拖拽给新的cube,提示不能添加:


    image.png

    ...没继承MonoBehavior当然不能添加。
    加入一行代码就可以了:

    using System;
    using Unity.Entities;
    
    /// <summary>
    /// 一个简单的结构体(ComponentData)
    /// </summary>
    [Serializable]
    public struct RotationSpeed : IComponentData
    {
        public float value;
    }
    
    /// <summary>
    /// 现阶段这个wrapper是为了能够把IComponentData添加给GameObject,
    /// 将来会被移去
    /// </summary>
    public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { }
    
    
    image.png

    再写一个RotationSpeedSystem,此脚本不用赋给任何物体。

    using Unity.Collections;
    using Unity.Entities;
    using Unity.Jobs;//c# jobs
    using Unity.Mathematics;//新的命名空间
    using Unity.Transforms;//新的命名空间
    using UnityEngine;
    
    public class RotationSpeedSystem : JobComponentSystem
    {
        /// <summary>
        /// 使用IJobProcessComponentData遍历符合条件的所有Entity。
        /// 此过程是单进程的并行计算(SIMD)
        /// IJobProcessComponentData 是遍历entity的简便方法,并且也比IJobParallelFor更高效
        /// </summary>
        [ComputeJobOptimization]
        struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
        {
            /// <summary>
            /// deltaTime
            /// </summary>
            public float dt;
            /// <summary>
            /// 实现接口,在Excute中实现旋转
            /// </summary>
             public void Execute(ref Rotation rotation, ref RotationSpeed speed)
            {
                //读取speed,进行运算后,赋值给rotation
                rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.value * dt));
            }
        }
    
        /// <summary>
        /// 我们在这里,只需要声明我们将要用到那些job
        /// JobComponentSystem 携带以前定义的所有job
        /// 最后别忘了返回jobs,因为别的job system 可能还要用
        /// 完全独立于主进程,没有等待时间!
        /// </summary>
        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            var job = new RotationSpeedRotation() { dt = Time.deltaTime };
            return job.Schedule(this, 64, inputDeps);
        }
    }
    
    

    cube 又转了。

    本系列文章99.9%的内容来自于官方github文档,其余为原创

    相关文章

      网友评论

        本文标题:Unity下一轮最大的变革-Entity Component S

        本文链接:https://www.haomeiwen.com/subject/twlvdftx.html