《Cocos 游戏联机实战》4.1 本地预测
从服务端权威带来的操作延迟感讲起,用一条完整时间线说明本地按下到服务端权威状态到达之间发生了什么,交代输入序号与「未确认输入队列」的设计,划清可预测与不可预测动作的边界,并给出 predict 模块的 L1 纯 TS 实现与「100 ms 延迟下本地位置立即变化」的 vitest 断言。
category
从服务端权威带来的操作延迟感讲起,用一条完整时间线说明本地按下到服务端权威状态到达之间发生了什么,交代输入序号与「未确认输入队列」的设计,划清可预测与不可预测动作的边界,并给出 predict 模块的 L1 纯 TS 实现与「100 ms 延迟下本地位置立即变化」的 vitest 断言。
前两节解决了「发什么」,这节解决「发多小」。先把 60 Hz 状态同步的每帧字节数按条件推出来,再逐条过五种压缩手段:位打包、量化定点、变长整数、字段掩码、增量基线。配纯 TS 的位读写与量化实现和 vitest 断言,最后讲量化误差怎么在表现层补回来。
上一节把「收快照、叠状态、写节点」的链路打通,但默认每帧发全部实体。AOI 要回答的是:怎样只同步客户端看得见的东西。本节对比四种空间划分方案的代价,给出带滞回带的视野判定与「进出视野差集」的纯 TS 实现加 vitest 断言,并说明《方块冲刺》双人竞速里 AOI 的真实价值在哪。
上一章用帧同步把确定性锁死,但它要求所有客户端跑同一份输入流。本章换到状态同步:服务端定期下发权威状态,客户端只负责表现。本节拆开全量快照与增量 diff 的取舍,用纯 TS 写一套实体表加字段比对生成 diff、接收端 patch 应用的算法,配 vitest 断言,并给出《方块冲刺》的快照消息格式与基线确认机制。
输入迟到或丢包时,等会卡、跳帧会失步,回滚才是帧同步的正解。本节对比三种策略,给出状态快照环与回滚重演的纯 TS 实现、内存代价估算,用 vitest 断言预测错误时回滚后状态与直接重算逐位一致,并讲清追帧的步数上限与「追不上就重连」的判据。
帧同步的每一帧都要集齐所有玩家的输入才能推进。本节讲输入如何采集、量化为位掩码、打包广播并按帧对齐:给出输入延迟窗口的估算方法、基于序号的去重,以及依赖抽象 Transport 的输入缓冲与帧推进器纯 TS 实现,并用假传输层断言乱序到达仍按帧号对齐。
帧同步的前提是同一输入必须得到逐位相同的状态。本节解释浮点为何跨端不一致(超越函数实现各异、随机源与遍历顺序不同),给出 Q16.16 定点数的完整纯 TS 实现与 vitest 断言,并列出确定性陷阱清单,最后说明《方块冲刺》里哪些量必须定点、哪些可留浮点。
本节讲两端时钟为什么对不齐(Date.now 不同源与单调时钟的意义),给出 NTP 式时钟偏移估计的 L1 实现——多次采样、取最小 RTT 样本作权威偏移、滑动平均平稳化,并用假时钟断言偏移收敛;同时讲 RTT 与抖动计算、延迟预算怎么切(单向延迟 / RTT / 处理延迟),以及这些估计值在 4.2 和解、5.2 超时、9.3 延迟补偿里怎么被复用。
本节讲联机协议怎么选:对比 WebSocket 上的 JSON、MessagePack 二进制与自定义紧凑二进制的取舍,给出用 const 对象加 as const 定义消息类型(禁用 enum)的方式,用 DataView 实现读写定点数、变长整数与字符串的 L1 编解码器并用 vitest 做往返断言,附《方块冲刺》的 INPUT / SNAPSHOT / ROOM_STATE 消息格式表。
本节建立客户端侧联机的心智模型:讲清「权威」为什么默认在服务端、客户端只能决定输入与表现而不能决定胜负与伤害,对比 C/S、P2P、C/S 中继、主机制四种拓扑并说明《方块冲刺》双人竞速的选择,给出传输/会话/同步/表现四层与 assets/scripts/net/ 目录的对应,并写出 Transport 接口与可注入延迟丢包的假传输层完整实现。
本节给出引擎升级的完整决策链:先判断该不该升,再列差异面清单,用两份 cc.d.ts 做 API 变更核对,按冒烟清单与卷二 13.3 的帧预算做回归验证,最后落到锁 tag、分支与回退方案。文末提供一个纯 TypeScript 的导出符号差异比对器,可在升级前先跑一遍。
本节把引擎定制拆成两条路:不改代码的模块裁剪与改代码的实现级修改。前者讲项目设置里的功能裁剪面板、engine.json 与引擎侧 cc.config.json 的 features 依赖闭包,后者以 2D 框架的 UITransform 源码为例给出改前改后对照,并说明编译、运行时守卫与回退的完整流程。
本节从 GitHub cocos/cocos-engine 的 v3.8.8 tag 拉取源码讲起,覆盖仓库四层目录、native external 第三方库的三种获取方式、Node 与 gulp 依赖安装、编辑器偏好设置里的自定义引擎接入,以及 TypeScript 引擎与原生引擎各自的编译路径和产物 bin 目录,并给出一个可类型检查的运行时版本核对脚本。
全卷收官:把从引擎分层、渲染管线、Effect、光照、物理、动画到内存、原生、定制、架构与跨引擎选型的线索收成一张全景图,按四个部分回顾每条主线解决的问题与交付的能力,给出一段可直接单测的能力矩阵数据结构与一段可类型检查的自检组件,列出继续深入的四条方向与不同读者的下一步,最后回望从「会用」到「懂」这段路。
把跨引擎对照落成可操作的选型方法:先问三个前置问题,再按目标平台、团队栈、包体与 3D 需求走决策树,用一段可直接单测的加权打分逻辑把直觉量化;随后拆解迁移成本的分项口径与参考系数,比较全量重写、逐模块替换、双引擎并存三种策略,并给出一段可类型检查的平台能力探测脚本,最后列出「什么时候不该换引擎」的判断清单。
把前 10 章讲的 Cocos 机制放进横向坐标系:对照 Cocos Creator、Godot、Phaser、Defold 四款引擎在对象模型、脚本语言、2D/3D 侧重、导出目标与生态上的核心差异,给出生命周期、资源加载、合批与物理的逐项对照表,并用一段可类型检查的组件脚本和一段可直接单测的引擎无关状态机,说明横向对照该用在哪、不该用在哪。
规范靠自觉必然失效,必须钉进工具。本节把 ESLint、TypeScript 严格模式与对 cc.d.ts 的类型门禁组成三道静态检查:用 no-restricted-imports 守住 core 不碰引擎、用严格编译项挡住空值与索引越界、用 tsc --noEmit 校验每个 cc 符号,最后给出三道门禁在 CI 里的落点与失败判定。
大工程最先崩的不是性能而是依赖关系。本节把 assets/scripts 的四层职责深化成可强制的依赖方向,讲清 Bundle 与代码模块的对应、平台层如何用接口隔离三端差异,并给出一个纯 TypeScript 的循环依赖扫描器与 vitest 用例,把「core 不许 import cc」这类纪律从口头约定变成会变红的检查。
Cocos 是组件-节点模型而非纯 ECS。本节先把两者的结构差异摆清楚,再划出「数据驱动的纯逻辑层」与「交给引擎的视图层」的边界:用纯 TypeScript 写一个 SoA 结构的数据层并配 vitest 断言,再用一层薄组件把它接回节点与预制体,最后给出该用与不该用的工程判据。
主线程为什么是瓶颈、哪些活能挪出去、挪出去要付什么代价。本节讲清 Worker 的数据边界——结构化克隆的复制成本与 Transferable 的零拷贝,用可跑的纯 TS 代码量化两者差异;说明 cc.d.ts 并不导出 Worker,必须走平台全局对象并做特性检测;最后给出分片计算任务与并行化的工程判断。