## 一、逻辑与表现分离 ①、逻辑层仅处理数据,准备好用于渲染的数据。 ②、表现层对已有的数据,进行插值表现,且不会改动数据。 ③、两者的帧率可以不同。 ## 二、时间相关 ①、客户端通过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、物理模拟、动作、自定义库等。 使用了字典,遍历顺序不一致。 多线程操作数据先后,执行结果处理先后。 “我”的开发角度,使得逻辑倾向于其中一方。 工具类、单例类有未纳入序列化的数据。 ## 十、定点数 ①、选择合适精度,这涉及计算性能与内存开销。 ②、开方,快速近似查表代替循环。 ③、反三角函数,查表代替泰勒展开(循环)。 ## 十一、多世界 ①、尽量不要使用有状态(数据)的单例类。 ②、可以存在无状态(数据)的静态类,工具类,辅助类等。 ## 十二、其他 ①、帧同步中,没有秒的概念,只有帧的概念,秒的相关数据都需要转换成帧。 ②、逻辑帧中的延迟模块,一定是由逻辑帧去轮询,比如自定义协程、延迟模块等。 ③、客户端和服务器都有帧缓冲,客户端快照也会有缓冲,以减少内存使用。 ④、预测帧数高时,预输入帧数可对应降低。