模拟层笔记
模拟层笔记
核心文件:
include/lab/sim/StateSnapshot.hinclude/lab/sim/InputCmd.hinclude/lab/sim/InputBuffer.hinclude/lab/sim/StateHistory.hsrc/sim/World.cppsrc/sim/Hasher.cpp
模拟层是整个项目最核心的部分。网络同步、客户端预测、服务端权威、回滚重放和压力测试,最后都依赖同一个模拟入口:
1 | World::Step(cmds, dt) |
1. 模拟层的目标
模拟层要做到:
- 同样初始状态 + 同样输入 + 同样 tick = 同样结果。
- 不依赖 SDL、socket、真实网络。
- 能生成完整快照。
- 能从快照恢复。
- 能被 hash 检查一致性。
- 能被测试直接调用。
这就是确定性模拟的基础。
2. InputCmd:最小输入命令
InputCmd 表示某个玩家在某个 tick 的输入。
字段:
tickbuttonsmoveXmoveY
设计重点:
- 输入必须带 tick。
- 输入只描述玩家意图,不描述最终结果。
- 输入要足够小,适合 UDP 高频发送。
- 输入可以重复发送,服务端按 tick 写入缓冲。
3. WorldSnapshot:状态快照
WorldSnapshot 包含:
tick- 玩家数组
- 子弹数组
mazeSeed- 迷宫宽高
- 迷宫网格
它用于:
- 服务端下发权威状态。
- 客户端回滚恢复。
- 压测保存权威历史。
- hash 对账。
判断一个字段是否应该进入 Snapshot,可以问:
1 | 这个字段是否会影响未来模拟结果? |
如果答案是会,就必须进入 Snapshot。
例如 aimX/aimY 很重要。玩家停下后开火时,开火方向依赖上一次瞄准方向。如果回滚时没有恢复这个字段,子弹方向就可能和服务端不同。
4. World::Step 推进流程
World::Step 做的事情:
- 检查输入数量是否等于玩家数量。
- 检查所有输入 tick 是否一致。
- 确保迷宫存在。
- 设置当前快照 tick。
- 对每个玩家处理 hitstun、移动、摩擦、墙体碰撞、朝向、射击冷却。
- 如果有攻击输入且冷却结束,生成子弹。
- 处理玩家之间 pushbox 分离。
- 推进子弹,处理撞墙和命中。
- 同步子弹和迷宫到快照。
一句话:
1 | World::Step 是唯一的模拟推进入口。 |
5. 玩家移动
玩家移动不是瞬间设置速度,而是向目标速度插值:
1 | targetVx = moveX * speed |
其中:
speed = 4.5friction = 8.0dt = 1 / 60
这样移动会更平滑。
墙体碰撞采用分轴处理:
1 | 先尝试 x 方向移动; |
6. 子弹和命中
攻击输入会生成子弹:
- 子弹 owner 是玩家编号,1-based。
- 子弹有位置、速度、life。
- 子弹从玩家前方生成。
- 子弹撞墙或命中后消失。
命中玩家后:
- 扣 HP。
- 设置
Action::Hitstun。 - 设置
stateTimer。 - 给目标一点击退速度。
注意:
子弹状态必须进入 Snapshot、网络包和 hash。否则子弹命中会造成服务端和客户端状态分叉。
7. 迷宫生成
迷宫由 mazeSeed 驱动,用 DFS carve 方式生成。
服务端 State 里会下发 mazeSeed,客户端用 seed 生成同样迷宫。
但注意:
当前实现使用 std::shuffle 和 std::mt19937。同机测试通常没问题,但跨编译器、跨标准库时最好不要只依赖 seed。更稳的做法是:
- 同步完整迷宫网格。
- 或实现项目自己的确定性随机和 shuffle。
项目的 Snapshot 和 hash 已经包含迷宫网格,这是正确方向。
8. InputBuffer 和 StateHistory
两者都是按 tick 索引的环形缓冲。
核心公式:
1 | index = tick % capacity |
读取时必须校验真实 tick:
1 | slot.tick == requestedTick |
否则环形缓冲覆盖后,可能把旧帧误认为新帧。
用途:
| 结构 | 保存内容 | 用途 |
|---|---|---|
InputBuffer |
每 tick 输入 | 输入冗余、预测、回放 |
StateHistory |
每 tick 快照 | 回滚、hash 对账、压测 |
9. Hasher
Hasher 用于状态一致性检查。
它覆盖:
- tick
- 玩家状态
- 子弹状态
- 迷宫 seed
- 迷宫尺寸
- 迷宫网格
float 字段不会直接按内存 hash,而是做毫米量化:
1 | round(value * 1000) |
这样和网络包里的毫米整数保持一致,避免微小浮点差异造成假 mismatch。
10. 模拟层注意事项
- 新增会影响未来的字段时,要同步改 Snapshot、StatePacket、Hasher、Restore、测试。
World::Step必须使用固定 dt,不要传真实帧耗时。- 不要在模拟层访问 SDL、socket、event loop。
- 不要直接 hash float 内存。
- 不要让随机数在服务端和客户端各自自由推进,随机结果必须可恢复、可同步。
- 回滚恢复时不只要恢复玩家,也要恢复子弹、迷宫、冷却、朝向等隐性状态。
- 玩家 pushbox 后可能需要再次考虑墙体约束,这是后续可改进点。