返回

文章详情

不是每个字节都有投票权

Hacker News2026年6月2日 07:13

在一个确定性的游戏引擎中,重放的开始很简单:记录输入,重新运行相同的步骤,然后比较结果。当我开始为模拟设置重放时,我的第一个直觉是:简单。对所有内容进行哈希处理。在最初的几个字段中,这感觉是对的。角色的生命值、弹丸的位置、随机数状态:只要其中任何一个不同,那运行结果可能就会分歧。然后,明显不那么直观的字段逐渐增多。人工智能有一个追踪,解释它为什么向左转。渲染器有来自先前帧的插值状态。路径寻找有一个满是方向的缓存。一个结构体有填充字节,因为内存就是内存。天真的校验和最终变成这样:哈希( & world.entities ); 哈希( & world.projectiles ); 哈希( & world.rng ); 哈希( & world.ai_trace ); // 啊哦 哈希( & world.render_helpers ); // 确实是啊哦 那种校验和将每个字段视为同样有意义的。它捕捉到真实的分歧,但它也将无害的实现变化转变为重放失败。使这一点显而易见的错误很小。我改变了一个用于检查的辅助字段,而重放校验和变了。模拟的行为仍然是一样的。玩家最终到了同一个地方。相同的敌人被消灭。但校验和却说不。重放失败,因为调试数据具有不同的布局。因此,问题变得更窄:哪个状态可以改变未来的游戏玩法? 玩家生命值 是, 弹丸位置 是, 随机数流 是, 调试事件 可能不是,它们是观察。 渲染插值 否, 有用,但不是游戏玩法真实。 路径寻找缓存 也许,是保存还是重建,但要指明是哪一个。关于路径寻找缓存,我想要一个明确的决策。它要么是在AI可以读取它之前重建,要么被持久化并视为重要的运行时状态的一部分。我想要避免的是缓存仅仅因为校验和碰巧经过它而漂流进重放。这篇文章概述了我最终在一个Zig ARPG引擎中使用的划分:权威游戏状态,派生缓存,观察/调试输出,以及表现状态。这些名称是本地的,但让每个字段选择一个角色已经很有用。确定性仍然需要常见的工作:固定的步骤、明确的随机数生成器、稳定的迭代顺序、初始化状态,以及没有对渲染时序或本地机器状态的隐性依赖。校验和只能告诉我两个运行是否达到了同样的权威状态。步骤提供了重放的固定检查点。模拟在固定的步骤中推进,而不是渲染帧。外部步骤函数主要安排阶段:模拟.zig pub fn tick ( self : * Simulation , sim_input : Input , maybe_tick_events : ?* TickEventQueue , ) void { assert ( self.world.phase == .idle ) ; self.world.assert_idle_phase_queues_drained() ; self.world.ai_trace.clear() ; self.run_ingress() ; self.run_control() ; self.run_derive() ; self.run_plan ( sim_input , maybe_tick_events ) ; self.run_apply ( maybe_tick_events ) ; self.run_cleanup ( maybe_tick_events ) ; self.world.tick_count += 1 ; self.world.transition_to ( .idle ) ; self.world.assert_idle_phase_queues_drained() ; if ( builtin.mode == .Debug ) { self.world.validate() ; } } 我喜欢这个函数,因为它有无聊的边界:闲置 -> 入口 // 接收排队的世界/会话变化 -> 控制 // 更新控制状态 -> 派生 // 在决策之前重建派生事实 -> 计划 // 将输入和AI转化为计划工作 -> 应用 // 提交移动、物理、战斗 -> 清理 // 退役逐步遗留物 -> 闲置。 重放需要这种无聊的顺序。每个步骤都从闲置状态开始,排空它期望排空的队列,以固定顺序运行系统,时间增加一次,然后返回闲置。这为校验和提供了一个循环中测量的具体点。如果在各个阶段之间有队列泄漏,下一个阶段可能会读取属于前一个阶段的命令。如果在错误阶段中系统突变了状态,就更难以解释哪个步骤导致了哪个结果。如果世界没有返回到闲置,那么下一个步骤将开始于未完成的工作已经加载。步骤边界规定了允许发生工作的地方。重放记录输入。对于这个重放设置,文件记录了输入内容。如果重放文件存储了“火球命中了18”,重放是在检查一个记录的答案,而不是检查模拟。契约是:种子 + 输入带 -> 步骤 -> 相同的权威结果。录音器故意做得很小:replay.zig pub const Recorder = struct { inputs : [ recording_ticks_max ] Input = undefined , count : u32 = 0 , seed : u64 = 0 , pub fn push ( self : * Recorder , input : Input ) void { if ( self.count >= recording_ticks_max ) { @panic (

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡