2.2 OnRep 回调:客户端收到新值时该做什么
OnRep 的执行时机
OnRep(ReplicatedUsing 的回调,蓝图称 RepNotify)在客户端收到该属性的新值之后、属性已被赋值之后执行。这决定了回调代码的两条铁律:
- 可以直接读新值:回调里
Health已是服务器发来的最新值。 - 只在客户端执行:服务器上修改属性不会触发 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(一次性事件)。
