Unity下一轮最大的变革-Entity Component System & C# Jobs System #1

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;

/// 
/// ComponentSystem管理游戏逻辑(类比以前的MonoBehavior)
/// 该类只有一个OnUpdate方法需要复写
/// 
class RotatorSystem : ComponentSystem
{
    /// 
    /// 简单的Group结构体,规定Entity必须包含哪些ComponentData
    /// 
    struct Group
    {
        public Transform transform;
        public Rotator rotator;
    }
    protected override void OnUpdate()
    {
        //遍历场景中同时包含transform和Rotator的Entity,执行操作
        foreach (var item in GetEntities())
        {
            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;

/// 
/// 一个简单的结构体(ComponentData)
/// 
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float value;
}

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


Unity下一轮最大的变革-Entity Component System & C# Jobs System #1_第1张图片
image.png

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

using System;
using Unity.Entities;

/// 
/// 一个简单的结构体(ComponentData)
/// 
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float value;
}

/// 
/// 现阶段这个wrapper是为了能够把IComponentData添加给GameObject,
/// 将来会被移去
/// 
public class RotationSpeedComponent : ComponentDataWrapper { }

Unity下一轮最大的变革-Entity Component System & C# Jobs System #1_第2张图片
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
{
    /// 
    /// 使用IJobProcessComponentData遍历符合条件的所有Entity。
    /// 此过程是单进程的并行计算(SIMD)
    /// IJobProcessComponentData 是遍历entity的简便方法,并且也比IJobParallelFor更高效
    /// 
    [ComputeJobOptimization]
    struct RotationSpeedRotation : IJobProcessComponentData
    {
        /// 
        /// deltaTime
        /// 
        public float dt;
        /// 
        /// 实现接口,在Excute中实现旋转
        /// 
         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));
        }
    }

    /// 
    /// 我们在这里,只需要声明我们将要用到那些job
    /// JobComponentSystem 携带以前定义的所有job
    /// 最后别忘了返回jobs,因为别的job system 可能还要用
    /// 完全独立于主进程,没有等待时间!
    /// 
    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 System & C# Jobs System #1)