unity dots_在DOTS上:实体组件系统
unity dots
This is one of several posts about our new Data-Oriented Tech Stack (DOTS), sharing some insights into how and why we got to where we are today, and where we’re going next.
这是有关我们新的面向数据的技术堆栈(DOTS)的几篇文章之一,分享了一些有关如何以及为什么我们到达今天的位置以及下一步去向的见解。
In my last post, I talked about HPC# and Burst as low-level foundational technologies for Unity going forward. I like to refer to this level of our stack as the “game engine engine”. Anyone can use this stack to write a game engine. We can. We will. You can too. Don’t like ours? Write your own, or modify ours to your liking.
在上一篇文章中 ,我谈到了HPC#和Burst作为Unity未来的低层基础技术。 我喜欢将此堆栈级别称为“游戏引擎引擎”。 任何人都可以使用此堆栈来编写游戏引擎。 我们可以。 我们会。 你也可以 不喜欢我们的吗? 自己编写,或根据自己的喜好修改我们的。
Unity的组件系统 (Unity’s Component System)
The next layer we’re building on top is a new component system. Unity has always been centered around the concepts of components. You add a Rigidbody component to a GameObject and it will start falling. You add a Light component to a GameObject and it will start emitting light. Add an AudioEmitter component and the GameObject will start producing sound. It’s a very natural concept for programmers and non-programmers alike, and easy to build intuitive UIs for. I’m actually quite amazed at how well this concept has aged. So well that we want to keep it. What hasn’t aged well is how we implemented our component system. It was written with an object-oriented mindset. Components and GameObjects are “heavy c++” objects. Creating/destroying them requires a mutex lock to modify the global list of id->objectpointers. All GameObjects have a name. Each one gets a C# wrapper object that points to the C++ one. That C# object could be anywhere in memory. The C++ object can also be anywhere in memory. Cache misses galore. We try to mitigate the symptoms as best we can, but there’s only so much you can do.
我们要构建的下一层是新的组件系统。 Unity始终以组件的概念为中心。 您将一个刚体组件添加到GameObject,它将开始下降。 您将Light组件添加到GameObject,它将开始发光。 添加一个AudioEmitter组件,GameObject将开始产生声音。 对于程序员和非程序员而言,这都是非常自然的概念,并且易于为其构建直观的UI。 实际上,我对这个概念的老化程度感到非常惊讶。 太好了,我们想保留它。 不好的是我们如何实现组件系统。 它是以面向对象的思维方式编写的。 组件和GameObjects是“大量c ++”对象。 创建/销毁它们需要互斥锁来修改id-> objectpointers的全局列表。 所有GameObjects都有一个名称。 每个人都获得一个指向C ++对象的C#包装对象。 该C#对象可以在内存中的任何位置。 C ++对象也可以位于内存中的任何位置。 缓存未命中。 我们尽力减轻症状,但是您只能做很多事情。
With a data-oriented mindset, we can do much better. We can keep the same nice properties from a user point of view (add a Rigidbody component, and the thing will fall), but also get amazing performance and parallelism with our new component system. This new component system is our Entity Component System (ECS). Very roughly speaking, what you do with a GameObject today you do with an Entity in the new system. Components are still called components. So what’s different? The data layout. Let’s look at some common data access patterns A typical component that you would write in Unity in the traditional way might look like this:
有了面向数据的思维方式,我们可以做得更好。 从用户的角度来看,我们可以保留相同的好属性(添加一个Rigidbody组件,它会掉下来),但是通过我们的新组件系统,还可以获得惊人的性能和并行性。 这个新的组件系统是我们的实体组件系统(ECS)。 粗略地讲,您今天使用GameObject所做的事情与使用新系统中的Entity所做的一样。 组件仍称为组件。 那有什么不同呢? 数据布局。 让我们看一些常见的数据访问模式 您将以传统方式在Unity中编写的典型组件可能如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
class Orbit : MonoBehaviour
{
public Transform _objectToOrbitAround;
void Update()
{
//please ignore this math is all broken, that's not the point here :)
var currentPos = GetComponent<Transform>().position;
var targetPos = _objectToOrbitAround.position;
GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
class Orbit : MonoBehaviour
{
public Transform _objectToOrbitAround ;
void Update ( )
{
//please ignore this math is all broken, that's not the point here :)
var currentPos = GetComponent < Transform > ( ) . position ;
var targetPos = _objectToOrbitAround . position ;
GetComponent < RigidBody > ( ) . velocity += SomehowSteerTowards ( currentPos , targetPos )
}
}
|
This pattern comes back over and over. A component has to find one or more other components on the same GameObject and read/write some values on it. There are a lot of things wrong with this:
这种模式反复出现。 组件必须在同一GameObject上找到一个或多个其他组件,并在其上读/写一些值。 这有很多问题:
-
The Update() method gets called for a single orbit component. The next Update() call might be for a completely different component, likely causing this code to be evicted from the cache the next time it has to run this frame for another Orbit component.
为单个轨道组件调用Update()方法。 下一个Update()调用可能是针对完全不同的组件,可能导致该代码在下次必须为另一个Orbit组件运行此帧时从缓存中清除。
-
Update() has to use GetComponent() to go and find its Rigidbody. (It could be cached instead, but then you have to be careful about the Rigidbody component not being destroyed).
Update()必须使用GetComponent()来查找其刚体。 (它可以被缓存,但是您必须注意不要破坏Rigidbody组件)。
-
The other components we’re operating on are in completely different places in memory.
我们正在处理的其他组件在内存中的位置完全不同。
The data layout ECS uses recognizes that this is a very common pattern and optimizes memory layout to make operations like this fast.
ECS使用的数据布局认识到这是一种非常常见的模式,并优化了内存布局以加快此类操作。
ECS数据布局 (ECS Data Layout)
ECS groups all entities that have the exact same set of components together in memory. It calls such a set an archetype. An example of an archetype is: “Position & Velocity & Rigidbody & Collider”. ECS allocates memory in chunks of 16k. Each chunk will only contain the component data for entities of a single archetype. Instead of having the user Update method searching for other components to operate on at runtime, per Orbit instance, in ECS land you have to statically declare “I want to run some operations on all entities that have both a Velocity and a Rigidbody and an Orbit component. To find all those entities, we simply find all archetypes that match a specific “component search query”. Each archetype has a list of Chunks where entities of that archetype are stored. We loop over all those chunks, and inside each of the chunks, we’re doing a linear loop of tightly packed memory, to read and write the component data. This linear loop that runs the same code on each entity also makes for a likely vectorization opportunity for Burst. In many cases, this process can be trivially split up into several jobs, making the code operating the ECS component run on nearly 100% core utilization. ECS does all this work for you, you just need to supply the code that you want to run on each entity. (You can do the chunk iteration manually if you want to though.) When you add/remove a component from an Entity, it switches archetype. We move it from its current chunk to a chunk of the new archetype, and back swap the last entity of the previous chunk to “fill the hole”. In ECS, you also statically declare what you intend to do with the component data. ReadOnly or ReadWrite. By promising (the promise is verified) to only read from the Position component, ECS can get more efficient scheduling of its jobs. Other jobs that also want to read from the Position component won’t have to wait. This data layout also allows us to deal with a long-standing frustration we’ve had, which are load times and serialization performance. Loading/streaming ECS data for a big scene isn’t much more than just loading raw bytes from disk and using them as is. This is the reason the Megacity demo loads in a few seconds on a phone.
ECS将内存中具有完全相同的一组组件的所有实体分组在一起。 它称这种集合为原型。 原型的一个例子是:“位置与速度,刚体与对撞机”。 ECS以16k的块分配内存。 每个块将仅包含单个原型实体的组件数据。 不必让用户Update方法在每个Orbit实例上搜索运行时要运行的其他组件,而是必须在ECS领域中静态声明“我想对同时具有Velocity和Rigidbody以及Orbit的所有实体执行某些操作零件。 为了找到所有这些实体,我们只需找到与特定“组件搜索查询”匹配的所有原型即可。 每个原型都有一个存储该原型实体的块列表。 我们遍历所有这些块,并在每个块内进行紧密封装的内存的线性循环,以读取和写入组件数据。 在每个实体上运行相同代码的线性循环也为Burst提供了可能的矢量化机会。 在许多情况下,此过程可以分为几个工作,从而使操作ECS组件的代码的内核利用率几乎达到100%。 ECS为您完成所有这些工作,您只需要提供要在每个实体上运行的代码即可。 (如果需要,您可以手动执行块迭代。) 当您从实体中添加/删除组件时,它会切换原型。 我们将其从其当前块移至新原型的块,然后向后交换前一个块的最后一个实体以“填补漏洞”。 在ECS中,您还可以静态声明要对组件数据执行的操作。 ReadOnly或ReadWrite。 通过承诺(已验证了承诺)仅从Position组件读取,ECS可以更有效地安排其作业。 也希望从“位置”组件中读取的其他作业将不必等待。 这种数据布局还使我们能够应对长期以来的困扰,即加载时间和序列化性能。 为大型场景加载/流式ECS数据不仅仅只是从磁盘加载原始字节并按原样使用它们。 这就是Megacity演示在手机上加载几秒钟的原因。
快乐的“事故” (Happy “Accidents”)
While entities can do what game objects do today, they can do more because they are so lightweight. In fact, what really is an Entity? In an earlier draft of this post I wrote “we store entities in chunks”, and later changed it to “we store component data for entities in chunks”. It’s an important distinction to make, to realize that an Entity is just a 32-bit integer. There is nothing to store or allocate for it, other than the data of its components. Because they’re so cheap, you can use them for scenarios that game objects weren’t suitable for. Like using an entity for each individual particle in a particle system.
尽管实体可以完成当今游戏对象的工作,但它们却可以做得更多,因为它们是如此轻巧。 实际上,什么是实体? 在本文的早期草稿中,我写了“我们以块存储实体”,后来将其更改为“我们以块存储实体的组件数据”。 认识到实体只是一个32位整数,这是一个重要的区别。 除了其组成部分的数据外,没有其他可存储或分配的内容。 由于它们非常便宜,因此您可以将其用于游戏对象不适合的场景。 就像为粒子系统中的每个粒子使用实体一样。
HPC#,突发,ECS。 太棒了,但是我的游戏引擎在哪里? (HPC#, Burst, ECS. Awesome, but where’s my game engine?)
The next layer we need to build is very big. It’s the “game engine” layer composed of features like “renderer”, “physics”, “networking”, “input”, “animation”, etc. This is roughly where we are today. We have started to work on these pieces, but they won’t be ready overnight. That might sound like a bummer. In a way it is, but in another way, it’s not. Because ECS and everything built on top of it are written in C#, it can run inside of traditional Unity. Because it runs inside of Unity, you can write ECS components that use pre-ECS functionality. There is no pure ECS mesh drawing system right now. However, you can write an ECS MeshRenderSystem that uses pre-ECS Graphics.DrawMeshIndirect API as an implementation, while you wait for a pure ECS version to ship. This is exactly the technique that our Megacity demo uses. Loading/Streaming/Culling/LODding/Animation is done with pure ECS systems, but the final drawing is not. So you can mix & match. What’s great about that is you can already reap the benefits of Burst codegen, and ECS performance for your game code, instead of having to wait for us to ship pure ECS versions of all subsystems. What’s not great about it is that in this transition phase, you can see and feel this friction that you’re “using two different worlds that are glued together”. We will ship all the source code to our ECS HPC# subsystems in packages. You can inspect, debug, modify each subsystem, as well as have more fine-grained control over when you want to upgrade which subsystem. You could, for example, upgrade the Physics subsystem package without upgrading anything else.
我们需要构建的下一层非常大。 它是“游戏引擎”层,由“渲染器”,“物理”,“网络”,“输入”,“动画”等功能组成。这大致就是我们今天的样子。 我们已经开始研究这些作品,但它们不会在一夜之间准备就绪。 这听起来简直太可惜了。 从某种意义上说是这样,但从另一种方式来说不是。 由于ECS及其基础之上的所有内容都是用C#编写的,因此它可以在传统的Unity内部运行。 因为它在Unity内部运行,所以您可以编写使用ECS之前功能的ECS组件。 目前没有纯ECS网格绘图系统。 但是,在等待纯ECS版本发布时,您可以编写一个使用ECS之前的Graphics.DrawMeshIndirect API作为实现的ECS MeshRenderSystem。 这正是我们的Megacity演示使用的技术。 加载/流传输/剔除/放置/动画是使用纯ECS系统完成的,但最终绘图却没有。 这样您就可以混合搭配。 这样做的好处是,您已经可以从Burst Codegen的优势和游戏代码的ECS性能中受益,而不必等待我们发布所有子系统的纯ECS版本。 不好的是,在此过渡阶段中,您可以看到并感觉到这种摩擦是“正在使用粘合在一起的两个不同的世界”。 我们会将所有源代码 打包 发送到我们的ECS HPC#子系统中 。 您可以检查,调试,修改每个子系统,并可以在要升级哪个子系统时进行更细粒度的控制。 例如,您可以升级Physics子系统软件包,而无需进行其他任何升级。
游戏对象会发生什么? (What will happen to Game Objects?)
Game Objects aren’t going anywhere. People have successfully shipped amazing games on it for over a decade. That foundation isn’t going anywhere.
游戏对象不会随处可见。 十多年来,人们已经成功地在其上发布了惊人的游戏。 这个基础是无处可去的。
What will change is that you will over time see our energy to make improvements tilt from going exclusively into the game object world, towards the ECS world.
将会发生的变化是,随着时间的推移,您将看到我们的改进动力,从专门进入游戏对象世界,转向ECS世界。
API可用性/样板 (API Usability / Boilerplate)
A common, very valid, point people bring up when looking at ECS, is that there’s a lot of typing. A lot of boilerplate code that stands in between you and what you’re trying to achieve.
人们在查看ECS时提出的一个常见的非常有效的观点是,打字很多。 介于您和您要实现的目标之间的许多样板代码。
There are a lot of improvements on the horizon that aim to remove the need for most boilerplate and make it simpler to express your intent. We haven’t implemented many of them yet as we’ve been focussing on the foundational performance, but we believe there is no good reason for ECS game code to have much boilerplate code, or be particularly more work to write than writing a MonoBehaviour.
即将出现的许多改进旨在消除对大多数样板的需求,并使其更易于表达您的意图。 由于我们一直专注于基本性能,因此我们尚未实现其中的许多功能,但是我们认为,ECS游戏代码没有太多的样板代码,或者比编写MonoBehaviour要做的工作特别多。
Project Tiny has already implemented some of these improvements (like a lambda based iteration API). Speaking of which..
Tiny项目 已经实现了其中一些改进(例如基于lambda的迭代API)。 说到哪个
Tiny项目的ECS如何适合所有这一切? (How does Project Tiny’s ECS fit into all this?)
Project Tiny will ship on top of the same C# ECS as this blog post has been talking about. Project Tiny will be a big ECS milestone for us in several ways:
Tiny项目 将在此博客所讨论的相同C#ECS上发布。 Tiny项目将从以下几个方面成为我们ECS的一个重要里程碑:
-
It will be able to run in a complete ECS-only environment. A new player with no baggage from the past.
它将能够在完整的仅限ECS的环境中运行。 一个新球员,过去没有行李。
-
That means it’s also pure-ECS and has to ship with all ECS subsystems a real (tiny) game needs.
这意味着它也是纯ECS,并且必须随所有ECS子系统一起提供真正的(微型)游戏需求。
-
We’ll adopt Project Tiny’s Editor support for Entity editing for all ECS scenarios, not just tiny.
我们将为所有ECS场景(不仅仅是很小的场景)的实体编辑采用Project Tiny的Editor支持。
加入我们? (Join us?)
We have job openings for all the different parts of the DOTS stack, particularly in Burbank and Copenhagen, check out careers.unity.com.
我们为DOTS堆栈的所有不同部分提供了职位空缺,尤其是在伯班克和哥本哈根,请访问 careers.unity.com 。
Also, make sure to join us on Unity Entity Component System and C# Job System forum to give feedback and get information on experimental and preview features.
另外,请确保加入 Unity Entity Component System和C#Job System论坛, 以提供反馈并获取有关实验和预览功能的信息。
翻译自: https://blogs.unity3d.com/2019/03/08/on-dots-entity-component-system/
unity dots