很多团队把实体同步问题简单归结为 AOI,但线上游戏里玩家真正关心的不只是距离。一个玩家可能离世界 Boss 很远,却因为加入了讨伐队伍需要持续看到 Boss 血量;一个商会成员不在同一地图,也要看到仓库被谁取走了材料;一个观战者不参与战斗,却需要订阅双方技能和比分。实体兴趣订阅架构就是把“谁需要知道什么”从单纯坐标计算中拆出来,变成可声明、可变更、可限流的服务端机制。
典型场景
假设一款多人动作 RPG 有主城、野外、队伍副本和跨服据点战。玩家进入野外时,需要订阅附近怪物、可采集物、队友位置、附近玩家动作;进入队伍后,还要订阅队友任务阶段和共享目标;加入据点战后,即便死亡回城,也要订阅战场比分和指挥标记。如果所有消息都通过地图服广播,带宽和 CPU 会迅速失控。如果所有订阅都写死在客户端,又无法防作弊。合理做法是由服务端维护兴趣集合,客户端只提交视角、玩法状态和显式关注对象,最终推送由订阅服务裁剪。
架构示意
flowchart TD
P["Player Session"] --> S["Subscription Service"]
S --> R["Rule Evaluator"]
R --> Z["Spatial Index"]
R --> G["Gameplay Group Index"]
R --> M["Manual Follow List"]
E["Entity Event Bus"] --> F["Fanout Filter"]
S --> F
F --> D["Delta Encoder"]
D --> P
订阅来源要分层,不要只有坐标半径
实体兴趣至少来自四类规则:空间规则、玩法关系、显式关注、系统强制。空间规则处理附近玩家和怪物;玩法关系处理队伍、同公会、同战场阵营;显式关注处理锁定目标、观战对象、交易对象;系统强制处理公告、强制剧情、反作弊抽样。分层之后,一个实体事件进入扇出过滤器时,可以按规则快速判断目标人群,而不是遍历全图玩家。
订阅状态由服务端生成,客户端只提供输入
客户端可以上报视角位置、缩放级别、锁定目标、UI 面板状态,但不能直接声明“我要订阅某玩家全部状态”。服务端需要根据权限、玩法阶段和速率限制生成最终订阅。比如观战者可以订阅技能释放和血量变化,但不能订阅私聊、背包或隐藏属性;敌对阵营可以看到旗帜归属变化,但看不到对方战术标记。
事件要分等级,实体全量不是默认选项
实体事件可以分为存在性、关键状态、表现状态和调试状态。存在性决定客户端是否创建或销毁对象;关键状态包括血量、阵营、位置校正、控制状态;表现状态包括动作、朝向、表情;调试状态只给内部工具。推送时应优先保证存在性和关键状态,表现状态可以合并、降频或丢弃。弱网玩家不需要收到每一次朝向微调,但必须知道目标是否死亡。
恢复机制要覆盖断线、切图和规则变更
订阅集合是动态的,玩家切图、加入队伍、进入战场、打开观战面板都会改变集合。每次集合发生大变更,服务端应发送订阅版本号,并提供一个快照点。客户端如果发现增量版本断档,应请求当前订阅范围内的快照,而不是要求地图服重放所有历史事件。这个机制能显著降低断线重连后的复杂度。
限流不能只按玩家,要按实体和规则双向保护
热门实体会制造扇出热点。例如世界 Boss 每秒血量变化和状态变化会推给大量玩家,队伍共享目标也可能在大型活动中频繁变化。系统要同时限制单玩家接收速率、单实体扇出速率和单规则命中规模。对于热点实体,可以把血量改成固定频率聚合推送,把低优先级表现事件只发给近距离玩家。
关键设计取舍
| 维度 | 架构处理 | 重点风险 |
|---|---|---|
| 空间订阅 | 附近实体创建、移动、销毁 | 半径、格子、视野方向 |
| 关系订阅 | 队友、公会、阵营共享信息 | 权限和玩法阶段 |
| 显式订阅 | 锁定目标、观战对象 | 频率和字段裁剪 |
| 系统订阅 | 剧情、公告、强制状态 | 优先级和打断策略 |
落地检查清单
- 订阅规则按空间、玩法关系、显式关注、系统强制分类
- 实体事件定义等级和可降级策略
- 订阅集合变更必须有版本号和快照恢复路径
- 热门实体有聚合推送和扇出上限
- 观战、敌对、队友等角色的字段权限写入契约
一线排障与复盘建议
这个架构上线后,团队要提前准备几类排障入口。第一是按玩家、业务单号或场景 id 查询完整链路,能看到请求进入、状态变化、关键版本、外部依赖结果和最终响应。第二是按时间窗口查看异常分布,区分是全局配置错误、单分片容量问题,还是少量玩家边界条件触发。第三是保留人工修复入口,但修复入口必须写审计流水,记录修复前状态、修复后状态、操作人、审批单和影响范围。没有审计的手工修复,短期能救火,长期会破坏系统可信度。
容量评估也要贴近玩法节奏,而不是只看平均在线。运营开活动、赛季结算、跨服匹配、周常刷新和主播带队都会让请求集中到很短窗口。压测脚本应模拟重复点击、弱网重试、服务超时、实例重启和消息乱序,不要只跑顺滑路径。对于玩家资产、资格、奖励、处罚这类敏感链路,压测结果里要额外检查幂等流水和最终状态,不只是吞吐量。
上线前可以采用影子模式:生产请求仍走旧逻辑,新架构旁路计算结果并记录差异。差异样本要由服务端、策划和客服一起看,因为有些差异来自旧逻辑 bug,有些来自新规则理解错误。等差异收敛后,再按小区服、低风险玩法或内部账号灰度。灰度期间观察错误码、超时、回滚次数、人工工单和玩家反馈,确认系统在真实噪声下仍然可解释。
订阅数据结构建议
订阅服务可以把每个玩家的兴趣集合拆成多个 bucket:spatialBucket 保存当前位置附近实体集合,relationBucket 保存队友、公会、阵营对象,focusBucket 保存锁定目标、观战目标、交易对象,systemBucket 保存剧情和活动强制对象。每个 bucket 都有版本号、过期时间和优先级。最终推送时不是简单合并,而是按实体 id 聚合权限,计算该玩家对该实体可以看到的字段集合。
实体事件也需要结构化。一个 EntityChanged 事件至少包含 entityId、entityType、eventVersion、positionCell、changedFields、importance、sourceTick。Fanout Filter 先按实体索引找到候选订阅者,再按字段权限裁剪。比如同一条“玩家释放技能”事件,队友可以看到技能目标和增益变化,敌人只看到表现和伤害结果,观战者看到更完整的时间线,普通路人可能只看到动作。字段级裁剪能减少很多后续补丁。
故障案例:大型据点战里的无效扇出
某次据点战压测中,服务器 CPU 使用率很高,但网络出口并未打满。追踪后发现,大量玩家死亡回城后仍然订阅战场内所有单位的移动事件,因为客户端需要显示比分面板,旧逻辑把“关注战场”写成了订阅整个战场实体。结果每次前线玩家移动,回城玩家也收到事件,只是客户端丢弃不用。
调整后,死亡回城玩家只保留战场比分、占点状态、指挥标记和队友复活倒计时订阅,不再订阅普通移动。前线玩家仍按空间规则接收实体事件。这个改动没有减少玩法信息,却把战场扇出量降了一大截。经验是:订阅规则要表达玩家真正需要的信息,而不是沿用某个粗粒度场景标签。
客户端协作边界
客户端应把视角和 UI 意图告诉服务端,例如当前镜头中心、缩放档位、是否打开大地图、是否锁定某个目标。服务端根据这些输入调整推送频率,但不要完全相信客户端。恶意客户端可能声明自己正在观战所有人,试图获取隐藏信息;也可能频繁切换关注目标,制造服务器重算压力。因此服务端要对订阅变更做速率限制,并对某些高权限订阅要求玩法状态证明,例如必须在观战席、同队伍或 GM 权限下才能订阅。
断线重连时,客户端不要假设本地实体仍有效。推荐流程是客户端带上 lastSubscriptionVersion 和 lastEventSeq,服务端判断是否可补增量;如果版本差距太大,直接返回当前快照。快照应包含实体存在性和关键状态,不必包含所有表现字段。这样重连后画面可能少量跳变,但不会出现幽灵实体或已死亡目标继续播放动作。
上线验收指标
兴趣订阅的验收重点是“少发了什么”和“多发了什么”。少发会导致客户端看不到关键状态,多发会浪费带宽并泄露信息。灰度期间可以对同一场景做双轨采样:旧逻辑计算应推送集合,新逻辑计算实际推送集合,按实体类型和字段类型比较差异。空间类差异允许一定比例,权限类差异必须严格审查。
压测脚本要模拟玩家高速移动、频繁打开大地图、锁定目标切换、队伍加入退出、观战席切换和断线重连。每个操作后检查订阅版本是否递增,客户端是否能通过快照恢复。回滚条件包括关键实体丢失、订阅版本断档升高、单实体扇出超过阈值、敌对玩家收到不应看到的字段。尤其是权限泄露,一旦发现要立即停止灰度。
团队协作边界
兴趣订阅需要客户端、战斗、地图和安全团队一起维护字段清单。新增一个实体字段时,不能默认进入所有推送包,而要先回答它属于关键状态、表现状态还是私有状态。客户端需要提前声明哪些界面依赖哪些字段,服务端则给每个字段配置最小订阅权限。安全团队定期抽查敌对玩家、观战者和陌生玩家能看到的字段,防止因为一次表现优化泄露隐藏信息。
策划新增玩法时,也要明确订阅语义。例如“远程支援队友”到底需要队友坐标、血量、技能冷却,还是只需要一个求援标记。把需求拆到字段级,后续带宽优化才不会伤害玩法。
常见误区
第一个误区是把兴趣订阅等同于距离判断,结果队伍、观战、任务共享都被迫绕路。第二个误区是只优化下行带宽,忽略订阅变更本身的计算成本。玩家高速移动时,如果每一帧都重算完整集合,CPU 压力会比网络更早爆。第三个误区是把字段权限留给客户端过滤,这不仅浪费带宽,还会给外挂提供信息来源。服务端必须在发送前完成字段裁剪。
总结
兴趣订阅不是为了追求抽象漂亮,而是为了避免服务器在“所有人都可能关心所有事”的假设里被拖垮。把兴趣来源和事件等级拆清楚,客户端表现会更稳,服务端扩容也更有抓手。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。