2.2 OnRep 回调:客户端收到新值时该做什么

OnRep 的执行时机

OnRep(ReplicatedUsing 的回调,蓝图称 RepNotify)在客户端收到该属性的新值之后、属性已被赋值之后执行。这决定了回调代码的两条铁律:

  1. 可以直接读新值:回调里 Health 已是服务器发来的最新值。
  2. 只在客户端执行:服务器上修改属性不会触发 OnRep(那是你写服务器逻辑的地方)。
void AMyCharacter::OnRep_Health(float OldHealth)
{
    // Health 已是新值,OldHealth 是回调前的旧值(UE5 特性,极大方便了差值表现)
    if (Health < OldHealth)
    {
        PlayHitReact(OldHealth - Health);   // 受击表现:按掉血量决定抖动幅度
    }
    UpdateHealthBar();                      // 刷新血条
}

带旧值参数的回调签名

UE5 以后 OnRep 支持一个可选的旧值参数:

UFUNCTION()
void OnRep_Health(float OldHealth);

蓝图 RepNotify 节点也提供旧值引脚。强烈建议用带旧值版本——否则你只能知道「变了」,不知道「变了多少」,做差血量条动画、伤害飘字都要自己再存一份旧值。

回调里该做什么、不该做什么

该做:

  • 表现层反馈:UI 刷新、特效、音效、动画状态切换
  • 纯客户端派生数据重算:例如血量变了重算血条百分比
  • 修正本地预测偏差(如预测血量和服务器不一致时以服务器为准)

不该做:

  • 反向修改权威状态:OnRep 里改复制属性不会同步回服务器(复制是单向的)。
  • 发 RPC:回调触发的表现别再通知服务器,会形成回环浪费。
  • 依赖其他刚复制的 Actor:回调触发时,同一帧内别的 Actor 的复制状态未必已到达(见 2.5 复制顺序)。跨 Actor 联动用 RPC 或下一帧延迟处理更稳。

多属性变化的「批处理」问题

服务器一帧内同时改了血量和护盾,客户端收到时 OnRep_Health 和 OnRep_Shield 各触发一次。如果你的 UI 是「一次性刷新整个状态面板」,两个回调各刷一遍 panel 就做了重复工作。解法:

  • 把 UI 刷新做成防抖:回调里只标记 bDirtyUI = true,在 Tick 或下一帧统一刷新一次。
  • 或者用一个聚合属性:复制一个 FPlayerVitals 结构体,只一个回调。

物理与 OnRep 的冲突

CharacterMovement 里的 网络运动平滑(Network Smoothing) 和你自己的 OnRep 位置回调会打架:如果你在 OnRep_Location 里手动 SetActorLocation,会与模拟代理的插值系统互相覆盖。表现层位置交给平滑系统,OnRep 只处理「非位置类」的逻辑(如进入特殊状态)。

心得:把 OnResp 看成「服务器推给客户端的事件入口」,它是 C/S 架构下服务器向客户端通信的两大通道之一(另一条是 Client RPC)。设计任何「服务器主导、客户端表现」的功能时,先想清楚是走 OnRep(状态变化)还是走 Client RPC(一次性事件)。