实体(Entity):标识符的本质
当我们谈论 ECS(Entity-Component-System)时,“E”即代表 Entity(实体)。对于习惯了面向对象编程(OOP)的开发者来说,实体是最容易被误解的概念。在 OOP 的世界里,实体往往被等同于“游戏对象(GameObject)”——一个包含了各种属性和方法的庞大实例。
但在 ECS 架构下,实体绝对不是对象(Object)。
1. 实体不是对象,只是一个 ID
Section titled “1. 实体不是对象,只是一个 ID”在纯粹的 ECS 设计中,实体(Entity)没有任何数据(比如没有坐标、没有血量),也没有任何逻辑(不能调用 entity.DoSomething())。
实体,仅仅是一个全局唯一的无符号整数(Unsigned Integer ID)。
在 C++ 或 C# 中,它的底层定义通常就像这样简单:
// 实体只是一个类型别名或一层极薄的包装public struct Entity { public uint Id;}当你“创建”一个实体时,ECS 框架在底层并没有为你分配一块用于存储对象数据的堆内存,它仅仅是向你返回了一个当前可用的整数编码(例如 1001)。
你可以将实体看作是关系型数据库(如 MySQL)表中的主键(Primary Key)。主键本身并不存储用户的姓名或年龄,它只是一个索引,用来在不同的数据表中查询属于该用户的具体信息。
2. 实体作为黏合剂
Section titled “2. 实体作为黏合剂”既然实体身上不挂载任何数据,那是如何表示一个“兽人”或一辆“汽车”的呢?
在 ECS 中,数据被拆分到了组件(Component)中(下一章会详细阐述)。实体的作用,就是将内存中各处离散的组件概念上“黏合”在一起。
例如,当我们说“实体 1001 是一辆汽车”时,在底层发生的实际情况是:
- 坐标组件数组中,索引关联到 1001 的位置存有
(x, y, z)。 - 速度组件数组中,索引关联到 1001 的位置存有
(vx, vy, vz)。 - 渲染组件数组中,索引关联到 1001 的位置存有汽车轮胎的模型引用。
实体本身是一层极轻的抽象,它向外界提供了一个统一的标识,用来查询所有“附加”在它身上的数据。
3. 代(Generation)与实体复用
Section titled “3. 代(Generation)与实体复用”由于在大型游戏或模拟中,实体的创建和销毁极为频繁(例如机枪射出的子弹)。如果 ID 一直递增,不仅可能导致整数溢出,还会导致底层数组的索引变得极其庞大和稀疏,浪费内存空间。
因此,所有成熟的 ECS 框架(包括 EnTT, Unity DOTS 的底层机制)都会复用实体 ID。当一个实体被销毁后,它的 ID 值会被回收到一个对象池中,等待下一次创建时复用。
但这带来了一个致命的风险:悬空引用(Dangling Reference)。
假设逻辑 A 记录了玩家的追踪导弹锁定目标为实体 1005。在此期间,实体 1005(一架敌机)被击毁了。框架回收了 1005 这个 ID。接着,系统立刻生成了一只小鸟,并分配了刚刚空闲的 ID 1005。此时,追踪导弹依然锁闭着 1005,结果却飞向了一只无辜的小鸟。
为了解决这个问题,ECS 实体通常不仅仅包含索引值,还会包含一个代(Generation)或版本号(Version)。一个 32 位的实体 ID 经常被拆分为两部分:
- Index(如低 20 位):用于作为底层数组查询数据的绝对索引。
- Generation(如高 12 位):每次该 Index 被复用时,Generation 就加 1。
// 一种常见的 Entity 内部结构拆分public struct Entity { public uint id; // 32位无符号整数
public uint Index => id & 0x000FFFFFu; // 取低 20 位 public uint Generation => (id & 0xFFF00000u) >> 20; // 取高 12 位}有了代(Generation),判断一个实体是否仍然存活就变得极度安全且廉价: 当通过保存的 Entity(Index: 5, Generation: 1) 去系统查询时,系统只需对比当前框架记录中 Index 5 的最新 Generation 还是不是 1 即可。如果最新 Generation 是 2(说明被复用过),则意味着原本想要引用的实体早已销毁,当前实体已无效。
4. 总结:轻量级的标签
Section titled “4. 总结:轻量级的标签”在深入理解 ECS 时,第一步就是将对“对象”的执念转化为对“数据和标签”的认知:
- 实体不包含游戏对象的状态。
- 实体的创建和销毁不再意味着昂贵的堆内存分配与垃圾回收(GC),仅仅是数字的递增与回收。
- 实体的本质,是一组组件集合的寻址凭证。
理解了实体的“虚无”,我们就可以去看看真正承载“物质”的容器:组件(Component)。