Unity ECS 系列(一)- Unity ECS简介

最近发觉刚看过的东西在脑中却忘越来越快了,比如Unity的ECS,仅仅一个月,就忘的什么也不剩。有些记忆可能还是写下来更持久些,从今天开始边写边重新学吧。


了解过ECS的开发者都知道ECS与Unity原本的开发理念相差很大,需要所有Unity开发者重新去学习和适应新的开发框架的代价还是很大的,Unity为何要做出这么大跨度的尝试呢?

Unity正在尝试解决什么问题?

以前我们基于Unity的GameObject/MonoBehaviour机制,可以非常简单为创作游戏编写代码,但最终往往让代码陷入难以阅读,维护和优化的境地。这是一系列因素联合导致的:

  • 面向对象模型

  • 由Mono编译的非最优机器码

  • GC机制

  • 单线程开发


Entity-Component-System 登场

Entity-Component-System 是一种编写代码的方式,简称ECS,近年因OW被广泛熟知,ECS主要关注开发中一个很基本的问题:如何组建并处理游戏中的数据和行为

后续文章我们会更具体的讲解ECS的概念,本章我们简单介绍ECS在Unity中的使用

采用ECS不但在设计上可以更好的进行游戏编程,还可以利用Unity提供的JobSystem和Brush编译器充分发挥多核处理器的性能。

Unity2017以后已经发布了JobSystem,基于JobSystem可以在C#代码中更好的实现多线程批处理技术,JoySystem底层为多线程间的竞争提供的安全保障。

对于开发者而言,更重要的是要使用一种新的思维方式和编码方式来充分利用JobSystem。


ECS有什么不同?

MonoBehavior -我们的老战友

MonoBehavior 既包含数据也包含行为。下面这段代码演示了Rotator组件每帧都要对Transform组件进行旋转操作。


using UnityEngine;

class Rotator : MonoBehaviour
{
    // 数据:可以在Inspector窗口中编辑的旋转速度值
    public float speed;
    
    // 行为:从component中读取速度值,然后修改Transform组件中的rotation
    void Update()
    {
        transform.rotation *= Quaternion.AngleAxis(Time.deltaTime * speed, Vector3.up);
    }
}

然而MonoBehaviour 是继承于数个其它类的,且每个其它类包含了他们自己的数据,除了Transform,上面代码中没有用到任何他们中的数据。这其实浪费了很多不必要的内存,因此我们在设计一个系统时,需要考虑哪些数据是我们真正需要的。

ComponentSystem -迈入新纪元的一步

using Unity.Entities;
using UnityEngine;

// 数据:可以在Inspector窗口中编辑的旋转速度值
class Rotator : MonoBehaviour
{
    public float Speed;
}

// 行为:继承自ComponentSystem来处理旋转操作
class RotatorSystem : ComponentSystem
{
    struct Group
    {
        // 定义该ComponentSystem需要获取哪些components
        public Transform Transform;
        public Rotator   Rotator;
    }
    
    override protected void OnUpdate()
    {
        // 这里可以看第一个优化点:
        // 我们知道所有Rotator所经过的deltaTime是一样的,
        // 因此可以将deltaTime先保存至一个局部变量中供后续使用,
        // 这样避免了每次调用Time.deltaTime的开销。
        float deltaTime = Time.deltaTime;
        
        // ComponentSystem.GetEntities<Group>可以高效的遍历所有符合匹配条件的GameObject
        // 匹配条件:即包含Transform又包含Rotator组件(在上面struct Group中定义)
        foreach (var e in GetEntities<Group>())
        {
            e.Transform.rotation *= Quaternion.AngleAxis(e.Rotator.Speed * deltaTime, Vector3.up);
        }
    }
}

在ECS模型中,Component(组件)只包含数据

ComponentSystem 则包含行为,一个 ComponentSystem 更新所有与之组件类型匹配的GameObject。


混合ECS:使用与 ComponentSystem 现有的 GameObject & components 一起工作

目前,现有的Unity工程基本都是基于MonoBehaviour&GameObject&components,如果想与现有GameObject&components一起使用ECS,混合ECS将是个不错的选择。上面的例子演示了我们可以简单的遍历访问即包含Rotator又包含Transform组件的实体对象。

ComponentSystem 是怎么访问Rotator和Transform的?

为了能像上面例子中那样可以遍历所有匹配组件类型的实体,这些实体必须由 EntityManager 创建。

ECS 框架提供了一个叫 GameObjectEntity 的组件,在OnEnable时,GameObjectEntity会在GameObject上创建一个含有所有组件的实体(Entity)。所以ComponentSystems 可以获取完整的GameObject及其所有组件。

因此在目前的情况下,如果你需要在ComponentSystems访问一个GameObject,则必须在该GameObject上添加一个GameObjectEntity组件。

如何将现有代码转为混合ECS?

我们要把MonoBehaviour.Update转换为ComponentSystems.OnUpdate的方式,可以继续将所有的数据保存在MonoBehaviour中,这是一种很简单向ECS的过渡方式。

因此场景数据仍然存在于GameObjects & components中,可以继续使用GameObject.Instantiate以创建实例等。

混合ECS的优点:

  • 数据与行为的分离的方式,会让代码整体看起来更清晰

  • 系统对许多对象是可以进行批量操作的,避免了一些无意义的调用。(见上面deltaTime优化)

  • 我们可以继续使用现有的Inspectors, Editor tools等工具

混合ECS的缺点:

  • 实例化时间并没有得到优化

  • 加载时间并没有得到优化

  • 数据是随机访问的,没有线性内存访问的高效性

  • 没有发挥多核功能

  • 没有SIMD

因此,使用ComponentSystem, GameObject 和 MonoBehaviour 结合是编写ECS代码的一个简易的改变。混合ECS提供了一些简单的性能改进,但是它并没有充分发挥ECS的所有性能优势。


纯ECS: 使用IComponentData & Jobs全面提升性能

通常让游戏具有更好的性能是选择ECS的一个重要原因,但如果我们利用CPU的SIMD特性来编写所有代码,其实最终的性能和基于ECS编写的是差不多的。

结合ECS与C# JobSystem将提供SIMD的可能性,以发挥CPU最大性能。

C# JobSystem 只支持structs和NativeContainers,并不支持托管数据类型。所以,在C# JobSystem中,只有IComponentData数据可以被安全的访问。

另外,EntityManager内部保证了ComponentData(组件)数据的线性内存布局,这是C# JobSystem中可以高效的使用IComponentData最重要的依据。

using System;
using Unity.Entities;

// 定义一个ComponentData用于存储旋转速度
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float Value;
}

// ComponentDataWrapper用于将ComponentData添加到GameObject,
// 这一步需要手动添加,将来Unity会自动化这步操作。
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { } 
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

// IJobProcessComponentData 是遍历匹配组件类型Entity的一种很简易的方式,
// 这比使用 IJobParallelFor 更方便、有效。
// Entity的处理(Execute)是并行的,主线程只负责调度Job
public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
    {
        public float dt;

        // IJobProcessComponentData 声明了需要读取 RotationSpeed 和写入 Rotation.
        public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
        {
            rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.Value * dt));
        }
    }

    // 继承自JobComponentSystem会让系统为Job提供必要的依赖关系,
    // 其它之前任何写入Rotation或RotationSpeed的JobComponentSystem都将参与依赖计算.
    // 这里必须返回调度后的JobHandle,以便系统处理依赖执行顺序。
    // 这样处理的优点:
    //  * 主线程是非阻塞的,只需考虑依赖关系调度Job,当依赖项全部执行完成,Job才会执行。
    //  * 依赖项的构成是自动计算的,因此我们可以模块化的编写多线程代码。
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, 64, inputDeps);
    } 
}

下一次我们将更详细的介绍ECS。

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

推荐阅读更多精彩内容