基本的Unity GameObject

传统的Unity GameObject是很消耗性能的,我们先看一下传统GameObject的结构:

UnityEngine.GameObject

  • 属于C#类
  • UnityEngine.Component各种实例的容器
  • 永远包含一个Transform Component
  • 可以作为其他GameObject的子级

UnityEngine.Component

  • 属于C#类
  • 内部包含Update()等一些需要在特定游戏生命周期触发的函数。
  • 会引用很多managed object(包括GameObjects、其他components还有assets等)

UnityEngine.GameObjectUnityEngine.Component都是managed object,不能够在Burst编译器内应用,在大部分情况下也不能应用Jobs。

创建和销毁GameObjects和它们的Components开销很大,因为Managed Object在内存中分散存在,而不是像在Array中整块儿存在,在Unity生命周期中这些GameObjects和它们的Components在内存中不断被访问,这样并不高效,还会导致出现缓存未命中的性能问题。

基本的Entity

为了避免上述情况,我们使用Entity代替GameObjects

Unity.Entities

  • 是一串整数ID
  • 和一系列components相关联(只能关联一种component type,例如关联一个component,它声明为Foo,这个Entity只能关联一个Foo,再加一个是不行的)
  • 没有内置的父子级概念。

Unity.Entities.IComponentData

  • C# 结构体
  • 也许通过ID引用entities,但ID不是managed object,像float、int都不是managed object
  • 大部分情况下,component和Entity内部没有methods,它们都是纯数据。但这也并不是严格限制
  • 在UnityECS中,Entities和components会紧密地在arrays里面打包,对内存更好,这样查询(Query)它们的数据会非常高效。

注意:IComponentData是可以应用在class上的,这种component就能够包含managed object了,但这种component就会有上面说的性能问题

基本的Component声明

1
2
3
4
5
6
7
8
using Unity.Mathematics;
using Unity.Entities;

public struct Movement : IComponentData
{
public float3 Direction;
public float Speed;
}

World概念解释

World是entities的容器,每个World的Entity都有一个对于当前World唯一的ID,但是如果这个Entity在其他的World中可能会有相同的ID,这一点请注意,不要把ID对应的World搞错了。

在大多数的情况下,一般只需要一个World,不过创建更多的World在需要逻辑分离的时候很有用,Unity的DOTS Netcode网络开发包为每个服务器和每个客户端都创建了不同的World

Unity Multiplayer Networking | Unity Multiplayer Networking (unity3d.com)

为了管控Entities和components,World提供了EntityManager:

  • CreateEntity()
  • DestroyEntity()
  • AddComponent()
  • RemoveComponent()
  • GetComponent()
  • SetComponent()

Entities和components的存储方式

Archetype

每一个World的Entities都会被分成几类Archetype,每个拥有相同components组合的Entity就会归为一类Archetype

Chunk

Chunk是每一类Archetype里存储各个entity和components的块,这些块的大小是均匀一致的

示意图:

Archetype和Chunk

这是一个拥有三类Archetype的World

Archetype A B C指的是同时包含有component A、component B、component C的entities,这些entities和components分成了4个chunk

Archetype A B 指的是同时包含有component A、component B的entities,这些entities和components分成了2个chunk

Archetype A C指的是同时包含有component A、component C的entities,这些entities和components分成了3个chunk

使用当前World的EntityManager在一个entity中add或者move component,Unity会隐式转换这个entity对应的Archetype。这些增删component的操作会间接导致Archetype和Chunk的改变,我们称之为“structural change operations”,简单地读写component并不是“structural change operations”

Chunk的结构

这是Archetype A B C的Chunk结构

Chunk结构

每一个Chunk内部的entities数量和每个entity的component的数量和Size有关,每个Chunk最大可以有128个entities,超过了这个数量就会创建新的Chunk。

Chunk内部由entity和component各自的数组组成,Chunk内部的排序规则是:第一个空槽必须被填上。也就是说,如果上图中index0的entity被移除,index1和index2就会向前补位,index1的entity就会变成index0,index2的entity就会变成index1。

需要注意的是,虽然entity和component各自的数组有相同的index,但是component各个数组的size可能是不同的

Size of component

Entity metadata Array

World的Entity manager通过“Entity metadata Array”来让我们通过Entity ID来查找Entity,每个Entity ID都指出了自己所在metadata Array Index,即Entity ID Index,此metadata Array[Entity ID index]存储了一个指针和一个Index,该指针指向存储了这个Entity的Chunk,而Index则是这个Entity在Chunk中的Index。如果该Chunk在对应的Index没有Entity,那么metadata Array[Entity ID index]指向这个Chunk的指针就会成为null。

通过Chunk的结构我们知道,一个Entity销毁了以后,剩下的部分Entity在Chunk内部的Index就会更新,也就是说,当前被销毁的Entity在Chunk内部的index会被重复利用。为了不出问题,在metadata Array[Entity ID index]中还存储了一个Version Number,当Entity被销毁,它在Entity metadata Array中的Version就会改变(增量),所以当一个Entity ID的Version Number和metadata Array中的不一致时,就表示这个Entity被销毁了或者不存在。

Entity metadata Array

Query

Query可以帮助我们高效地按照要查询的component种类来轮询对应的Chunks。

例如,如果我们创建了一个Query,这个Query想要轮询具有component A和component C的Chunks,那么Archetype A B C和Archetype A C都会轮询到。

当然我们也可以指定一个Query,轮询只包含component A和component C的Chunks,并不包含component B,那么只有Archetype A C会轮询到。

Query

JobChunk和JobEntity

当需要用到轮询的操作,Job System就能够登场了。Unity Entities提供了IJobChunkIJobEntity接口,前者用于创建轮询chunk的Query Job(在chunk内部,job System也会处理里面的entities和它们的components),后者用于创建轮询entities的Query Job,属于直接轮询entities,并不经过chunk。

大体上讲IJobChunk并不方便使用,但是它显式提供了更底层的控制方法。

IJobEntity更加方便,它依赖Source Generation来减少模板代码(Boilerplate),不过它在某些用例中可能不适用,必须使用IJobChunk

Baking

Entities并不能在Unity scene环境中工作,必须创建自己的Subscene,Baking可以在运行时转换一个GameObject,把它变成序列化的Entity,以供在Entities Subscene中使用。

一个GameObejct在Bake成为Entity之前可以通过Baking System进行一些可选的处理。

我们可以通过继承一个Baker类来将GameObject下的Component转化为Enitity下的component。