影子回放不是录屏
竞速、跑酷计时、滑雪、飞行穿环这类 Phaser 游戏很适合加入影子回放。玩家看到自己的最佳路线或好友路线,会更容易进入“再快一点”的循环。很多人第一反应是录制每一帧角色位置,然后回放时照着画。这个方案能很快工作,但数据量大、难压缩、难验证,而且一旦物理或地图版本变化,回放就可能穿墙。更好的思路是把回放当作一段可重放的输入或状态轨迹,而不是视频。
影子回放的目标有三个层级。第一层是本地观赏:玩家能看到过去的自己。第二层是成绩证明:排行榜成绩能附带一段可验证的轨迹。第三层是异步竞技:玩家能和好友影子同场比赛。不同层级要求不同。如果只是本地观赏,可以记录位置采样;如果涉及排行榜,就要考虑确定性、版本和反作弊。本文以 Phaser 2D 竞速为例,讲清楚输入采样、状态采样和混合方案的取舍。
输入回放和状态回放的取舍
输入回放记录玩家每一帧或每个 tick 的输入,比如左、右、加速、刹车、跳跃。回放时重新跑一遍模拟,得到角色位置。优点是数据小,能验证成绩;缺点是要求模拟确定性。浏览器浮点、不同帧率、物理引擎更新顺序都可能造成偏差。状态回放记录角色位置、速度、朝向等结果。优点是回放稳定;缺点是难验证,数据更大,也容易被伪造。
实用方案通常是混合:排行榜上传输入和关键状态校验点,本地展示使用压缩后的状态轨迹。服务端或客户端验证时用输入重放,检查每隔一段时间的位置 hash 是否接近;展示时直接插值状态轨迹,避免物理差异导致影子抖动。对于纯单机小游戏,可以只做状态轨迹,但也要记录地图版本和游戏版本。
flowchart TD
A["玩家比赛开始:seed / mapVersion / physicsVersion"] --> B["InputRecorder 每 tick 记录输入"]
B --> C["Simulation 更新车辆状态"]
C --> D["CheckpointSampler 记录位置校验点"]
C --> E["GhostTrackSampler 记录展示轨迹"]
D --> F["成绩提交:输入流 + 校验点"]
E --> G["本地影子:压缩轨迹"]
F --> H["Verifier:重放输入并比对校验点"]
G --> I["GhostRenderer:插值显示半透明影子"]
固定 tick 比帧率更可靠
如果要输入回放,模拟应该使用固定 tick,比如每秒 60 次或 30 次。Phaser 的 update(time, delta) 可能因为帧率波动传入不同 delta,如果直接用它更新物理,回放就不稳定。可以在 Scene 中累积 delta,然后按固定步长推进模拟。渲染层可以插值,但模型层每次只走固定时间。Arcade Physics 自带更新节奏,但你仍然要控制输入采样和自定义逻辑的 tick。
固定 tick 还让输入数据更容易压缩。每个 tick 记录一个位掩码:左、右、加速、刹车、跳跃。连续相同输入可以用 run-length encoding 存储,比如“加速+右保持 18 tick”。一局 90 秒、60 tick,每 tick 一个字节也只有 5400 字节,压缩后更小。相比每帧记录浮点位置,输入流轻得多。
记录开始条件
回放不只是一串输入。它还需要开始条件:地图 id、地图版本、物理参数版本、随机种子、角色配置、车辆升级、开始时间、客户端版本。如果这些条件不同,同一输入会跑出不同结果。尤其是竞速游戏,哪怕加速度从 420 改成 425,旧回放都会偏。版本不一致时,客户端应该标记“旧版本回放,仅供观看”或使用状态轨迹展示,而不是拿它参与验证。
如果游戏允许皮肤但不影响碰撞,皮肤可以不进入验证条件;如果某个轮胎升级影响抓地力,就必须进入条件。把影响模拟的配置和纯表现配置分开,是回放系统可信的前提。
一个输入录制器
下面的代码用位掩码记录输入,并做简单连续压缩。真实项目可以再加 checksum、二进制编码和 Base64。
const enum InputBit {
Left = 1 << 0,
Right = 1 << 1,
Accelerate = 1 << 2,
Brake = 1 << 3,
Jump = 1 << 4,
}
interface InputRun {
mask: number;
ticks: number;
}
export class InputRecorder {
private runs: InputRun[] = [];
private lastMask = -1;
sample(keys: Phaser.Types.Input.Keyboard.CursorKeys, extraJump: boolean) {
let mask = 0;
if (keys.left?.isDown) mask |= InputBit.Left;
if (keys.right?.isDown) mask |= InputBit.Right;
if (keys.up?.isDown) mask |= InputBit.Accelerate;
if (keys.down?.isDown) mask |= InputBit.Brake;
if (extraJump) mask |= InputBit.Jump;
const last = this.runs[this.runs.length - 1];
if (last && mask === this.lastMask) {
last.ticks += 1;
} else {
this.runs.push({ mask, ticks: 1 });
this.lastMask = mask;
}
}
export() {
return this.runs.slice();
}
}
采样要发生在固定 tick 前,而不是渲染后。移动端触摸输入也可以转换成同样的 mask。模拟层不关心输入来自键盘、手柄还是触摸,它只消费当前 tick 的控制状态。这样同一套回放可以跨平台展示。
影子渲染要避免干扰玩家
Ghost 视觉通常使用半透明、低饱和度、无碰撞的 Sprite。它应该在玩家附近可见,但不能遮挡当前玩家。可以给影子加轻微残影或轮廓,但不要使用和危险物相同的颜色。影子音效默认关闭,除非是教学回放。多人好友影子同时出现时,只显示最相关的一个或两个,否则画面会变乱。
回放插值很重要。状态轨迹可以每 100ms 记录一次位置和朝向,渲染时按当前比赛时间在两个采样点之间插值。这样数据量小,移动也平滑。若影子跑到当前玩家屏幕外,可以暂停渲染或只显示边缘指示,节省性能。
验证不是万能反作弊
如果成绩有排行榜,客户端上传的回放不能完全信任。服务端可以重放输入并检查完成时间、路径和校验点,但 Web 游戏中的物理完全一致并不总是容易。一个折中方案是客户端提交输入流、校验点和最终成绩,服务端做基础验证:输入长度与成绩时长匹配,速度不超过上限,关键检查点顺序正确,位置误差在阈值内。高分榜前列可以做更严格复算或人工抽查。
不要把影子回放当成绝对反作弊。它更适合提高作弊成本和提供复盘证据。真正涉及奖励的榜单,还需要服务端权威结算或至少服务端签名的关卡配置。纯客户端 Phaser 游戏可以做到“可信 enough”,但不能承诺完全安全。
版本兼容和数据迁移
每条回放都要记录 replayVersion。当输入编码、物理参数或地图格式变化时,旧回放需要迁移或降级。状态轨迹通常更容易兼容,因为它只是展示路径;输入回放更依赖模拟。可以保留多个解码器,或者在版本差异过大时只允许观看不允许验证。玩家的最佳成绩不要因为更新后旧回放无法验证就直接删除,应保留成绩来源和版本说明。
本地存储也要控制容量。只保留每张图的最佳、最近若干局和收藏回放。轨迹数据可以压缩后放 IndexedDB,不要全塞 localStorage。排行榜影子可以按需下载,进入赛道前先加载概要,真正开始比赛时再拉详细轨迹。
上线前检查清单
确认模拟使用固定 tick;确认回放记录地图版本、物理版本、角色配置和随机种子;确认输入采样和模拟更新顺序固定;确认本地展示轨迹经过压缩和插值;确认 Ghost 没有碰撞,不遮挡玩家关键视野;确认排行榜提交包含输入流和校验点;确认旧版本回放有降级策略;确认存储容量受控;确认调试工具能播放、暂停、逐 tick 前进和导出回放。
影子回放做得好,会让玩家看到“我差在哪里”,也会让排行榜更有说服力。Phaser 完全能承载这类系统,但不要把它做成录屏替代品。记录输入、固定模拟、压缩轨迹、标明版本,回放才既轻量又可信。
赛道检查点让回放更可解释
竞速游戏通常有检查点,不只是为了防止抄近路,也为了验证回放和分析路线。每条回放可以记录通过检查点的 tick、速度和偏移。展示影子时,UI 可以告诉玩家“你在第二个弯比最佳慢 0.18 秒,在第三个跳台快 0.07 秒”。这比单纯看终点成绩更能驱动练习。Phaser 中可以把检查点做成不可见 Zone,模型层记录通过顺序,表现层只在需要时显示分段成绩。
检查点还可以帮助处理版本差异。地图微调后,如果检查点 id 保持,旧回放至少能用于分段参考;如果检查点顺序变了,就应该标记为不兼容。不要只用地图文件 hash 一刀切。对于小改动,比如背景装饰或碰撞外的视觉更新,旧回放仍然可以观看;对于赛道宽度、跳台角度、加速带位置变化,旧输入回放就不应参与排行验证。
影子数据的隐私和社交边界
好友影子看起来只是路线数据,但它可能暴露玩家习惯和昵称。上传前要明确玩家是否同意公开最佳回放。排行榜可以默认上传成绩,但影子轨迹最好有开关,至少在隐私设置里能关闭。下载好友影子时,也不要一次拉取大量数据。先展示成绩列表,玩家选择挑战某个好友时再下载对应轨迹。这样节省流量,也减少不必要的数据暴露。
如果游戏面向儿童或校园场景,社交影子还要避免自由文本。影子名称可以使用系统昵称或好友关系,不要允许在回放里附加任意留言。Phaser 客户端只负责展示,但产品规则要提前定清楚。技术上,影子文件可以只包含 userId 映射后的展示名、车辆外观 id 和轨迹,不包含设备信息、输入原始时间戳之外的敏感字段。
调试回放偏移的方法
回放系统最常见的 bug 是“跑着跑着偏了”。排查时不要盯着终点看,要逐段比较。开发模式可以同时播放输入重放结果和状态轨迹,二者颜色不同;每隔一秒显示位置误差;误差超过阈值时暂停并输出最近输入、速度、碰撞面和随机数状态。这样可以定位是第几个 tick 开始发散。常见原因包括:某个粒子或表现对象意外参与碰撞,随机数在回放时多调用一次,浮点 delta 使用了真实帧时间,地图碰撞层版本不一致。
一旦定位到发散点,要优先修模拟边界,而不是放大误差容忍。容忍阈值太大,验证就失去意义;阈值太小,又会误伤正常设备。对于 Web 游戏,建议用固定 tick、离散输入和关键检查点共同降低不确定性。影子展示可以宽松,成绩验证必须保守。
玩家视角的回放控制
影子系统还需要给玩家足够控制。比赛中可以选择显示自己的最佳、好友最佳或全服纪录,也可以完全关闭影子。回放透明度、名称显示和轨迹线都应可配置,尤其在小屏手机上,过多信息会影响当前操作。结算页则可以提供分段对比、重看最后十秒和保存本局影子。不要把所有能力都塞进比赛中,比赛时要少打扰,复盘时才展开细节。
对于教学关卡,官方影子可以作为“示范路线”,但要避免让玩家误以为只有一种解法。示范影子可以在关键点放慢或显示输入提示,普通竞速影子则保持真实速度。两类回放使用同一套渲染器,但元数据不同:教学回放带说明标签,成绩回放带时间和版本。这样功能复用,语义也清楚。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。