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