Unity笔记
首发于Unity笔记
Unity ECS编程官方文档选译——ECS In Detail(1)

Unity ECS编程官方文档选译——ECS In Detail(1)

译注:如果暂时还没有对ECS的初步了解,请首先参阅Getting Started
本文节选自ecs_in_detail.md


一、Entity,Component与内存布局


1,Entity


Entity(本质上)是一个ID。你可以把它当作一种超级轻量级的GameObject,默认情况下它甚至没有名字。


您可以在运行时从Entity动态添加或删除Component。Entity ID是可靠的。 实际上,如果你想要存储对另一个Component或Entity的引用,使用Entity ID去存储也是唯一可靠的方式。


译注:作者说把Entity当作GameObject有一定的误导性,实际上它和GameObject非常不同。Entity本质是数据(就是Component)分类分组以后得到的一个ID而已,我们用这个ID去操作一组相同类型的数据。


2,IComponentData


传统的Unity Component(包括MonoBehaviour)是面向对象的类,所以说它包含了数据和定义行为的方法。IComponentData是纯粹的ECS类型的组件,意味着他不定义行为,只包含数据。IComponentData是结构体(struct)而非类(class),所以他们进行值拷贝而不是引用拷贝。


你一般需要使用如下模式去修改IComponentData的值:

var transform = group.transform[index]; //读取

transform.heading = playerInput.move; //修改
transform.position += deltaTime * playerInput.move * settings.playerMoveSpeed;

group.transform[index] = transform; //写入


注意:ECS很快就要基于C#7.0编译了,如果使用C#7.0的ref return特性就使额外的内存分配变得没有必要了。


IComponentData结构体不能包含对托管对象的引用,因为组件里所有的数据都存在于简单的没有垃圾回收的块(chunk)里。


译注:IComponentData作为struct直接在栈里分配,它不像class一样分配在托管堆里,不进行垃圾回收,是实现所谓“连续紧凑的内存布局”的好结构。
纯粹ECS下,Component名为Component,但要抛弃过去的认识,实际上它就只是纯粹的数据而已,与逻辑完全无关。


3,EntityArchetype


一个EntityArchetype是一个ComponentType构成的独特数组。EntityManager使用它来对所有使用相同ComponentType的Entity进行分组。


// 使用typeof关键字从一组Component中创建一个EntityArchetype
EntityArchetype archetype = EntityManager.CreateArchetype(typeof(MyComponentData), typeof(MySharedComponent));

// 相同功能的API,但是更轻巧也更高效
EntityArchetype archetype = EntityManager.CreateArchetype(ComponentType.Create<MyComponentData>(), ComponentType.Create<MySharedComponent>());

// 从EntityArchetype创建一个Entity
var entity = EntityManager.CreateEntity(archetype);

// 方便起见,隐式地创建一个EntityArchetype
var entity = EntityManager.CreateEntity(typeof(MyComponentData), typeof(MySharedComponent));


4,EntityManager


EntityManager拥有EntityData、EntityArchetypes、 SharedComponentData和ComponentGroup。


你在EntityManager 里面可以找到API来创造Entity、检查一个Entity是否存活、实例化Entity以及添加或移除组件。


使用EntityManager对Entity进行操作实例如下:

// 创建一个不包含Component的Entity
var entity = EntityManager.CreateEntity();

// 在运行时向Entity动态添加Component
EntityManager.AddComponent(entity, new MyComponentData());

// 获取Entity的ComponentData
MyComponentData myData = EntityManager.GetComponentData<MyComponentData>(entity);

// 设置Entity的ComponentData
EntityManager.SetComponentData(entity, myData);

// 在运行时从Entity动态移除Component
EntityManager.RemoveComponent<MyComponentData>(entity);

// 是否存在拥有指定Component的Entity?
bool has = EntityManager.HasComponent<MyComponentData>(entity);

// Entity是否alive?
bool has = EntityManager.Exists(entity);

// 实例化Entity
var instance = EntityManager.Instantiate(entity);

// 销毁这个实例
EntityManager.DestroyEntity(instance);


EntityManager也提供了批处理的API来一次创建或销毁多个Entity,这些API显然更快,出于性能考虑,我们应该尽可能地使用这些API。

// 实例化500个Entity并它们的ID写到instances这个数组
var instances = new NativeArray<Entity>(500, Allocator.Temp);
EntityManager.Instantiate(entity, instances);

// 销毁这500个实例
EntityManager.DestroyEntity(instances);


5,内存块(chunk)的实现详情


每个Entity的ComponentData都储存于我们称之为内存块(chunk)的地方。ComponentData以流的方式布局,意味着:所有类型为A的Component,都被紧紧地包裹在一个数组里面。紧跟着的是包裹着B类型Component的数组,再紧跟着的是包裹着C类型Component的数组,以此类推。


每个内存块总是链接到一个具体的EntityArchetype上,从而在一个块里的所有Entity都遵循完全相同的内存布局。在遍历Component时,块内Component的内存访问总是完全线性的,并没有浪费加载到缓存行中。这点必须要保证。


ComponentDataArray本质上是一个迭代器,它能够遍历全部与所需Component兼容的EntityArchetype。就访问EntityArchetype来说,就是去遍历所有与它相匹配的块;就访问块来说,就是去遍历所有在那个块里的Entity。


译注:原文<ComponentDataArray is essentially an iterator over all EntityArchetypes compatible with the set of required components; for each EntityArchetype iterating over all chunks compatible with it and for each chunk iterating over all Entities in that chunk.>。


一旦一个块里的Entity都被访问过了,我们就找到下一个匹配的块,然后访问那里面的那些Entity。


当Entity被销毁了,我们就把别的Entity移到它的位置去,然后再更新Entity表。
如上文所说,要对Entity的线性布局做出有力保证,所以这么做是必需的。Component数据在内存中移动的代码也经过了高度优化。


译注:Unity官方ECS样例:Unity-Technologies/EntityComponentSystemSamples

所以,Entity、Component只相当于纯粹的数据,一点逻辑处理都没有。写逻辑处理的部分的是System和Job。第二部分的“System、Job和多线程处理”的部分会进行阐述,这一部分有点复杂,很多地方我也没有搞懂作者在说些什么(假如作者自己知道自己在说什么的话),为了避免误人子弟,这一部分的译文过几天再出。。。

编辑于 2018-05-22

文章被以下专栏收录