实体模型:Base / Cell / Client
BigWorld 的实体不是单对象模型,而是拆成 Base、Cell、Client 三个权威域。Base 承载会话、数据库和跨 Cell 协调;Cell 承载空间、位置、AOI、物理和 real/ghost;Client 只接收经过 Witness 和带宽调度过滤后的视图。
先给结论
BigWorld 的核心抽象是“同一个游戏实体在不同进程中有不同部分”:
Base:长期在线身份、数据库 ID、Proxy/客户端连接、Cell mailbox、创建/销毁 Cell 实体、写 DB、备份归档。Cell Entity:空间内权威对象,维护位置、方向、Controller、AOI、real/ghost、事件历史和客户端同步。Client Entity:客户端可见投影,通过 Witness、EntityCache、Bundle 和 EntityDef 同步生成。
关键源码:
Base注释说明 Base entity 驻留在 BaseApp,可创建关联 Cell entity,并通过CellEntityMailBox与 Cell entity 通信,见 base.hpp。Base提供createCellEntity()、restoreTo()、cellBundle()、sendToCell()、writeToDB()、backupTo()、offload()等接口,见 base.hpp。Entity注释说明 Cell entity 可为 real 或 ghost,每个实体只有一个 authoritative real,可有 0 到多个 ghost,见 entity.hpp。Entity能访问 base 和 client mailbox,远程调用由.def文件指定,见 entity.hpp。
三层职责
Base
身份、会话、数据库、Proxy、全局逻辑、Cell 引用和跨 Cell 协调。
Cell
空间权威、位置、物理、AOI、real/ghost、Controller 和移动。
Client
只接收可见实体和属性投影,不拥有服务端权威状态。
这种分层不是 MVC,也不是普通 ECS。它是 MMO 分布式状态归属模型。
Base 的职责
Base 继承 PyObjectPlus,是 Python 脚本可见对象。它保存:
EntityIDDatabaseIDEntityType- Cell mailbox
- 当前
SpaceID - UDP channel
- timers
- backup/archive 策略
- destroy/create cell pending 状态
源码见 base.hpp。
Base 核心数据结构
// base.hpp:58 - Base 类定义
class Base: public PyObjectPlus
{
public:
Base( EntityID id, DatabaseID dbID, EntityTypePtr pType );
EntityID id() const { return id_; }
DatabaseID databaseID() const { return databaseID_; }
CellEntityMailBox * pCellEntityMailBox() const { return pCellEntityMailBox_; }
SpaceID spaceID() const { return spaceID_; }
Mercury::UDPChannel & channel() { return *pChannel_; }
bool isProxy() const { return isProxy_; }
bool isDestroyed() const { return isDestroyed_; }
// 关键状态标志
bool isCreateCellPending() const { return isCreateCellPending_; }
bool isGetCellPending() const { return isGetCellPending_; }
bool isDestroyCellPending() const { return isDestroyCellPending_; }
bool hasBeenBackedUp() const { return hasBeenBackedUp_; }
};设计要点:
id_是运行时 EntityID,由 BaseApp 分配,用于进程内快速查找databaseID_是持久化 ID,初始为 0,首次writeToDB()后由 DBApp 分配pCellEntityMailBox_指向 Cell Entity,Base 通过它发送消息到 CellApppChannel_是到 CellApp 的 UDP 通道,承载所有 Base-Cell 通信isProxy_标记是否为玩家代理(有客户端连接),普通 Base 没有客户端
Base 重要方法
writeToDB():写数据库。requestCellDBData():向 Cell 请求持久化数据。backupTo()/writeBackupData():备份到其他 BaseApp。offload():BaseApp 退休或负载迁移时迁出。createCellEntity():创建对应 Cell 实体。setCurrentCell():更新 Cell 实体所在 CellApp 地址。callBaseMethod()/callCellMethod():跨域方法调用。
这说明 Base 是实体长期身份和跨域协调的中心。
Cell Entity 的职责
Entity 也继承 PyObjectPlus,但它驻留在 CellApp,负责空间内权威逻辑。
源码注释列出关键语义:
- real / ghost。
- 位置和方向。
- Controller 驱动的位置变化。
- AOI。
- Client 可见性。
- Base mailbox 和 Client mailbox。
见 entity.hpp。
Entity 核心数据结构
// entity.hpp:138 - Entity 类定义
class Entity : public PyObjectPlus
{
public:
// 位置验证:防止 NaN 坐标
static bool isValidPosition( const Position3D &c )
{
const float MAX_ENTITY_POS = 1000000000.f;
return (-MAX_ENTITY_POS < c.x && c.x < MAX_ENTITY_POS &&
-MAX_ENTITY_POS < c.y && c.y < MAX_ENTITY_POS &&
-MAX_ENTITY_POS < c.z && c.z < MAX_ENTITY_POS);
}
// 实体初始化:real 和 ghost 走不同路径
bool initReal( BinaryIStream & data, const ScriptDict & properties,
bool isRestore,
Mercury::ChannelVersion channelVersion,
EntityPtr pNearbyEntity );
void initGhost( BinaryIStream & data );
// offload 迁移
void offload( CellAppChannel * pChannel, bool isTeleport );
void onload( const Mercury::Address & srcAddr,
const Mercury::UnpackedMessageHeader & header,
BinaryIStream & data );
// ghost 创建
void createGhost( Mercury::Bundle & bundle );
};设计要点:
initReal()和initGhost()是两条完全不同的初始化路径- real entity 从二进制流恢复完整状态(包括属性、控制器、AOI)
- ghost entity 只恢复位置和必要状态,不拥有权威逻辑
isValidPosition()防止 NaN 坐标污染空间系统- offload/onload 是实体迁移的核心,real 变成 ghost,ghost 变成 real
Cell Entity 保存和暴露
position()/direction()。volatileInfo()。isReal()/pReal()。realAddr()/nextRealAddr()。Space/Cell。EventHistory。writeClientUpdateDataToBundle()。addHistoryEventLocally()。offload()/onload()/createGhost()。
见 entity.hpp。
Real 与 Ghost
每个 Cell Entity 有且只有一个 authoritative real,可能有多个 ghost。
ghost 的作用:
- 让邻近 CellApp 提前拥有实体副本。
- 支撑 AOI 跨 Cell 边界。
- 支撑 entity offload 前后消息不中断。
- 支撑 CellApp death 和恢复场景下的状态判断。
real 的职责:
- 拥有权威脚本逻辑。
- 写 real-only 属性。
- 驱动位置、Controller 和事件历史。
- 维护 haunts,也就是 ghost 所在 CellApp。
这就是 BigWorld 与普通分片服务器的差异:它不是简单把坐标落在哪个分片就只在哪个分片存在,而是在边界附近主动维护 ghost。
Mailbox 是跨域引用
Base 可以通过 CellEntityMailBox 与 Cell entity 通信;Cell entity 可以通过 Base mailbox 和 Client mailbox 调用远端方法。
这和 通信抽象与 RPC 直接相关:
- Mailbox 不是对象指针。
- 它封装 EntityID、地址、组件归属和接口方法。
- 方法参数由 EntityDef 的
MethodArgs序列化。 - 传输层由 Mercury Bundle / Channel 承载。
所以 BigWorld 的“对象方法调用”本质是分布式消息。
生命周期回调
Base 侧回调包括:
onDestroyonOnloadonRestoreonGetCellonLoseCellonCreateCellFailureonWriteToDBonPreArchiveonTimer
源码见 base.cpp。
Cell 侧回调包括:
onRestoreonGhostCreatedonGhostDestroyedonWriteToDBonNoiseonSpaceGoneonGetWitnessonLoseWitnessonWitnessedonEnteringCellonEnteredCellonLeavingCellonLeftCell
源码见 entity.cpp。
这些回调是脚本层扩展点,但也是风险点:回调中可能销毁、迁移、offload 实体,所以 C++ 代码经常要防止对象生命周期变化。
跨域方法调用链
Base 与 Cell 之间的方法调用通过 Mailbox 和 Mercury 消息完成。这是 BigWorld 分布式对象模型的核心:一个实体的方法调用可能跨越进程边界。
Cell -> Base 调用链
概述: Cell Entity 需要调用 Base 方法时(如通知 Base 写数据库、请求 Base 创建新 Cell),通过 callBaseMethod() 将调用序列化为 Mercury 消息,发送到 BaseApp。
源码入口: entity.cpp:5255
// entity.cpp:5255 - Cell Entity 调用 Base 方法
bool Entity::callBaseMethod( int methodIndex, BinaryIStream & data,
bool isForGhost )
{
// 1. 查找方法定义
const MethodDescription * pMethod =
pType_->description().base().internalMethod( methodIndex );
// 2. 创建 Base Entity Mailbox
BaseEntityMailBox mailbox( pType_, id_, baseAddr_ );
// 3. 获取输出流并写入方法调用
Mercury::Bundle & bundle = mailbox.startMessage(
BaseAppIntInterface::callBaseMethod );
bundle << methodIndex;
bundle.transfer( data, data.remainingLength() );
return true;
}流程图:
sequenceDiagram participant Cell as Cell Entity participant Mailbox as BaseEntityMailBox participant Bundle as Mercury Bundle participant Channel as UDP Channel participant BaseApp as BaseApp participant Base as Base EntityCell->>Mailbox: callBaseMethod(index, data) Mailbox->>Bundle: startMessage(callBaseMethod) Bundle->>Bundle: 写入 methodIndex Bundle->>Bundle: 写入参数数据 Bundle->>Channel: 发送消息 Channel->>BaseApp: UDP 包 BaseApp->>Base: dispatch callBaseMethod Base->>Base: 执行 Python 方法
详细讲解:
方法索引查找:
methodIndex是 EntityDef 中方法的内部索引,由.def文件编译时生成。Cell 和 Base 共享同一个 EntityDef,所以索引一致。Mailbox 封装:
BaseEntityMailBox不是简单指针,而是封装了EntityID、目标地址(BaseApp 地址)和实体类型。它知道如何路由消息。Bundle 序列化:参数通过
BinaryIStream直接转移到 Bundle,避免额外拷贝。Bundle 内部管理可靠/不可靠语义。异步语义:这是单向消息,Cell 不等待 Base 回复。如果需要回复,Base 会通过
callCellMethod()反向调用。
Base -> Cell 调用链
概述: Base 需要调用 Cell 方法时(如通知 Cell 移动实体、创建子实体),通过 callCellMethod() 将调用序列化,发送到 CellApp。
源码入口: base.cpp:1391
// base.cpp:1391 - Base 调用 Cell 方法
bool Base::callCellMethod( int methodIndex, BinaryIStream & data )
{
// 1. 检查 Cell Mailbox 是否存在
if (!pCellEntityMailBox_)
{
ERROR_MSG( "Base::callCellMethod: No cell mailbox\n" );
return false;
}
// 2. 通过 Cell Mailbox 发送
Mercury::Bundle & bundle = pCellEntityMailBox_->startMessage(
CellAppIntInterface::callCellMethod );
bundle << id_;
bundle << methodIndex;
bundle.transfer( data, data.remainingLength() );
return true;
}流程图:
sequenceDiagram participant Base as Base Entity participant Mailbox as CellEntityMailBox participant Bundle as Mercury Bundle participant Channel as UDP Channel participant CellApp as CellApp participant Entity as Cell EntityBase->>Mailbox: callCellMethod(index, data) Mailbox->>Bundle: startMessage(callCellMethod) Bundle->>Bundle: 写入 EntityID Bundle->>Bundle: 写入 methodIndex Bundle->>Bundle: 写入参数数据 Bundle->>Channel: 发送消息 Channel->>CellApp: UDP 包 CellApp->>Entity: dispatch callCellMethod Entity->>Entity: 执行 Python 方法
详细讲解:
Cell Mailbox 检查:Base 必须有对应的 Cell Entity 才能调用 Cell 方法。如果 Cell 已销毁或未创建,
pCellEntityMailBox_为空。EntityID 传递:CellApp 有多个实体,消息中需要携带
EntityID让 CellApp 路由到正确的 Entity。地址路由:
pCellEntityMailBox_知道目标 CellApp 的地址,这个地址可能因实体迁移而变化。Base 通过setCurrentCell()更新地址。失败处理:如果 CellApp 已死或实体已迁移,消息会丢失。BigWorld 通过 CellApp 死亡检测和 Base 恢复机制处理这种情况。
属性同步到客户端
概述: Cell Entity 的属性变更需要同步到客户端。这不是实时全量同步,而是通过 Witness 和 AOI 过滤后,只发送可见且变更的属性。
源码入口: entity.cpp:2475
// entity.cpp:2475 - Cell Entity 写客户端更新数据
void Entity::writeClientUpdateDataToBundle( Mercury::Bundle & bundle,
bool isFullUpdate )
{
// 1. 写入车辆变更(如果有)
this->writeVehicleChangeToBundle( bundle );
// 2. 遍历事件历史,写入属性变更
EventHistory::iterator iter = eventHistory_.begin();
while (iter != eventHistory_.end())
{
if (iter->isForClient())
{
iter->writeToBundle( bundle );
}
++iter;
}
// 3. 写入 volatile 数据(位置、方向等高频数据)
if (volatileInfo_.hasVolatile())
{
this->writeVolatileDetailedDataToBundle( bundle );
}
}流程图:
sequenceDiagram participant Cell as Cell Entity participant History as EventHistory participant Bundle as Mercury Bundle participant Witness as Witness participant Cache as EntityCache participant Client as ClientCell->>History: 记录属性变更 Cell->>Cell: writeClientUpdateDataToBundle() Cell->>Bundle: 写入车辆变更 Cell->>Bundle: 写入事件历史 Cell->>Bundle: 写入 volatile 数据 Bundle->>Witness: 发送给 Witness Witness->>Cache: 更新 EntityCache Cache->>Client: 同步到客户端
详细讲解:
EventHistory 机制:属性变更不是立即发送,而是记录到
EventHistory。每个事件标记是否需要同步到客户端、是否可靠。Volatile 数据:位置、方向等高频数据使用
volatileInfo_控制同步频率。不同实体类型可以有不同的同步策略。Witness 过滤:只有在客户端 AOI 内的实体才会同步。Witness 管理每个客户端的可见实体集合。
EntityCache 缓存:客户端有
EntityCache缓存已知实体,避免重复同步。只有属性变更才发送。带宽控制:Bundle 内部有带宽限制,单个 tick 内发送的数据量有上限,防止网络拥塞。
实体恢复调用链
当 CellApp crash 后,Base 通过 restoreTo() 恢复 Cell Entity。这是 BigWorld 高可用性的核心机制:即使 CellApp 崩溃,玩家的 Base Entity 仍然存活,可以恢复到新的 CellApp。
概述: CellApp 崩溃时,CellAppMgr 检测到死亡,通知 BaseAppMgr,BaseAppMgr 再通知所有 BaseApp。每个 BaseApp 检查自己是否有 Entity 的 Cell 在死亡的 CellApp 上,如果有,就从备份数据恢复到新的 CellApp。
源码入口: base.cpp:3810
// base.cpp:3810 - Base 恢复 Cell Entity
void Base::restoreTo( const Mercury::Address & addr,
const Mercury::UnpackedMessageHeader & header,
BinaryIStream & data )
{
// 1. 读取备份数据
this->readBackupData( data );
// 2. 检查是否有 Cell Entity
if (!this->hasCellEntity())
{
return;
}
// 3. 创建新的 Cell Entity
this->createCellEntityOn( addr );
// 4. 更新 Cell 地址
this->setCurrentCell( spaceID_, addr );
}流程图:
sequenceDiagram participant CellApp as CellApp (死亡) participant CellAppMgr as CellAppMgr participant BaseAppMgr as BaseAppMgr participant BaseApp as BaseApp participant Base as Base Entity participant NewCellApp as 新 CellAppCellApp->>CellAppMgr: 死亡检测 (心跳超时) CellAppMgr->>BaseAppMgr: 通知 CellApp 死亡 BaseAppMgr->>BaseApp: 广播 CellApp 死亡 BaseApp->>Base: 检查 Cell 是否在死亡 CellApp Base->>Base: 读取 cellBackupData_ Base->>NewCellApp: createCellEntityOn() NewCellApp->>NewCellApp: 恢复 Cell Entity Base->>Base: setCurrentCell(新地址)
详细讲解:
死亡检测:CellAppMgr 通过心跳机制检测 CellApp 死亡。如果 CellApp 超过一定时间没有心跳,CellAppMgr 标记它为死亡。
备份数据:Base 定期通过
backupTo()将 Cell Entity 的状态备份到其他 BaseApp。备份数据存储在cellBackupData_中。恢复时机:Base 收到 CellApp 死亡通知后,立即检查自己的 Cell 是否在死亡的 CellApp 上。如果是,就从备份数据恢复。
新 CellApp 选择:BaseApp 会选择一个存活的 CellApp 来恢复 Cell Entity。选择算法考虑负载、空间位置等因素。
状态恢复:备份数据包含 Entity 的属性、位置、AOI 状态等。恢复后,Entity 在新 CellApp 上继续运行,客户端可能感受到短暂卡顿。
为什么这样设计:
- 玩家不掉线:Base Entity 始终在 BaseApp 上,即使 CellApp 崩溃,玩家连接不会断开。
- 快速恢复:从备份数据恢复比从数据库加载快得多,减少玩家等待时间。
- 透明迁移:对客户端来说,只是感觉到短暂卡顿,不需要重新登录。
为什么这样拆
这种拆分解决了 MMO 的几个问题:
- 玩家连接和空间模拟不一定在同一进程。
- 空间负载可以按 Cell 切分迁移。
- Base 长期存在,Cell 可随位置创建、销毁、迁移。
- 数据库写入由 Base 统一协调,必要时请求 Cell 数据。
- Client 只看 Witness 筛选后的投影,不需要知道完整服务端状态。
这是 BigWorld 的核心设计价值之一。
代价
代价同样明显:
- 一个实体的状态分散在 Base、Cell、Client、DB、Ghost 中。
- 生命周期复杂,创建/销毁/迁移都要跨进程协调。
- Mailbox 可能过期,Channel version 需要处理旧包。
- EntityDef 变更会影响多个域。
- 脚本回调可能改变迁移过程中的对象状态。
- 调试一个实体问题需要跨 BaseApp、CellApp、DBApp、客户端日志。
源码取舍
Base/Cell/Client 三层模型的源码取舍是把不同权威域拆开:
- Base 承担长期身份、数据库协调、Proxy 和跨 Cell 生命周期管理。
- Cell 承担空间、位置、AOI、物理和 real/ghost 状态。
- Client 只接收 Witness 筛选后的实体投影和自身可调用的 exposed 方法。
- DB 只持久化 EntityDef persistent 视图,不保存完整运行时对象图。
源码代价是一个实体问题通常跨多个域传播:Mailbox、Channel、EntityDef、DBID、Cell real/ghost、Client entity cache 都可能参与同一条故障链。
源码验证重点
实体模型测试应覆盖域边界:
- Base-only、Cell-only、Client-visible、Persistent 属性应按 EntityDef 数据域进入对应流。
- 创建 Cell entity 时,Base mailbox、Cell mailbox 和客户端 player 关系应一致。
- Cell 丢失、offload、restore 后,Base 持有的 Cell mailbox 不能指向旧地址。
- Client 方法只能通过 exposed method range 调用,不能绕过 Proxy 直连 CellApp。
- DB 写入应由 Base 协调,必要时请求 Cell persistent 数据。
- Ghost 只应作为跨 Cell 副本和迁移辅助,不能被误当成客户端实体连接。
本章边界
本章解释实体三层模型。下一章继续分析 AOI、Witness 与 Ghost,这是 Client 可见性和跨 Cell 边界一致性的核心。
