实体迁移与 Offload
BigWorld 的实体迁移不是把对象 memcpy 到另一个进程。它是 Real/Ghost 状态转换、脚本回调、属性序列化、Channel 迁移、Ghost sign-off、buffered messages 和 Cell 边界控制共同完成的状态机。
先给结论
Cell 实体 offload 的核心路径:
- Cell 周期性执行
checkOffloadsAndGhosts()。 OffloadChecker遍历 real entities。EntityGhostMaintainer判断实体位置属于哪个 home cell。- 确认目标 CellApp channel 健康并已准备 ghost。
Cell::offloadEntity()调用onLeavingCell。Entity::offload()向目标 CellApp 写onload消息。Entity::convertRealToGhost()写 real 数据、设置nextRealAddr_、销毁 real part。- 目标 CellApp 读取 real 数据,ghost 转 real。
- 旧 real 通知其他 ghosts 下一个 real 地址。
- Ghost 播放 buffered messages。
关键源码:
Cell::checkOffloadsAndGhosts()创建OffloadChecker,见 cell.cpp。OffloadChecker::run()遍历 real entities 并发送 offloads,见 offload_checker.cpp。EntityGhostMaintainer::checkEntityForOffload()依据Space::pCellAt()和目标 Channel 状态判断是否迁移,见 entity_ghost_maintainer.cpp。Cell::offloadEntity()调用onLeavingCell并把 real 变 ghost,见 cell.cpp。Entity::offload()写CellAppInterface::onload并调用convertRealToGhost(),见 entity.cpp。Entity::readRealDataFromStreamForOnloadInternal()按 offload 相同顺序读 real data,见 entity.cpp。
Offload 触发条件
实体不是一跨边界就立即迁移。EntityGhostMaintainer::checkEntityForOffload() 会检查:
Space::pCellAt(position.x, position.z)找到 home cell。- home cell 不能是当前 cell。
- home cell 不能处于 delete pending。
- 目标
CellAppChannel必须存在。 - 目标 channel 必须
isGood()。
源码见 entity_ghost_maintainer.cpp。
这避免把实体迁移到不可用或正在删除的 Cell。
Ghost 维护顺序
OffloadChecker::run() 不是只检查是否跨边界。它在 Space 没有 shutdown 时遍历本 Cell 的 realEntities(),每个 real entity 交给 EntityGhostMaintainer::check(),最后统一 sendOffloads(),见 offload_checker.cpp。
EntityGhostMaintainer::check() 的顺序很关键:
- 如果
Cell::shouldOffload()为 true,先执行checkEntityForOffload(),可能把pOffloadDestination_填好并加入 offload list。 markHaunts()把当前 real entity 的所有 haunts 标记为待检查,同时记录目标 offload Cell 是否已有 ghost。createOrUnmarkRequiredHaunts()按ghostDistance + appealRadius构造 interest area,遍历Space::visitRect()命中的 Cell。visit()对已有 haunt 清 mark;对新需要的 Cell 调addHaunt()和Entity::createGhost()。- 如果正在 offload,只允许在目标 Cell 上创建 ghost,避免给非目标 Cell 创建多余 channel。
deleteMarkedHaunts()删除仍被 mark 的过期 ghosts,但受maxGhostsToDelete()、new real 保留时间和minGhostLifespanInTicks()限制。
源码见 entity_ghost_maintainer.cpp、entity_ghost_maintainer.cpp 和 entity_ghost_maintainer.cpp。
所以“目标 CellApp 已准备 ghost”不是口头约束:源码用 doesOffloadDestinationHaveGhost || numGhostsCreated_ == 1 断言目标 ghost 已存在或本轮刚创建,见 entity_ghost_maintainer.cpp。
Offload 状态机
stateDiagram-v2 [*] --> RealOnSource RealOnSource --> GhostPreparedOnDest: create/unmark haunt GhostPreparedOnDest --> OffloadQueued: addToOffloads OffloadQueued --> LeavingSource: onLeavingCell LeavingSource --> StreamingToDest: write onload + real data StreamingToDest --> GhostOnSource: convertRealToGhost StreamingToDest --> RealOnDest: onload + read real data GhostOnSource --> AwaitGhostSetReal AwaitGhostSetReal --> GhostFollowingNewReal: ghostSetNextReal RealOnDest --> [*]
这不是单步操作,而是跨进程、多消息、多对象状态转换。
写入顺序与读取顺序
Entity::convertRealToGhost() 中,如果有目标 channel:
- 写 real data 到 stream。
- 设置
pRealChannel_。 - 设置
nextRealAddr_。 - 调用
offloadReal()删除 real part。 - 删除 real-only properties,只留下 ghost + real shared 属性。
源码见 entity.cpp。
实际写流发生在 Entity::writeRealDataToStream() 和 Entity::writeRealDataToStreamInternal():
- 先写
EntityID。 - 再写
teleportFailure = false。 - 用
internalNetworkCompressionType()包一层压缩流。 - 写 real-only properties,也就是
propCountGhost()到propCountGhostPlusReal()之间的属性。 - 写 token
RealProps。 - 调
RealEntity::writeOffloadData()写 RealEntity 状态。 - 写 exposed-for-replay 的 base properties。
源码见 entity.cpp 和 entity.cpp。
RealEntity::writeOffloadData() 继续写:
- channel 状态。
- velocity、topSpeed、topSpeedY、physicsCorrections。
shouldAutoBackup_。- 当前
spaceID和isTeleport。 - haunt 地址列表,并且如果当前 CellApp 地址不在 haunt 里,会强制追加当前地址,因为源端 offload 后会变成 ghost。
controlledBy_、profiler、real controllers、periodsWithoutWitness_、recordingSpaceEntryID_。- 如果有 Witness,写标记
'W'并写 witness offload data;否则写'-'。
源码见 real_entity.cpp。
目标端读取时,readRealDataFromStreamForOnloadInternal() 注释明确要求按照 offload 相同顺序读取:
- 先 Entity。
- 再 script 相关数据。
- 再 real 数据。
源码见 entity.cpp。
目标端具体顺序:
Entity::onload()从GHOST_ONLY的 raw varlen 消息里读teleportFailure,先触发onEnteringCell,再调用convertGhostToReal(),见 entity.cpp。convertGhostToReal()禁止普通回调,清掉指向旧 real 的pRealChannel_,再readRealDataFromStreamForOnload(),见 entity.cpp。readRealDataFromStreamForOnloadInternal()先补齐 real-only properties,校验RealPropstoken,再createReal()并调用RealEntity::init(..., CREATE_REAL_FROM_OFFLOAD, ...),见 entity.cpp。- 读取 exposed-for-replay base properties 后,启动 real controllers,见 entity.cpp。
convertGhostToReal()把实体加入 Cell 的 real 列表,调用relocated(),再用 high-priority callback buffer 触发onEnteredCell,见 entity.cpp。
这说明 wire format 和状态转换强耦合,不能随意调整字段顺序。
脚本回调顺序
Cell offload 会触发脚本回调:
- 源 Cell:
onLeavingCell。 - 源 Cell 转为 ghost 后:
onLeftCell。 - 目标 Cell:
onEnteringCell。 - 目标 ghost 转 real 后:
onEnteredCell。
源码注释见 entity.cpp。
文档里必须强调源码注释中的提醒:这些回调“Think twice before using”,很多情况应该用更好的数据类型避免。
原因是回调能执行脚本,脚本可能销毁、teleport、再次 offload 或修改状态。
Channel 与 nextRealAddr
Entity::convertRealToGhost() 会把 nextRealAddr_ 设置成目标 CellApp 地址,见 entity.cpp。
RealEntity::destroy() 在 offload 时会通知所有非目标 ghosts:
- 发送
ghostSetNextReal。 - 告诉 ghost 新 real 的地址。
- reset/destroy 当前 real channel。
源码见 real_entity.cpp。
Entity::ghostSetNextReal() 设置 nextRealAddr_ 后,会播放 buffered messages,见 entity.cpp。
目标端 RealEntity::readOffloadData() 读取 haunt 地址列表后,会对每个有效 ghost 发送 ghostSetReal,告诉它新的 owner 地址和 numTimesRealOffloaded,见 real_entity.cpp。
源端和其他 ghost 的握手顺序是:
- 源 real 在
convertRealToGhost()中设置nextRealAddr_,然后offloadReal()。 RealEntity::destroy( pNextRealAddr )给所有非目标 haunt 发送ghostSetNextReal,这是当前 real 发给这些 ghosts 的最后一条消息。- 目标 real 创建后,
readOffloadData()给保留的 ghosts 发送ghostSetReal。 - ghost 收到
ghostSetNextReal后,只接受来自nextRealAddr_的后续 GHOST_ONLY 消息,并播放该地址对应的 buffered subsequence。 - ghost 收到
ghostSetReal时,如果numTimesRealOffloaded不是期望值,会把这条消息作为新 subsequence 延迟,等前序 offload 完成。
源码见 real_entity.cpp、entity.cpp 和 entity.cpp。
这正是 Mercury indexed channel version 的业务背景:offload 后旧 real、新 real、缓冲包可能交错到达,必须用 owner 地址、offload 次数和子序列边界识别消息来源。
Buffered ghost messages
BufferedGhostMessages 按 EntityID 管理缓冲,再按来源 Mercury::Address 分 queue:
BufferedGhostMessages::add()把消息放进某实体某来源队列,见 buffered_ghost_messages.cpp。playSubsequenceFor(entityID, srcAddr)只播放当前应该接收的来源地址队列,见 buffered_ghost_messages.cpp。BufferedGhostMessagesForEntity::playSubsequence()如果找不到当前来源队列,会保留其它来源队列,不会错误播放,见 buffered_ghost_messages_for_entity.cpp。BufferedGhostMessageQueue::playSubsequence()会播放到下一个 subsequence end 为止;如果播放到ghostSetNextReal,可能递归触发下一来源队列,见 buffered_ghost_message_queue.cpp。delaySubsequence()要求第一条消息是 subsequence start,并把旧的 unexpected subsequence 一起延迟,见 buffered_ghost_message_queue.cpp。
BufferedGhostMessages::isSubsequenceStart() 把 ghostSetReal 视为子序列开始,isSubsequenceEnd() 把 ghostSetNextReal 和 delGhost 视为子序列结束,见 buffered_ghost_messages.hpp。
Space::createGhost() 也参与乱序处理:如果同 ID 实体已经存在,或该实体从同来源已有缓冲消息,就把 createGhost 包装成 BufferedCreateGhostMessage,而不是直接创建或覆盖,见 space.cpp。
消息入口处还有一层保护:
EntityMessageHandler::handleMessage()对GHOST_ONLY消息,如果实体不存在、shouldBufferMessagesFrom(srcAddr)为 true、或同来源正在 delay,就创建 buffered message,见 message_handlers.cpp。- 如果
GHOST_ONLY消息明显发给已销毁 ghost,例如当前实体已经是 real、或当前实体正在 offload 到该来源但消息不是 subsequence start,会丢弃并回失败,见 message_handlers.cpp。 REAL_ONLY消息到达时,如果本进程没有 real,会查 ghost 上缓存的 real channel 或 population 的 real channel 并转发,见 message_handlers.cpp。Entity::shouldBufferMessagesFrom()的规则很直接:real 和 zombie ghost 不缓冲;如果设置了nextRealAddr_,只接受 next real;否则只接受当前 real,见 entity.cpp。
这几层共同保证:旧 real 的尾包、新 real 的首包、ghost 创建包、ghost 删除包可以乱序到达,但不会直接按到达顺序破坏实体生命周期。
Offload 与 Ghost 的关系
OffloadChecker::sendOffload() 注释说,单个 offload 受 ghosting capacity 和 channel 状态约束,见 offload_checker.cpp。
虽然当前函数实现只是调用 cell_.offloadEntity(),但前置的 EntityGhostMaintainer 已经保证目标条件。
换句话说:
- Ghost 不是 offload 的附属品。
- Ghost 是 offload 安全执行的前置条件之一。
Cell 删除边界
Cell 删除也依赖 offload 完成。
Cell::isReadyForDeletion() 要求:
- buffered entity messages 为空。
- buffered input messages 为空。
isRemoved_为 true。- real entities 为空。
- space entities 为空。
- pending ACK 为空。
源码见 cell.cpp。
这说明 BigWorld 不会在迁移消息未清空时粗暴删除 Cell。
Teleport 分支
普通 offload 是相邻 Cell 之间的权威迁移;teleport 允许跨空间或跨远端 CellApp,因此源码走了单独消息 CellAppInterface::onloadTeleportedEntity,见 cellapp_interface.hpp。
源端 RealEntity::teleport() 的关键差异:
- 如果 mailbox id 为 0,只在本地改位置并调用
onTeleportSuccess(NULL),不走 offload。 - 远端 teleport 会先调用
onLeavingCell,再向目标 CellApp 写onloadTeleportedEntity。 - 该消息先携带 nearby entity id,再携带 createGhost 数据长度和 ghost 数据。
- 源端会调用
Cell::offloadEntity(..., isTeleport=true),此时Cell::offloadEntity()不再重复调用onLeavingCell。 - 源端临时改 local position/direction 写入 offload 数据,发送后恢复旧位置,避免源端 ghost 状态被错误位置污染。
源码见 real_entity.cpp 和 real_entity.cpp。
目标端 CellApp::onloadTeleportedEntity() 会把一条复合消息拆成两条内部消息:
- 用 nearby entity 的 space id 补齐 createGhost stream。
- 调
CellAppInterface::createGhosthandler,先创建 ghost。 - 调
CellAppInterface::onloadhandler,把 ghost 转 real。 - 找到新实体后调用
onTeleportSuccess( nearbyEntity )。
如果 nearby entity 不存在,目标端丢弃 createGhost 数据,并把剩余 onload 数据回送给源端,设置 teleportFailure=true。源端 Entity::onload() 看到 teleport failure 后会移除失败目的地 haunt,并调用 onTeleportFailure,见 cellapp.cpp 和 entity.cpp。
与 Base Offload 的区别
本章主要讲 Cell entity offload。BaseApp 也有 Base offload,用于 BaseApp retiring、备份和恢复。
Base 侧有:
Base::backupTo()Base::writeBackupData()Base::offload()Base::readBackupData()
见 base.hpp。
两者区别:
- Cell offload 是空间权威迁移。
- Base offload 是会话/实体长期部分在 BaseApp 间迁移。
- 两者都依赖 Mercury、BinaryStream、EntityDef 和 Mailbox。
源码取舍
BigWorld 的 offload 机制复杂,但源码里的复杂性主要来自这些约束:
- Real/Ghost 是同一 Entity 在不同 CellApp 上的不同权威状态,不能直接 memcpy。
- 目标端必须先有 ghost,才能把 ghost 转 real;否则 real-only 属性、controller、witness、history 和 channel 都缺上下文。
- Wire format 和读写顺序强耦合,
RealPropstoken 只是开发期校验,不是 schema 演进机制。 - 脚本回调被插在迁移关键点,源码必须用
callbacksPermitted(false)和 high-priority callback buffering 缩小破坏窗口。 - Ghost 消息只能保证单个 CellApp channel 内有序,跨旧 real 和新 real 的消息要靠 subsequence 拼接。
- Cell 删除必须等 buffered messages、real entities、space entities 和 removal ACK 全部清空。
这套设计换来的是在线边界迁移、AoI 连续和 CellApp 负载均衡,但代价是状态机难以局部推理。阅读或修改源码时,应优先保护消息顺序、回调顺序和 Real/Ghost 生命周期边界,不要先抽象成“对象迁移”。
源码验证重点
- 在普通 offload 场景确认源端顺序:
onLeavingCell->Cell::realEntities_.remove()->Entity::offload()->offloadReal()->onLeftCell。 - 检查目标端
onload消息必须打到 ghost:CellAppInterface::onload是GHOST_ONLY,不是普通 CellApp RPC。 - 给
writeRealDataToStreamInternal()和readRealDataFromStreamForOnloadInternal()加 token/golden stream 测试,确保 real-only 属性顺序不变。 - 构造目标 ghost 不存在的情况,确认
EntityGhostMaintainer::check()会在 offload 前创建目标 ghost,或触发断言。 - 构造
ghostSetReal先于ghostSetNextReal到达,确认Entity::ghostSetReal()会 delay subsequence,而不是立刻切换 real channel。 - 构造
createGhost到达时实体仍存在,确认Space::createGhost()会进入BufferedCreateGhostMessage,不会覆盖实体。 - 构造 teleport nearby entity 不存在,确认目标端回送
teleportFailure=true,源端触发onTeleportFailure。 - 删除 Cell 时确认
isReadyForDeletion()的 buffered entity messages、buffered input messages、real entities、space entities、pending ACK 全部为空。
本章边界
本章解释 Cell entity offload。下一章分析持久化与 DB 线程模型,解释实体状态如何落库、备份和恢复。
