一般认为帧同步的录像会更容易实现,因为对于每个客户端,战斗由用户操作命令序列驱动,只需要记录命令序列,在播放录像时再重播这个序列即可。对于状态同步,其实录像的原理和帧同步比较类似,因为驱动服务器执行战斗逻辑的也是用户的命令序列,我们同理可以记录每个服务器帧用户的输入,在播放录像时回放用户的输入,即可复现战斗过程。
复现战斗过程的本质是要做到完全一致的命令序列,在完全一致的时间轴上,在完全相同的环境(相同的环境目前主要是随机性一致)下执行,即可得到完全一致的结果。要注意几个比较核心的问题:
要保证录像和战斗是完全一致的命令序列,需要按顺序记录处理的命令。这个虽然看起来比较简单,但是实际上却比较容易踩坑,要注意接受到用户命令的时间点和真正处理用户命令的时间点之间的差异。
状态同步战斗服务器执行命令序列最好的做法是定帧处理命令,这样就可以在播放的录像的时候将命令序列划分到不同的帧去执行,能够比较容易实现一致的时间轴。反之,如果命令是实时处理的,在复现时对时间精度要求极高,不易实现复现。
所有计时都由唯一的delta_time驱动,如果服务器帧是10帧,delta_time取const 100ms,这里要注意的是delta_time不能获取真实的帧与帧之间的时间差,因为delta_time会有很低的概率出现波动,不是100ms。这一点也是用来保证时间轴一致的。
随机性可控,要保证录像和实际战斗的随机序列的一致,一般使用同样的随机种子即可,如果使用了第三方的库也要注意保证随机一致的问题。这个是保障战斗是在相同的随机环境下执行。
驱动状态同步计算的不仅仅是用户输入。对于我们的游戏的某个版本,为了提高子弹命中和扣血的匹配,所有子弹的扣血由客户端命中后向服务器请求触发,获取扣血结果进行显示。这个子弹命中的命令是实时处理的,这点与我们第一条要求相悖,我们要做到完全同样的时间点执行子弹命中是比较麻烦的一件事,但是好在我们有第二点原则,所有时间都是由delta_time驱动,我们只需要在每帧的用户输入序列执行前执行与上一帧之间的子弹命中命令,即可完成复现。这里有一个假设是,服务器每帧的执行期间不会有子弹命中命令到达,这个我们通过加锁来实现。在游戏后面的版本中,子弹的命中改为由服务器驱动。
保证所有命令的执行不会有并发。
容器的访问顺序问题。不要使用对象直接做key,因为对象的内存地址是不稳定的,这样访问顺序可能会存在不确定性。