这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
《Cocos 游戏联机实战》3.3 带宽预算与压缩
前两节解决了「发什么」,这节解决「发多小」。先把 60 Hz 状态同步的每帧字节数按条件推出来,再逐条过五种压缩手段:位打包、量化定点、变长整数、字段掩码、增量基线。配纯 TS 的位读写与量化实现和 vitest 断言,最后讲量化误差怎么在表现层补回来。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
前两节解决了「发什么」,这节解决「发多小」。先把 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 接口与可注入延迟丢包的假传输层完整实现。
可访问性不是上线前的一次性检查,而是可以被数据描述的交互边界。本节把「回声」的焦点顺序、键盘可达、角色与标签、触摸目标尺寸写成纯数据规则,用 TypeScript 实现不依赖 DOM 的检查器,并用真实测试说明正 tabIndex、图标按钮缺名称、小触摸目标分别怎样被拦住。
命令不能直接改文档的内部结构,否则脏标记与重绘会被绕过。本节把命令接到第 8 章的文档模型上:文档是唯一编辑目标,命令经它落值、打脏标记、发变更通知;SetProperty 与 AddNode 两条命令演示属性与结构两种脏标,并附本机真跑结果。
一次「多选后一起改属性」必须整体撤销,一次拖拽滑条刷出的几十条微编辑却该合并成一步。本节实现带原子性的事务(任一子命令抛错就补偿回滚)、按时间窗合并的连续编辑判据,并讨论撤销栈的条数上限与字节预算,全部代码在本机真跑并附 9 条 vitest 用例。
编辑器最让人信任的能力是 Ctrl+Z。本节把一次编辑封装成带 do/undo 的 Command 对象,从零实现命令栈的撤销、重做与合并判定,并用 7 条 vitest 用例把 do/undo/redo、redo 链断裂、同目标合并与栈上限逐条钉死。
关卡怎么存盘才既好读又耐改?本节把 8.2 的场景文档写成《微光岛》的 JSON 关卡格式:节点之间用稳定 uid 互相引用而不是数组下标,文件头带 format 与 version,加载时按版本迁移、遇到更高的版本直接拒绝,并用一组用例真跑往返与迁移。
一份可编辑的场景文档长什么样?本节用扁平节点表存节点树,区分运行期句柄 id 与稳定标识 uid,把「改了一个节点,哪些东西要重算」交给脏标记来回答:任何改动沿父链向上标脏到根,刷新时按祖先先于后代的顺序处理。每次改动还追加一条变更记录,作为第 9 章撤销栈的接缝。
编辑器与运行时到底共用什么、隔离什么?本节用一份共享的场景数据模型,把编辑态与运行态拆成两份实例:进入 Play 时深拷贝文档,运行时只改副本,Stop 后编辑态原样恢复。最后用一组 vitest 用例把快照、暂停与幂等停止在本机真跑一遍,并说明为何场景文档用扁平节点表而不是 ECS。
本节不承诺真正的 HMR,只做「模块边界 + 数据与代码分离 + 状态迁移」的数据路径:可替换模块把状态导出成纯数据加版本戳,替换时沿迁移链升到新版本再灌进新实例。状态迁移逻辑用 vitest 真跑,浏览器里的动态 import 与真实刷新时序标为 L4。
脚本不是魔法,就是一组可选回调。本节把 onCreate / onUpdate / onDestroy 接到 ECS 实体的世代句柄上,用状态机挡住误用,让调度顺序由优先级与挂载次序决定,并把帧内卸载延迟到帧末以免遍历中改表;全部用 vitest 真跑。
组件字段要能被脚本与编辑器读写,就不能靠字符串硬编码。本节把内核的类型描述符接到组件实例上,实现带类型强制、只读标记与隐藏字段的属性绑定,并用 vitest 把类型强制、快照与变更比对在本机真跑一遍,给出可供撤销栈与多选编辑复用的最小变更集。
资产浏览器要把引擎眼里的扁平「路径→资产」解释成一棵可展开的树,关卡编辑则把《微光岛》的关卡数据装进编辑器并校验引用。本节实现资产树构建、类型判定、稳定 GUID、拖放载荷编解码与关卡加载,并在本机真跑 13 条 vitest 用例;拖放交互属 L4,需在浏览器中手动验证。