RPC 语义与可靠性
自动关联目录:RPC 语义与可靠性
RPC 是"一次性动作"的通道:客户端告诉服务端"我开火了",服务端告诉客户端"你被击中了"。它不适合持续状态——那是 属性复制 的事。
一句话定位:RPC 能不能执行,取决于调用者是不是这个 Actor 的"拥有者";RPC 会不会丢,取决于 Reliable 还是 Unreliable。这两条解释了 90% 的"我的 RPC 没跑"。
三种 RPC
| 类型 | 谁调用 | 在哪里执行 |
|---|---|---|
Server | 客户端 | 服务端(且必须是拥有者) |
Client | 服务端 | 拥有该 Actor 的那个客户端 |
NetMulticast | 服务端 | 服务端 + 所有相关客户端 |
UFUNCTION(Server, Reliable, WithValidation)
void ServerFire(FVector_NetQuantize AimDir);
UFUNCTION(Client, Unreliable)
void ClientPlayHitFeedback(FVector_NetQuantize Location);
UFUNCTION(NetMulticast, Unreliable)
void MulticastExplosion(FVector_NetQuantize Location);NetMulticast 从客户端调用时只在本地执行(不会广播出去)。这是刻意的设计:客户端没有广播权。
为什么"我的 RPC 没跑"
按概率排序:
| 原因 | 说明 |
|---|---|
| 调用端不是拥有者 | Server RPC 必须由拥有该 Actor 的连接发起 |
| Actor 没开复制 | bReplicates = false |
| Actor 与该连接不相关 | 见 Replication 的相关性 |
| Actor 处于休眠 | DORM_Awake 才会处理 |
| 调用时机太早 | BeginPlay 时连接可能还没建立/Actor 还没复制过去 |
| 函数名/签名不匹配 | 实现与声明不一致(参考 UHT 的 _Implementation 规则) |
| 在服务端调 Server RPC | 会直接在服务端执行,不报错但语义不对 |
最高频的一条是"谁拥有这个 Actor"。拾取物、门、箱子这些世界 Actor 默认属于服务端,客户端对它们调 Server RPC 会被丢弃——正确做法是在 PlayerController 或玩家 Pawn 上定义一个"转发用"的 Server RPC,参数里带上目标 Actor。
// 玩家身上:把"我想开这个箱子"发给服务端
UFUNCTION(Server, Reliable)
void ServerInteract(AActor* Target); // Target 必须是可网络寻址的拥有者(Ownership)速查
| 调用方 | Actor 归属 | 结果 |
|---|---|---|
| 拥有玩家的 Pawn | 该玩家 | ✓ 能调 Server RPC |
| 自己的 PlayerController | 自己 | ✓ |
| 自己的 PlayerState | 自己 | ✓ |
| 世界里的门/箱子 | 服务端 | ✗ 被丢弃 |
| 别人的 Pawn | 别人 | ✗ |
Actor->SetOwner(NewOwner); // 可以显式转移(如玩家上车后拥有载具)Reliable vs Unreliable
| Reliable | Unreliable | |
|---|---|---|
| 保证 | 有序、必达(ACK + 重传) | 可能丢、可能乱序 |
| 代价 | 队列堆积,丢包时会阻塞后续 | 无 |
| 适用 | 必须执行一次且只执行一次(购买、完成任务、释放技能) | 高频、丢了下一帧会补(位置、动画、表现) |
| 风险 | 可靠缓冲区溢出 → 断开连接 | 无 |
默认用 Unreliable。Reliable 是稀缺资源:UE 的可靠队列有上限,一个 200 次/秒的 Reliable RPC 在任何弱网下都会把连接挤爆。
// 错误示范:高频状态用 Reliable
UFUNCTION(Server, Reliable)
void ServerSetAim(FVector Dir); // 每帧调用 → 迟早爆
// 正确:用 Unreliable,或干脆走属性复制/移动组件的输入
UFUNCTION(Server, Unreliable)
void ServerSetAim(FVector_NetQuantizeNormal Dir);WithValidation
bool AMyActor::ServerFire_Validate(FVector_NetQuantize AimDir)
{
return AimDir.IsNormalized(); // 返回 false → 判定为作弊/异常 → 断开该连接
}
void AMyActor::ServerFire_Implementation(FVector_NetQuantize AimDir)
{
// 服务端执行
}_Validate 返回 false 会直接踢掉连接,所以它只能用来拦"明显不可能"的输入(数值越界、状态机非法),不能用来做"业务上不允许"的判定——那会变成玩家开枪失败就掉线。
业务校验应该在 _Implementation 里做,返回一个 Client RPC 告知失败。
参数与带宽
| 要点 | 说明 |
|---|---|
| 用小类型 | FVector_NetQuantize、FVector_NetQuantizeNormal、整数替代浮点 |
| 避免传数组/结构体 | 会整体序列化;传 ID,让对端查表 |
| 大 RPC 会分片 | 超过单包大小的会被拆成 bunch |
| 频率 > 大小 | 通常"发太频繁"比"一次发太多"更致命 |
一条 RPC 的实际成本 = 包头 + 函数标识 + 参数,高频调用时包头开销占比惊人。合并成一次批量 RPC 通常比多次小 RPC 划算。
与休眠、相关性的交互
| 状态 | RPC 行为 |
|---|---|
DORM_Awake | 正常 |
DORM_DormantAll | 属性复制暂停;要发 RPC 需先 FlushNetDormancy() |
| 不相关 | 不会发给该连接 |
Actor->FlushNetDormancy(); // 唤醒,下一次复制会处理"休眠的 Actor 收不到 RPC"是常见困惑:休眠会暂停网络处理,唤醒后才会继续。
常见坑
| 坑 | 说明 |
|---|---|
| 对不属于自己的 Actor 调 Server RPC | 静默丢弃 |
滥用 Reliable | 队列溢出、断连 |
_Validate 里做业务校验 | 玩家行为异常就掉线 |
| 每帧发 RPC 同步状态 | 应用属性复制 |
从客户端调 NetMulticast | 只在本地执行,不是广播 |
BeginPlay 里立刻调 RPC | 连接/复制可能未就绪 |
| 传大结构体 | 浪费带宽,应传 ID |
忘记 bReplicates = true | RPC 完全不工作 |
| Server/Client 实现写错函数名 | 少了 _Implementation |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/a2859f2f.html
更新于:2026年10月10日