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

ReliableUnreliable
保证有序、必达(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 = trueRPC 完全不工作
Server/Client 实现写错函数名少了 _Implementation