这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
《Cocos 游戏联机实战》4.3 插值与外推
远端玩家怎么动得平滑:讲清渲染延迟为什么故意把远端状态回放晚一个缓冲时间,对比插值(稳但有延迟)与外推(跟手但丢包会跳),交代快照缓冲与时间轴对齐、外推的衰减与上限,给出插值器与外推器的 L1 纯 TS 实现,并用 vitest 断言「抖动输入下输出连续」「丢包时外推不发散」。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
远端玩家怎么动得平滑:讲清渲染延迟为什么故意把远端状态回放晚一个缓冲时间,对比插值(稳但有延迟)与外推(跟手但丢包会跳),交代快照缓冲与时间轴对齐、外推的衰减与上限,给出插值器与外推器的 L1 纯 TS 实现,并用 vitest 断言「抖动输入下输出连续」「丢包时外推不发散」。
收到服务端快照后客户端该怎么处理:对比直接接受、平滑插值与回滚重演三种做法,讲清回滚重演的完整流程与误差阈值如何避免「橡皮筋」,给出 reconciler 的 L1 纯 TS 实现与「丢包 30% 下预测位置仍收敛」「误差超阈值触发硬切」的 vitest 断言,并估算每帧重演的 CPU 代价。
从服务端权威带来的操作延迟感讲起,用一条完整时间线说明本地按下到服务端权威状态到达之间发生了什么,交代输入序号与「未确认输入队列」的设计,划清可预测与不可预测动作的边界,并给出 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 接口与可注入延迟丢包的假传输层完整实现。
命令不能直接改文档的内部结构,否则脏标记与重绘会被绕过。本节把命令接到第 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 真跑。