Entity System — 实体系统
theseed 实体的核心设计:Base/Cell 双身体模型、结构化内存布局、生命周期管理。
来源源头:BigWorld Base/Cell 分离模型。 参考实现:KBEngine 的轻量 BaseEntity / CellEntity 落地方式。 theseed 新增 PropertyBlock 结构化内存布局。
0.5 引擎实现对照与取舍
BigWorld 是怎么实现的
BigWorld 的实体是 Base / Cell 双身体模型:
- Base 负责长期状态与非实时逻辑
- Cell 负责实时位置、AOI、战斗与视野
- 两者通过 EntityCall 和复制协议协作KBEngine 是怎么实现的
KBEngine 提供了更轻量的 Base / Cell 落地方式:
- BaseEntity / CellEntity 分工明确
- Python 脚本承接业务逻辑
- 结构上更贴近可直接落地的 runtime core优缺点
共同优点:
- 职责边界清楚
- 实时和非实时逻辑自然拆分
- 适合分布式部署
共同缺点:
- 跨身体调用天然是网络边界
- 状态同步和迁移复杂度高于单体实体theseed 的取舍
theseed 继续采用 Base / Cell 双身体模型,
但把属性布局、生命周期和复制边界写得更明确,
避免把脚本对象和运行时对象混成一层。1. Entity 的两副身体:Base 和 Cell
一个逻辑实体(如"玩家")在集群中有两副身体:
Base Entity Cell Entity
┌─────────────────────┐ ┌─────────────────────┐
│ 存在于 BaseApp │ │ 存在于 CellApp │
│ 生命周期长 │ │ 生命周期短 │
│ 持久化到数据库 │ │ 进入空间才创建 │
│ 处理非实时逻辑 │ │ 处理实时逻辑 │
│ - 邮件系统 │ │ - 位置/移动 │
│ - 好友系统 │ │ - 战斗/伤害 │
│ - 背包/装备 │ │ - AOI/视野 │
│ - 公会 │ │ - Ghost │
│ 无空间位置 │ │ 有空间位置 │
└────────┬──────────────┘ └──────────┬──────────┘
│ │
└──── EntityCall 双向通信 ────────────────┘2. Entity 内存布局
KBEngine 的 Entity 是 Python 对象,属性存储在 Python dict 中。theseed 改成结构化内存布局:
cpp
// runtime/Entity.h
class Entity {
public:
// --- 身份 ---
EntityId id() const;
const std::string& entityType() const;
EntitySide side() const; // Base / Cell
// --- 生命周期 ---
EntityState state() const; // Creating / Active / Migrating / Destroying / Destroyed
// --- Base ↔ Cell 连接 ---
EntityCall* baseEntityCall() const; // Cell → Base 的调用句柄
EntityCall* cellEntityCall() const; // Base → Cell 的调用句柄
// --- 属性操作 ---
template<typename T>
T getProperty(PropertyId id) const;
template<typename T>
void setProperty(PropertyId id, const T& value);
bool isPropertyDirty(PropertyId id) const;
void clearDirtyFlags();
// --- 空间(Cell 侧) ---
SpaceId spaceId() const;
const Vector3& position() const;
const Vector3& direction() const;
// --- Witness(Cell 侧,有客户端的实体) ---
Witness* witness() const;
bool isReal() const; // real vs ghost
// --- 脚本对象 ---
ScriptObject* scriptObject() const;
private:
// --- 属性存储(结构化内存) ---
// 不用 Python dict,用紧凑的内存块
struct PropertyBlock {
void* data; // 连续内存块
const EntityDef* def; // 属性定义(描述布局)
DirtyMask dirtyMask; // 脏标记位图
};
EntityId id_;
uint16_t entityTypeUType_;
EntitySide side_;
std::atomic<EntityState> state_;
// Base/Cell 连接
EntityCall* baseCall_;
EntityCall* cellCall_;
// 属性
PropertyBlock properties_;
// Cell 侧
CoordinateNode* coordNode_;
Witness* witness_;
SpaceId spaceId_;
// Ghost 相关
ComponentId realCell_; // 0=自己是real, 否则=real所在CellApp
ComponentId ghostCell_; // 0=无ghost, 否则=ghost所在CellApp
GhostManager* ghostMgr_;
// 脚本
ScriptObject* scriptObj_;
// 控制器
ControllerStack controllers_;
// 定时器
std::vector<TimerHandle> timers_;
};3. PropertyBlock:连续内存属性存储
KBEngine 把所有属性存在 Python dict 中,每次读写都要经过 Python API,性能差。
theseed 的改进:属性用紧凑的内存块存储,脚本通过偏移量直接访问。
cpp
// runtime/PropertyBlock.h
// 编译期确定每个属性的偏移量和大小
// 运行时只需 pointer + offset 即可读写
class PropertyBlock {
public:
// 初始化:根据 EntityDef 分配内存
void init(const EntityDef& def);
// 读写(零拷贝)
template<typename T>
const T& get(PropertyId id) const {
auto& desc = def_->getProperty(id);
return *reinterpret_cast<const T*>(
data_ + desc.offset()
);
}
template<typename T>
void set(PropertyId id, const T& value) {
auto& desc = def_->getProperty(id);
*reinterpret_cast<T*>(data_ + desc.offset()) = value;
dirtyMask_ |= (1ULL << id);
}
// 脏标记
DirtyMask dirtyMask() const { return dirtyMask_; }
void clearDirty() { dirtyMask_ = 0; }
// 序列化(只序列化脏属性)
void serializeDirty(MemoryStream& stream) const;
void serializeAll(MemoryStream& stream) const;
void deserialize(MemoryStream& stream);
// 脚本层访问桥接
// Python/Lua 通过 ScriptPropertyBridge 访问
// 桥接层负责类型检查和转换
private:
char* data_; // 连续内存块
const EntityDef* def_; // 属性定义(描述布局)
DirtyMask dirtyMask_; // 脏标记位图(uint64 或 vector<uint64>)
};与 KBEngine 的对比:
KBEngine:
entity.health = 75
→ Python _tp_setattro
→ Entity::onScriptSetAttribute
→ PropertyDescription::isSameType (类型检查)
→ PyDict_SetItem (存入 Python dict)
→ onDefDataChanged (通知变更)
theseed:
entity.health = 75
→ ScriptPropertyBridge::set("health", 75)
→ PropertyBlock::set<INT32>(health_id, 75)
→ 直接写入内存偏移 + 设置脏标记位
→ 无 Python dict 开销4. Entity 生命周期状态机
createEntity()
│
▼
┌───────────┐
│ Creating │ def 加载 + 属性初始化 + 脚本对象创建
└─────┬─────┘
│ onInitialize()
▼
┌───────────┐
┌────│ Active │──────────────────────┐
│ └─────┬─────┘ │
│ │ │
teleport destroy() migrate()
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Teleporting│ │ Destroying │ │ Migrating │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
▼ ▼ ▼
Active(in new ┌───────────┐ Active(in new
Space/Cell) │ Destroyed │ Process)
└───────────┘5. Entity 创建流程
参考 KBEngine Cellapp::onCreateCellEntityInNewSpaceFromBaseapp:
1. 解析消息流:entityType / entityID / spaceID / hasClient
2. 查找或创建 SpaceMemory
3. 分配 Entity 内存
4. 调用 PropertyBlock::init(entityDef)
5. 调用 PropertyBlock::deserialize(stream) // 从 Base 传来的初始数据
6. 建立 baseEntityCall_
7. 如果有客户端:
a. 建立 clientEntityCall_
b. 创建 Witness
c. 调用 onGetWitness()
8. space->addEntity(entity)
9. entity->initializeEntity() // 脚本层 onInitialize
10. space->addEntityToNode(entity) // 加入 AOI 坐标系统6. Runtime Memory
6.1 对象池
游戏服务器高频分配的对象:
- MemoryStream(序列化缓冲区)
- Bundle(网络消息包)
- Entity(实体)
- CoordinateNode(AOI 节点)
KBEngine 使用对象池(OBJECTPOOL_POINT):
MemoryStream::createPoolObject() / reclaimPoolObject()
Witness::createPoolObject() / reclaimPoolObject()
theseed 继续使用对象池,但改进:
- Arena 分配器(实体相关内存连续分配)
- 水位监控(对象池水位上报到 Metrics)
- 自动扩缩(根据使用模式调整池大小)6.2 Bundle 生命周期
Bundle 是网络消息的载体,生命周期关键:
1. 获取 Bundle
auto bundle = BundlePool::acquire();
2. 写入消息
bundle->newMessage(msgId);
(*bundle) << entityId;
(*bundle) << propertyId;
(*bundle) << value;
3. 发送
channel->send(bundle);
// 注意:发送后 bundle 的所有权转移给 Channel
// Channel 发送完毕后自动归还到池
4. 接收
auto bundle = channel->receive();
// 处理完毕后
BundlePool::release(bundle);