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;

/// <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文档,其余为原创

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容