3.5 实战:一次受击同步的完整链路

把前三节的内容串起来。需求:玩家 A 砍中怪物 M,要求——服务器裁决伤害、A 看到打击感、其他玩家看到 M 受击表现、M 的血量对所有端正确。

错误版本(新手典型)

A 的客户端:
  碰撞检测命中 M → 本地直接 M.Health -= 50(不等服务器)
  → MulticastPlayHit(M) 想让所有人看见
结果:A 本地血量改了但服务器不知道 → 复制属性同步回来把血量冲掉;Multicast 从客户端调用无效,别人看不到。

三个错:客户端改复制状态、客户端调 Multicast、没有单一裁决。

正确版本

步骤 1 — 客户端:检测命中,发起请求(带防伪造)

// A 的武器(客户端权威检测命中盒)
if (HasAuthority()) { ApplyHit(M, 50); }            // 服务器自己触发,直接走
else { ServerReportHit(M, 50); }                    // 客户端只「报告」

步骤 2 — 服务器:验证并裁决

bool AWeapon::ServerReportHit_Validate(AMonster* M, int32 Dmg)
{
    if (!IsValid(M) || Dmg <= 0 || Dmg > 1000) return false;   // 参数合法
    if (!M->IsAlive()) return false;                            // 状态合法
    if (!IsWithinRange(M)) return false;                        // 距离合法(防瞬移攻击)
    return true;
}
void AWeapon::ServerReportHit_Implementation(AMonster* M, int32 Dmg)
{
    const float Final = M->ApplyDamage(Dmg);         // 服务器算最终伤害(含减伤/暴击)
    // 血量是 M 的复制属性 → 自动广播给所有相关客户端(OnRep_Health 触发表现)
}

步骤 3 — 受击表现:谁看到什么

  • 打击感(A 自己):客户端预测播放(挥砍顿帧、镜头抖动),不等服务器——手感第一。
  • 受击表现(所有看到 M 的客户端):M 的 OnRep_Health 回调播放「掉血飘字 + 受击动画」。这就是事件通道的选择——用属性复制天然解决「所有相关端」的分发。
  • 死亡.special 表现:如果伤害致死,服务器在 M 上 Multicast 一次播死亡特写(只在死亡瞬间一次,Reliable 值得)。

步骤 4 — 校正

A 的客户端可能预测了伤害 50,服务器算出 45(M 有减伤)。血量复制回来时 OnRep 把血量设成正确值——飘字可能先飘了 50 再被修正。解法:伤害飘字也由 OnRep 驱动(以服务器值为准),客户端预测的指示性伤害做小动画淡出即可。

链路总结图

A.Client            Server              M(复制属性)         其他客户端
   │                   │                      │                 │
   │── 命中检测(本地) ─→│                      │                 │
   │   ServerReportHit │                      │                 │
   │                   ├── Validate+裁决 ──→ Health -= Final    │
   │                   │                      │                 │
   │                   │              复制属性广播 ───────────→│
   │                   │                      │  OnRep_Health   │
   │←────────── 复制属性(A 也是相关客户端)──────────────────│
   │   OnRep_Health(A 也看到正确血量)                        │

四条设计原则复盘:

  1. 客户端只报告意图,服务器裁决结果。
  2. 持续状态(血量)走复制,一次性事件(死亡特写)走 RPC。
  3. 表现层能预测就预测,服务器值到达时校正。
  4. Validate 是防线,Implementation 里再做状态二次校验。

延伸:打击感与延迟的平衡

在 100ms 延迟下,A 挥刀劈中 M 的「命中停顿」如果等服务器回包,手感糊成泥。标准做法是客户端预测命中表现:本地播顿帧 + 音效(0.05s 内完成,短到服务器校正到来前就结束了),伤害飘字延迟到 OnRep。玩家感知不到延迟,逻辑仍然权威。

心得:把这套「报告-裁决-复制-表现」的链路画成项目内标准模板。我们团队所有伤害、技能、交互都套这个模板,联机 bug 率降了一个数量级——因为整个项目的网络交互只有一种形状,review 和排错都模式化。