Files
obsidian-notes/3Resources/游戏开发/帧同步总结.md
2025-06-20 09:51:50 +08:00

5.0 KiB
Raw Blame History

一、逻辑与表现分离

①、逻辑层仅处理数据,准备好用于渲染的数据。

②、表现层对已有的数据,进行插值表现,且不会改动数据。

③、两者的帧率可以不同。

二、时间相关

①、客户端通过Ping包方式预测服务器的战斗时间。

②、RTT计算方式,发一个Ping包,包含发送时的本地时间戳服务器接收到以后返回这个时间客户端接收到后用最新的本地时间戳减去这个时间就可以得到RTT。

③、RTT计算时服务器还会返回一个服务器的战斗时间取最低RTT的一半加上服务器返回的战斗时间即可以估算为战斗时间。

④、多次模拟,越来越接近服务器的战斗时间。

三、预输入

①、非锁步同步中客户端先于服务器运行比如快个3帧。

②、理想状态下,服务器因为有收包缓冲在,每帧都能收到每个客户端的输入。

③、但网络延迟高时,客户端发送的输入,可能超过了服务器的收包缓冲。

④、所以基于RTT的计算在延迟高时

客户端会增大预输入的帧数比如原本发送超过服务器3帧的输入改成6帧。

服务器也会增大收包缓冲,在收到最新的同帧号输入时,会替换之前的输入。

缓冲可以动态调整。

⑤、一般预输入只处理预测失败概率低的操作,比如持续移动。

⑥、客户端冗余发送

四、预测

①、客户端先行于服务器,所以需要预测其他客户端的行为。

②、一般基于连续输入原则,比如连续的移动输入等。

③、对于高敏感操作,一般不进行预测。

五、回滚

①、在本地帧输入数据与服务器发送过来的帧输入不一致时,发生回滚。

②、找到最近可用的快照,进行推进。

③、逻辑层回滚技巧:

基于帧索引的可序列化数据,比如随机数状态,属性等。

针对于大型的,序列化成本较高的数据,可记录每帧变动的指令,在回滚时,逐帧逐条回滚这些指令。

④、表现层回滚技巧:

表现层一般设计为可插值,而非多帧状态累加。

有一些效果,可等到逻辑帧确认后再表现。

插值回滚,而不是瞬间表现到位。

⑤、ECS回滚参考

Entity/Component不增删时拷贝整个Entity/Component的数据块。

Entity删除时可标记为禁用等确定不回滚后再删除增加时操作记录回退。

Component增删时操作记录回退。

六、追帧

①、落后太多时,可选择最近目标帧的快照进行处理。

②、一般会设置当前帧可用于追帧的时间,避免追帧卡顿。

七、快照

①、较大的间隔生成全量快照比如1秒。

②、较小的间隔生成增量快照,记录基于全量快照的变动。

③、OOP中一般通过标签或者内建数据结构类实现序列化、反序列化、数据变动记录。

④、ECS中一般关注Entity数量、Component数量和数据即可。

⑤、序列化一般使用自定义二进制,且数据一般为整数,如果对大小有要求,可以参考整数压缩方案。

八、日志

①、常规日志,比如输入,行为,随机数,属性等。

②、自动日志,大致实现思路:

遍历所有涉及逻辑的代码文件,通过正则表达式,查找到所有的有值类型参数的方法。

生成记录代码包括宏开关记录方法ID(方法名经过映射后的ID),参数数量,每个参数大小的代码和记录值的泛型接口代码。

③、哈希相关:

每次记录可算哈希,每帧可算哈希,多帧哈希可叠加。

哈希计算是可能会有冲突的,尽可能的降低计算成本与哈希冲突。

④、不一致定位,定位哈希不同的帧和定位同帧下的不同步点。

九、不同步

①、基于日志定位。

②、可能的原因:

使用了浮点数相关的模块比如内置数学API、物理模拟、动作、自定义库等。

使用了字典,遍历顺序不一致。

多线程操作数据先后,执行结果处理先后。

“我”的开发角度,使得逻辑倾向于其中一方。

工具类、单例类有未纳入序列化的数据。

十、定点数

①、选择合适精度,这涉及计算性能与内存开销。

②、开方,快速近似查表代替循环。

③、反三角函数,查表代替泰勒展开(循环)。

十一、多世界

①、尽量不要使用有状态(数据)的单例类。

②、可以存在无状态(数据)的静态类,工具类,辅助类等。

十二、其他

①、帧同步中,没有秒的概念,只有帧的概念,秒的相关数据都需要转换成帧。

②、逻辑帧中的延迟模块,一定是由逻辑帧去轮询,比如自定义协程、延迟模块等。

③、客户端和服务器都有帧缓冲,客户端快照也会有缓冲,以减少内存使用。

④、预测帧数高时,预输入帧数可对应降低。