A Server-Authoritative Architecture for Lag-Compensated Projectile Hits

1.7k 词

在上一篇博客介绍对象池管理子弹的方案中,我们完成了子弹的纯演出化,放弃了子弹的全过程同步,仅保留了子弹的初始状态同步,进而由本地的对象池来进行管理。

但这套方案存在非常明显的问题:

  • 在Ability中使用射线检测进行伤害判断是一种非常粗糙的判定方式,它与子弹的过程性——子弹需要一定时间才能到达目的地,相违背。
  • 单纯由服务器进行伤害判定,对客户端玩家并不友好,尤其考虑到不稳定的网络延迟。

在本篇博客中,我将尝试构建一个具有延迟补偿的子弹碰撞伤害判定。

子弹的功能

在上次重构时,我们将子弹从逻辑+演出,全部降级为纯演出形式,由Cue来通过对象池构造,但此刻,我们必须面对子弹命中与技能的异步性,不能单纯考虑使用射线检测来进行伤害判定。

基于此,我将子弹所承担的功能分为两种情况:

  • 本地客户端:本地控制角色发出的预测性子弹,应包含演出与伤害请求的功能,其中伤害请求是指客户端判定命中后,向服务器发出请求判定,申请权威服务器对角色数值修改。
  • 其他(包含非本地客户端与服务器):纯演出子弹,不应造成任何伤害。

但是,若服务器玩家发出子弹命中其他玩家,则直接造成伤害。

伤害判定的流程

本地客户端子弹在客户端命中其他玩家后,将尝试发出请求。

发出请求的对象是ULagCompensationComponent:这是一个延迟补偿组件,挂载在Character上,子弹命中后,获取Owner,进而调用申请伤害的接口。

接收请求的对象是服务器上对应发起者的ULagCompensationComponent:它将查阅被命中玩家存储的数据,进行伤害判定,若成功,则通过GameplayEffect造成伤害。

对于服务器玩家,命中其他玩家则直接造成伤害。

构造子弹

在上次重构时,我们将子弹的构造全部移入GameplayCue中管理,但与上次不同,由于本地控制玩家的子弹需要承担伤害申请的职责,属于逻辑层面的功能,所以对于本地控制角色的技能释放,将子弹构造从Cue中移到Ability内部来实现,避免职责混乱。

子弹类

引入申请伤害判定接口,当Owner为本地控制玩家时,碰撞到其他敌对玩家,将调用Owner的申请接口,并传递必要的信息:

  • 伤害判定发起者。
  • 伤害判定的目标对象。
  • 子弹的起点。
  • 子弹的终点。
  • 伤害判定时间(需考虑延迟)。
  • 伤害类型Tag. (如果后续有不同的技能造成伤害,由恰巧需要延迟补偿,就可以用这个Tag区分子弹伤害与技能伤害,调用对应的GE_Spec).

引入直接造成伤害的判定接口,当且仅当服务器本地控制的玩家的子弹,才能使用该接口直接造成伤害。

ULagCompensationComponent

数据存储

为了服务器能够回档指定时间的相对位置关系,需要存储角色的碰撞体的空间信息:

  • 存储时的游戏时间。
  • Character关键骨骼的Transform

服务器回档有时间限制,需要定期加入与移除记录的数据,在每一帧填入新的数据,并同时移除超出一定时间的旧数据。

数据查询

数据查询是Victim向进行验证的Instigator提供数据的接口,接收一个命中时间。

假设组件保存了[t1,t2][t1, t2]的数据:

  1. 请求的时间小于t1t1,返回t1t1的数据。
  2. 请求的时间大于t2t2,若误差小,返回t2t2数据,若远远大于,异常,返回空数据(客户端的时间不可能比服务器早)。
  3. 请求的时间处于[t1,t2][t1, t2]
    • 遍历寻找时间所处的首尾切片的插值。
    • 若直接命中,则返回对应插值。
    • 若处于两个快照间隔,则进行插值后返回。

申请与验证

客户端通过ServerRPC,将必要的信息传递给服务器。

服务器接收信息后进行判定:

  1. Victim中的延迟补偿组件的数据查询接口,获取命中时间的Victim的空间信息。
  2. 弹道模拟判定命中部位。
    • 若命中,则通过Tag确定伤害类型,施加对应的GameplayEffect.
    • 若未命中,则不做任何事情。

判定过程

  1. 备份数据:记录此刻Victim的空间信息,以便于恢复现场。
  2. 移动关键骨骼,将Victim的碰撞体设置为与查询结果一致的空间状态。
  3. 强制物理更新,确保物理引擎已经意识到了,碰撞体的空间信息改变。
  4. 物理扫掠验证,判定命中部位。
  5. 恢复骨骼位置。
留言