3.2 Reliable 与 Unreliable:可靠性背后的代价
两个维度的正交选择
RPC 的关键字里有两层属性:
UFUNCTION(Server, Reliable, WithValidation) // 可靠 + 带校验
UFUNCTION(Server, Unreliable) // 不可靠
| Reliable | Unreliable | |
|---|---|---|
| 保证到达 | 是(丢包会重传) | 否 |
| 保证顺序 | 同一通道内按序 | 否 |
| 网络差时 | 积压、延迟增大 | 直接丢弃,不堵塞 |
| 适用 | 结果不可丢:开火、上车、交易 | 可丢的表现:位置修正、临时特效 |
Reliable 的真实代价:队列积压
Reliable 不是免费的午餐。UE 的可靠通道有队列上限,一旦网络拥堵,Reliable 包积压在队列里,新包排在后面——整个通道的延迟会雪崩式增大。典型症状:玩家卡在原地几秒,然后「瞬移」一大段。所以铁律是:
- 高频 RPC(每帧调的)绝不用 Reliable。移动同步这种走复制属性 + 客户端预测,不该走 Reliable RPC。
- Reliable RPC 只用于「必须执行一次」的逻辑请求:开火、使用道具、上车、结算。
- 表现层的高频推送(受击闪红、弹道修正)用 Unreliable 或属性复制。
一个真实的血泪案例
某项目把「客户端位置上报」做成了 Reliable Server RPC,每帧调用。开发内网毫无问题,上线后弱网玩家(手机 4G)大面积卡死——可靠通道永远追不上,积压几千个包。定位方法正是 stat net:PktLoss 正常,但出站队列长度暴涨。改为复制属性 + 预测后恢复。位置/状态这类「最新值有效」的数据天生适合复制,不适合 Reliable RPC。
执行时机的三条经验
- RPC 不是立即执行。客户端调 Server RPC 后,服务器在下一个网络 tick 处理;服务器调 Client RPC 类似。不要在调用后立刻读「调用结果」。
- RPC 参数的序列化陷阱。传
AActor*时,对方端通过引用解析找 Actor——如果那Actor 对目标端不存在/不相关,参数变成 nullptr(且不报错)。高频误踩点。传值类型(FVector、int32、结构体)最安全。 - 结构体参数要 USTRUCT 支持。RPC 参数必须是可网络序列化的(基础类型、USTRUCT(BlueprintType)、Actor 引用)。
WithValidation:Server RPC 的第一道防线
UFUNCTION(Server, Reliable, WithValidation)
void ServerPurchaseItem(int32 ItemId);
bool ServerPurchaseItem_Validate(int32 ItemId) // 防线
{
if (!PlayerShopComponent || ItemId < 0) return false;
if (!PlayerShopComponent->CanAfford(ItemId)) return false;
return true;
}
void ServerPurchaseItem_Implementation(int32 ItemId) // 真实逻辑
{
PlayerShopComponent->Purchase(ItemId);
}
Validate 返回 false 时,框架会丢弃这次调用并断开这个客户端连接(默认 notify clients=false 时静默踢人)。注意:
- Validate 里不要做副作用(别在 Validate 里扣钱/广播),它只是「这次调用是否合法」。
- Validate 是反作弊的最低线:验证「调用者是否有权、参数是否合法、状态是否允许」。
- Validate 的成本要低(它对每个进入的 Server RPC 都跑)。
RPC 与复制的时机配合
常见组合拳:Server RPC 请求 → 服务器修改复制属性 → 所有客户端 OnRep 自动响应。这比「服务器 Multicast 广播」更符合 UE 哲学(状态优于事件)。
心得:给 RPC 选 Reliable/Unreliable 的口诀——「这个调用没执行会出 bug 吗?」会 → Reliable;「最多就是表现晚一点/丢一次」→ Unreliable。不确定就用 Reliable,然后观察
stat net队列积压再降级。
