4、RPC调用:Server函数、Client函数、Multicast函数、RPC可靠性设置、RPC参数限制

RPC,全称是远程过程调用。说白了,就是让一个客户端上的代码,能跑到服务器上去执行。或者反过来,让服务器上的逻辑,跑到某个客户端上执行。

我刚开始接触Unreal多人游戏时,总觉得RPC很神秘。后来做项目多了,发现它其实就是「跨机器喊话」的工具。你在这台机器上调用一个函数,实际执行的地方却在另一台机器上。嗯,理解了这个本质,后面就好办了。

4.1 为什么要用RPC?

想象一下,你做了一个射击游戏。玩家A按下了鼠标左键,子弹要飞出去。这个「开火」的逻辑,如果只在玩家A的机器上执行,那服务器和其他玩家根本不知道发生了啥。

所以我们需要一种机制:

  • 玩家A告诉服务器:「我开火了」
  • 服务器验证一下:「嗯,你确实可以开火」
  • 服务器再告诉所有玩家:「A开火了,大家快看」

这个「告诉」的过程,就是RPC在做的事。

核心要点:RPC的本质是让函数调用跨越机器边界。调用方在本地发起调用,实际执行在远端。

4.2 Server函数

Server函数,是从客户端调用,在服务器上执行。我习惯叫它「上报函数」。

什么时候用?玩家想移动、想开火、想捡东西——这些需要服务器做最终裁决的操作,都得走Server函数。

// 在角色类中声明
UFUNCTION(Server, Reliable, WithValidation)
void Server_FireWeapon();

// 实现
void AMyCharacter::Server_FireWeapon_Implementation()
{
    // 这段代码只在服务器上执行
    // 生成子弹、播放特效、伤害检测...
    SpawnBullet();
    Multicast_PlayFireEffect();
}

// 验证函数(可选但推荐)
bool AMyCharacter::Server_FireWeapon_Validate()
{
    // 检查玩家是否真的可以开火
    return CurrentAmmo > 0 && !bIsReloading;
}

我在项目中遇到过一个问题:玩家疯狂点击开火,结果Server函数被调用了无数次。后来加了验证函数,检查弹药和冷却时间,才把这个问题堵住。

注意:Server函数只能由拥有该Actor的客户端调用。如果是其他客户端想触发,得先通过服务器中转。

4.3 Client函数

Client函数,是从服务器调用,在某个特定的客户端上执行。我管它叫「下发函数」。

典型场景:服务器想通知某个玩家「你中弹了」「你获得了道具」「你的UI需要更新」。

UFUNCTION(Client, Reliable)
void Client_ShowDamageEffect(float DamageAmount);

void AMyCharacter::Client_ShowDamageEffect_Implementation(float DamageAmount)
{
    // 这段代码只在拥有该角色的客户端上执行
    // 显示受伤UI、播放音效、屏幕震动...
    HUD->ShowDamageIndicator(DamageAmount);
    PlaySound(DamageSound);
}

你想想看,如果服务器直接在所有客户端上播放受伤效果,那其他玩家也会看到你的受伤UI,这显然不对。所以Client函数就是用来精准定位到某个客户端的。

小技巧:我习惯在Client函数名前加「Client_」前缀,一眼就能看出这是下发到客户端的逻辑。团队协作时,这个命名规范能省不少沟通成本。

4.4 Multicast函数

Multicast函数,是从服务器调用,在所有客户端(包括服务器自己)上执行。说白了就是「广播函数」。

什么时候用?播放全局特效、播放声音、同步动画状态——这些需要所有人都看到的东西。

UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayExplosionEffect(FVector Location);

void AMyCharacter::Multicast_PlayExplosionEffect_Implementation(FVector Location)
{
    // 这段代码在所有客户端和服务器上执行
    // 生成爆炸粒子、播放爆炸声、屏幕震动...
    SpawnEmitter(ExplosionParticle, Location);
    PlaySound(ExplosionSound, Location);
}

这里有个坑,我曾经踩过:如果Multicast函数是从客户端直接调用的,它只会在该客户端上执行,不会广播到其他机器。所以Multicast函数必须由服务器调用,或者由客户端通过Server函数间接触发。

一句话总结:Server函数是「上报」,Client函数是「下发」,Multicast函数是「广播」。三个角色各司其职,别搞混了。

4.5 RPC可靠性设置

RPC有两种可靠性模式:Reliable(可靠)和Unreliable(不可靠)。

模式 说明 适用场景
Reliable 保证到达,会重发直到对方确认收到 开火、拾取、伤害计算等关键逻辑
Unreliable 不保证到达,发送一次就不管了 位置同步、特效播放、声音等非关键信息

我个人的习惯是:所有涉及游戏状态变更的RPC,都用Reliable。比如玩家扣血、捡到武器、触发机关。而像位置更新、粒子特效这种丢了也无所谓的,用Unreliable就行。

注意:Reliable RPC是有上限的。Unreal引擎默认限制每个连接最多同时有1024个未确认的Reliable RPC。如果超过了,连接会被强制断开。所以别滥用Reliable,该用Unreliable的地方就用Unreliable。

4.6 RPC参数限制

RPC的参数传递,不是你想传什么就能传什么的。这里有几个硬性规定:

  • 基本类型:int、float、bool、FString、FName、FText等,随便传
  • 结构体:需要标记为USTRUCT,并且包含UProperty宏
  • UObject指针:可以传,但必须保证对象在两端都存在且有效
  • 动态数组:TArray<int>这种可以,但TArray<FMyStruct*>不行
  • 禁止传递:原始指针(非UObject)、复杂容器嵌套、Lambda表达式
// 可以这样传
UFUNCTION(Server, Reliable)
void Server_UpdateScore(int32 NewScore, FString PlayerName);

// 也可以这样传
USTRUCT()
struct FDamageInfo
{
    GENERATED_BODY()
    
    UPROPERTY()
    float DamageAmount;
    
    UPROPERTY()
    FVector HitLocation;
    
    UPROPERTY()
    AActor* DamageCauser;
};

UFUNCTION(NetMulticast, Unreliable)
void Multicast_ApplyDamage(FDamageInfo DamageInfo);

我记得有一次,我想传一个TMap<FString, int32>作为RPC参数,结果编译直接报错。后来查文档才发现,TMap默认不支持RPC序列化。解决办法是把它拆成两个TArray,或者用自定义结构体包装一下。

避坑指南:如果你不确定某个类型能不能作为RPC参数,最简单的办法是看它有没有「NetSerialize」函数。没有的话,大概率传不了。

4.7 实战中的RPC选择策略

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 谁发起?如果是客户端发起,用Server函数
  2. 谁需要知道?如果只需要通知特定客户端,用Client函数
  3. 所有人都需要知道?用Multicast函数
  4. 丢了会出问题吗?会的话用Reliable,不会的话用Unreliable

举个例子:玩家开火。客户端调用Server_FireWeapon(Reliable),服务器验证后调用Multicast_PlayFireEffect(Unreliable)。这样既保证了开火逻辑的可靠性,又避免了特效同步占用太多带宽。

嗯,RPC这块内容其实不难,关键是多动手写。我刚开始做多人项目时,经常搞混Server和Multicast的调用方。后来养成了一个习惯:每次写RPC之前,先在脑子里过一遍「谁调用、谁执行、谁需要知道」。这个习惯帮我避免了很多线上bug。