概述
以前写的
本文主要由AI编写
核心分层
┌────────────────────────────────────────────────────────────────────┐
│ 游戏逻辑层 │
│ Actor / Component / GameMode / PlayerController / GameState │
├────────────────────────────────────────────────────────────────────┤
│ 复制层(Replication) │
│ 属性复制 / RPC / 预测 / 校正 │
│ FObjectReplicator / FRepLayout / FRepState │
├────────────────────────────────────────────────────────────────────┤
│ 通道层(Channel) │
│ UActorChannel / UControlChannel / UVoiceChannel │
├────────────────────────────────────────────────────────────────────┤
│ 连接层(Connection) │
│ UNetConnection → SendBuffer / ReceiveBuffer │
├────────────────────────────────────────────────────────────────────┤
│ 驱动层(Driver) │
│ UNetDriver (UIpNetDriver) → Socket / Packet 收发 │
├────────────────────────────────────────────────────────────────────┤
│ 传输层(Transport) │
│ UDP Socket → Internet │
└────────────────────────────────────────────────────────────────────┘
数据流向
属性复制(服务器→客户端):
Actor 属性变化
→ FObjectReplicator 检测属性差异(FRepLayout 比较)
→ 序列化变化的属性到 FOutBunch
→ UActorChannel 打包 Bunch
→ UNetConnection 合并到 SendBuffer
→ UNetDriver 通过 UDP Socket 发送 Packet
→ 客户端 UNetDriver 接收
→ UNetConnection 解包
→ UActorChannel 派发 Bunch
→ FObjectReplicator 反序列化并应用属性
RPC(双向):
调用 UFUNCTION(Server/Client/NetMulticast)
→ AActor::CallRemoteFunction()
→ UActorChannel::QueueRemoteFunctionBunch() 或直接发送
→ 序列化函数参数到 Bunch
→ 通过 Channel → Connection → Driver 发送
→ 接收端 FObjectReplicator::ReceivedBunch()
→ ProcessRemoteFunction() 反序列化参数并调用函数
网络模式(NetMode)
// EngineBaseTypes.h
enum ENetMode
{
NM_Standalone, // 单机模式,无网络。仍被视为服务器(拥有全部服务器功能)
NM_DedicatedServer, // 专用服务器,无本地玩家,无渲染
NM_ListenServer, // 监听服务器,本地玩家同时是Host
NM_Client, // 网络客户端,连接到远程服务器
};
各模式关键区别
| 特性 | Standalone | DedicatedServer | ListenServer | Client |
|---|---|---|---|---|
| 有服务器逻辑 | ✅ | ✅ | ✅ | ❌ |
| 有本地玩家 | ✅ | ❌ | ✅ | ✅ |
| 渲染/显示 | ✅ | ❌ | ✅ | ✅ |
| GameMode 存在 | ✅ | ✅ | ✅ | ❌ |
| Actor 复制方向 | 无 | Server→Client | Server→Client | 接收 |
常用接口
// 在 Actor/Component 中
GetNetMode() // 返回 ENetMode
HasAuthority() // 等价于 Role == ROLE_Authority
IsNetMode(NM_DedicatedServer) // 快速判断
// 实用宏/模式
if (GetLocalRole() == ROLE_Authority) // 当前进程对此 Actor 有权威
if (GetRemoteRole() == ROLE_SimulatedProxy) // 远端是模拟代理
网络角色(NetRole)
// EngineTypes.h
enum ENetRole : int
{
ROLE_None, // 无角色(不参与网络)
ROLE_SimulatedProxy, // 模拟代理:服务器复制状态,客户端模拟插值
ROLE_AutonomousProxy, // 自主代理:客户端本地控制,拥有输入权
ROLE_Authority, // 权威:拥有最终决定权的服务器端
};
每个 Actor 有两个角色属性:
Role(GetLocalRole()):此 Actor 在当前进程中的角色RemoteRole(GetRemoteRole()):此 Actor 在远端进程中的角色
服务器端的 PlayerCharacter:
Role = ROLE_Authority ← 服务器是权威
RemoteRole = ROLE_AutonomousProxy ← 客户端是自主代理(因为玩家自己控制)
该 PlayerCharacter 在控制它的客户端:
Role = ROLE_AutonomousProxy ← 本地是自主代理
RemoteRole = ROLE_Authority ← 远端是权威(服务器)
其他客户端看到的该 PlayerCharacter:
Role = ROLE_SimulatedProxy ← 仅模拟
RemoteRole = ROLE_Authority ← 远端是权威
Role 的决定因素
| 条件 | 服务器端 Role | 控制端 Client Role | 其他 Client Role |
|---|---|---|---|
| 玩家控制的 Pawn | Authority | AutonomousProxy | SimulatedProxy |
| PlayerController | Authority | AutonomousProxy | 不复制 |
| 一般 replicated Actor (如果不复制的actor只存在于spawn那一端) |
Authority | SimulatedProxy | SimulatedProxy |
| bOnlyRelevantToOwner 的物品 | Authority | AutonomousProxy | 不可见 |
核心类层次结构
UNetDriver — 网络驱动
Engine/Classes/Engine/NetDriver.h
网络系统的核心,负责 Socket 管理、Packet 收发、连接管理。
class UNetDriver : public UObject
{
UWorld* World; // 所属世界
UNetConnection* ServerConnection; // [客户端] 到服务器的连接(服务器端为 null)
TArray<UNetConnection*> ClientConnections; // [服务器] 所有客户端连接
UReplicationDriver* ReplicationDriver; // 可选:替代默认 ServerReplicateActors (Rep Graph使用)
FNetworkObjectList& GetNetworkObjectList(); // 所有需要复制的 Actor 信息
// ★ 核心:每帧服务器端复制主循环
virtual int32 ServerReplicateActors(float DeltaSeconds);
// 通道配置
TArray<FChannelDefinition> ChannelDefinitions; // Actor/Control/Voice 等通道定义
};
关键子类:
UIpNetDriver— 基于 IP/UDP 的标准网络驱动UIpConnection— IP 连接实现
挂在World上
UNetConnection — 网络连接
Engine/Classes/Engine/NetConnection.h
代表一个 端到端的网络连接。服务器为每个客户端维护一个。
class UNetConnection : public UPlayer
{
TArray<UChannel*> OpenChannels; // 所有打开的通道
AActor* OwningActor; // 拥有此连接的 Actor(通常是 PlayerController)
AActor* ViewTarget; // 视图目标(用于相关性计算)
int32 MaxPacket; // 最大包大小
FBitWriter SendBuffer; // 发送缓冲区
// 子连接(Split Screen / 多玩家连接)
TArray<UChildConnection*> Children;
// 状态
EConnectionState GetConnectionState();
// 通道管理
UChannel* CreateChannelByName(FName ChName, EChannelCreateFlags Flags, int32 Index);
};
UChannel
Engine/Classes/Engine/Channel.h
通道是连接上的逻辑管道,不同类型的数据走不同通道。
// 通道类型
enum EChannelType
{
CHTYPE_None = 0, // 无效
CHTYPE_Control = 1, // 控制通道(登录握手、NMT 消息)
CHTYPE_Actor = 2, // Actor 通道(属性复制、RPC)
CHTYPE_File = 3, // 文件传输(弃用中)
CHTYPE_Voice = 4, // 语音
};
class UChannel : public UObject
{
UNetConnection* Connection; // 所属连接
int32 ChIndex; // 通道索引
FName ChName; // 通道名称
uint32 OpenAcked:1; // 通道是否已 ACK 打开
uint32 Closing:1; // 是否正在关闭
uint32 Dormant:1; // 是否休眠
uint32 OpenedLocally:1; // 是否本地打开
FInBunch* InRec; // 收到但有依赖的包队列
FOutBunch* OutRec; // 已发送但未 ACK 的可靠包队列
virtual void ReceivedBunch(FInBunch& Bunch); // 接收数据
virtual int64 Close(EChannelCloseReason); // 关闭通道
};
UActorChannel — Actor 通道 ★核心
Engine/Classes/Engine/ActorChannel.h
每个被复制的 Actor 在每个连接上有一个 ActorChannel。负责该 Actor 的属性复制和 RPC 传输。
+── ActorChannel Bunch 结构 ─────────────────────────────────────+
│ SpawnInfo (首次打开时) │
│ - Actor Class / Spawn Location / Rotation │
│ - Actor NetGUID / Component NetGUIDs │
├────────────────────────────────────────────────────────────────┤
│ Content Block (每个需要复制的对象一个 Block) │
│ - NetGUID ObjRef (标识对象) │
│ - Properties... (变化的属性) │
│ - RPCs... (排队的RPC) │
├────────────────────────────────────────────────────────────────┤
│ </End Tag> │
+────────────────────────────────────────────────────────────────+
class UActorChannel : public UChannel
{
AActor* Actor; // 关联的 Actor
FNetworkGUID ActorNetGUID; // Actor 的网络 GUID
TSharedPtr<FObjectReplicator> ActorReplicator; // Actor 属性复制器
TMap<UObject*, TSharedRef<FObjectReplicator>> ReplicationMap; // 子对象复制器
// ★ 核心:复制此 Actor 的属性差异
int64 ReplicateActor();
// 子对象复制
bool ReplicateSubobject(UObject* Obj, FOutBunch& Bunch, FReplicationFlags RepFlags);
// RPC 排队
void QueueRemoteFunctionBunch(UObject* CallTarget, UFunction* Func, FOutBunch& Bunch);
// 休眠
virtual bool ReadyForDormancy(bool debug=false);
virtual void StartBecomingDormant();
};
FObjectReplicator — 对象复制器
每个需要复制的 UObject(Actor 或子对象)有一个 FObjectReplicator,负责:
- 属性差异检测(通过 FRepLayout)
- 属性序列化/反序列化
- Properties 的 Recent 状态缓存
- Custom Delta Serialization(如 FastArray)
FRepLayout — 属性布局
描述一个 UClass/UStruct 的所有 Replicated 属性的内存布局:
- 属性偏移、大小、类型
- 条件复制标记(COND_OwnerOnly 等)
- 父级/子级关系
- 用于快速内存比较(memcmp)判断属性是否变化
类关系图
UNetDriver
├── ServerConnection (UNetConnection) // 客户端到服务器
├── ClientConnections[] (UNetConnection) // 服务器到各客户端
│ └── OpenChannels[] (UChannel)
│ ├── UControlChannel [Index 0] // 控制通道(仅一个)
│ ├── UActorChannel [Index 1~N] // 每个 Actor 一个
│ │ ├── ActorReplicator (FObjectReplicator)
│ │ │ └── RepLayout (FRepLayout)
│ │ └── ReplicationMap (子对象 Replicators)
│ └── UVoiceChannel [Index 1] // 语音
├── ReplicationDriver (UReplicationDriver) // 可选替代
└── NetworkObjectList // 全部复制 Actor 元数据
UNetDriver
├── ServerConnection (UNetConnection) // 客户端到服务器
├── ClientConnections[] (UNetConnection) // 服务器到各客户端
客户端 UNetDriver 用 ServerConnection 连接了,服务器 UNetDriver 的 ClientConnections 中的一个。
客户端,服务器各自一个连接对象,连上了。
连接上每一个复制的Actor都有一个ActorChannel 对应
连接建立与握手流程
连接过程
客户端 服务器
│ │
│──── NMT_Hello ──────────────────→│ 1. 客户端发送 Hello(版本号、加密等)
│ │
│←─── NMT_Challenge ──────────────│ 2. 服务器返回 Challenge(防伪造)
│ │
│──── NMT_Login ──────────────────→│ 3. 客户端发送 Login(玩家名、凭证等)
│ │ → AGameModeBase::PreLogin() 检查
│ │ → 拒绝则 NMT_Failure
│ │
│←─── NMT_Welcome ────────────────│ 4. 服务器发送 Welcome(地图名等)
│ │
│──── NMT_Netspeed ───────────────→│ 5. 客户端发送网络速度设置
│ │
│ │ 6. 服务器执行:
│ │ AGameModeBase::Login() → 创建 PlayerController
│ │ AGameModeBase::PostLogin()
│ │
│←─── 复制 PlayerController ───────│ 7. 服务器开始复制 PC 给客户端
│ │
│──── NMT_Join ───────────────────→│ 8. 客户端确认加入
│ │ → AGameModeBase::HandleStartingNewPlayer()
│ │ → RestartPlayer() → 生成 Pawn
NMT(Net Message Type)消息
客户端连接服务器时,双方需要先进行一系列"协商"(版本验证、登录、地图同步等),这个协商过程用的就是 NMT 消息。它是引擎内部的握手协议,不是游戏逻辑层的 RPC。
ControlChannel 就是上面 Connection 里面一堆Channel的第0个。
关键 GameMode 回调
| 回调 | 时机 | 典型用途 |
|---|---|---|
PreLogin(Options, Address, ErrorMessage) |
Login 消息到达时 | 验证、版本检查、Ban 检查 |
Login(NewPlayer, Portal, Options, UniqueId, ErrorMessage) |
PreLogin 通过后 | 创建 PlayerController |
PostLogin(NewPlayer) |
PC 完全初始化后 | 初始化 PlayerState、加入 GameState |
HandleStartingNewPlayer(NewPlayer) |
客户端 Join 后 | RestartPlayer()、选择出生点 |
Logout(Exiting) |
断开连接时 | 清理 |
Actor 复制(Replication)机制
Actor 如何进入复制系统
AMyActor::AMyActor()
{
bReplicates = true; // ★ 必须:启用复制
bNetLoadOnClient = true; // 关卡放置的 Actor 客户端是否加载
}
当 bReplicates = true 的 Actor 被 Spawn 时(服务器端):
- 引擎将其注册到
UNetDriver::NetworkObjectList - 每帧
ServerReplicateActors会考虑是否需要复制 - 首次对某个连接复制时,打开一个
UActorChannel
默认复制主循环 — ServerReplicateActors
ServerReplicateActors(DeltaSeconds)
│
├── 1. PrepConnections()
│ └── 准备所有活跃的客户端连接
│
├── 2. BuildConsiderList()
│ └── 遍历 NetworkObjectList,构建"可能需要复制"的 Actor 列表
│ 过滤条件:bReplicates, bActorInitialized, NetUpdateTime 到期
│
├── 3. 对每个 Connection:
│ │
│ ├── a. PrioritizeActors()
│ │ └── 对 ConsiderList 中每个 Actor:
│ │ ├── IsNetRelevantFor() → 相关性判断
│ │ ├── GetNetPriority() → 优先级计算
│ │ └── 构建 FActorPriority 列表
│ │
│ ├── b. Sort
│ │ └── 按优先级降序排列
│ │
│ └── c. ProcessPrioritizedActors()
│ └── 按优先级顺序复制:
│ ├── Channel 是否存在?不存在则创建
│ ├── UActorChannel::ReplicateActor()
│ │ ├── FObjectReplicator::ReplicateProperties()
│ │ │ └── 比较属性差异 → 序列化变化的属性
│ │ ├── ReplicateSubobjects()
│ │ └── 发送排队的 RPC
│ └── 带宽耗尽 → 停止,推迟到下帧
│
└── 4. 发送 DestructionInfo(Actor 销毁通知)
函数找不到啊,让AI找一下:
NetDriver.cpp 实际的函数名
ServerReplicateActors= UNetDriver::ServerReplicateActors L6057
PrepConnections= ServerReplicateActors_PrepConnections L5077
BuildConsiderList= ServerReplicateActors_BuildConsiderList L5182
PrioritizeActors= ServerReplicateActors_PrioritizeActors L5407
ProcessPrioritizedActors ServerReplicateActors_ProcessPrioritizedActorsRange L5558
IsNetRelevantFor 实际叫 IsActorRelevantToConnection
// 没有 Channel 的 Actor 才需要判断相关性
if (!Channel)
{
if (!IsLevelInitializedForActor(Actor, Connection))
continue; // 地图没加载
if (!IsActorRelevantToConnection(Actor, ConnectionViewers))
continue; // 不相关,跳过,不建 Channel
}
// 已经有 Channel 的 Actor → 无论相关性如何都继续(会在后面超时后关 Channel)
已经有 Channel 的 Actor 不会被相关性过滤掉,而是在 Channel 超时后才关闭,这样防止 Actor 因为短暂离开视野就立刻消失
Actor 生命周期与复制
服务器 SpawnActor
→ 注册到 NetworkObjectList
→ 下一帧 ServerReplicateActors 发现它
→ 对每个相关连接打开 ActorChannel
→ 发送 SpawnInfo(Class, Transform, 初始属性)
→ 客户端 SpawnActor(远端实例化)
→ 后续帧只同步属性差异
服务器 DestroyActor
→ 关闭所有 ActorChannel
→ 发送 DestructionInfo
→ 客户端 DestroyActor
FNetworkGUID — 网络唯一标识
每个需要网络通信的 UObject 都需要一个 FNetworkGUID 来在不同进程中标识同一个对象。
- 静态 Actor(关卡放置):基于路径生成稳定 GUID
- 动态 Actor(SpawnActor):服务器分配递增 GUID,复制给客户端
UPackageMap负责 GUID ↔ UObject 的映射
属性复制(Property Replication)
声明可复制属性
// 头文件
UPROPERTY(Replicated)
float Health;
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
// 源文件
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyActor, Health);
}
属性复制的底层流程
UActorChannel::ReplicateActor()
→ FObjectReplicator::ReplicateProperties(Bunch, RepFlags)
→ FRepLayout::ReplicateProperties(RepState, ChangelistState, Data, ...)
│
├── 1. CompareProperties(属性比较)
│ └── 遍历所有 Replicated Property
│ ├── 使用 FRepLayout 中记录的属性 Offset 和 Size
│ ├── 与 ShadowBuffer(上次发送的值缓存)做 memcmp
│ ├── 如果启用 Push Model:只比较标记为 Dirty 的属性
│ └── 产出 Changed Property Handles 列表
│
├── 2. 条件过滤
│ └── 对每个 Changed Property 检查 COND_OwnerOnly / COND_InitialOnly 等
│
└── 3. SendProperties(序列化发送)
└── 对每个通过过滤的 Changed Property
├── 写入 Property Handle(标识)
└── NetSerialize 或 NetDeltaSerialize 写入值
Shadow Buffer(影子缓冲区)
- 每个
FObjectReplicator维护一个 Shadow Buffer - 记录上次成功发送给客户端的属性值
- 用于下次比较时判断哪些属性发生了变化
- 只有在 Bunch 被 ACK 后才更新 Shadow Buffer
RepNotify — 属性复制回调
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
UFUNCTION()
void OnRep_Health()
{
// 客户端收到 Health 变化后自动调用
UpdateHealthBar();
}
// 带旧值参数版本(UE5)
UFUNCTION()
void OnRep_Health(float OldHealth)
{
float Delta = Health - OldHealth;
ShowDamageNumber(Delta);
}
RepNotify 触发时机:
- 客户端收到属性新值并应用后立即调用
- 服务器端不会调用 RepNotify(服务器不接收自己的属性复制)
- 多个属性同时变化时,RepNotify 按属性声明顺序依次调用
条件复制
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(AMyActor, Ammo, COND_OwnerOnly); // 只给 Owner
DOREPLIFETIME_CONDITION(AMyActor, TeamId, COND_InitialOnly); // 只首次
DOREPLIFETIME_CONDITION(AMyActor, Stealth, COND_Custom); // 运行时动态
DOREPLIFETIME_CONDITION(AMyActor, MoveData, COND_SimulatedOnly); // 只给模拟端
}
完整条件列表
| 条件 | 说明 | 场景 |
|---|---|---|
COND_None |
无条件,始终复制 | 默认 |
COND_InitialOnly |
只在首次复制时发送 | 阵营 ID、角色名称 |
COND_OwnerOnly |
只发给 Owner 连接 | 弹药数、技能冷却 |
COND_SkipOwner |
除 Owner 外所有人 | 第三人称动画状态 |
COND_SimulatedOnly |
只发给 SimulatedProxy | 移动模拟数据 |
COND_AutonomousOnly |
只发给 AutonomousProxy | 本地玩家特有数据 |
COND_SimulatedOrPhysics |
Simulated 或物理模拟 | 物理数据 |
COND_InitialOrOwner |
首次或 Owner | - |
COND_Custom |
运行时动态开关 | 隐身、可见性 |
COND_ReplayOrOwner |
Replay 或 Owner | - |
COND_ReplayOnly |
仅 Replay | - |
COND_SkipReplay |
跳过 Replay | - |
COND_Never |
永不复制 | 占位/手动控制 |
COND_NetGroup |
基于 NetGroup 过滤 | Iris 系统 |
COND_Custom 动态条件
DOREPLIFETIME_CONDITION(AMyActor, StealthState, COND_Custom);
void AMyActor::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
Super::PreReplication(ChangedPropertyTracker);
// 只在存活时复制隐身状态
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, StealthState, bIsAlive);
}
RPC 远程过程调用
RPC 类型
// Server RPC:客户端 → 服务器
UFUNCTION(Server, Reliable)
void ServerFireWeapon(FVector_NetQuantize AimLocation);
// Client RPC:服务器 → 拥有此 Actor 的客户端
UFUNCTION(Client, Reliable)
void ClientShowDamageNumber(float Damage, AActor* DamagedActor);
// NetMulticast RPC:服务器 → 所有已打开 Channel 的客户端
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayHitEffect(FVector HitLocation, FVector HitNormal);
RPC 类型对比
| 属性 | Server | Client | NetMulticast |
|---|---|---|---|
| 方向 | Client → Server | Server → Client | Server → All Clients |
| 调用者 | AutonomousProxy | Authority | Authority |
| 目标 | Authority | OwningClient | 所有相关 Client |
| 需要 Ownership | ✅ | ✅ | ❌* |
| 常见用途 | 输入/操作请求 | UI 通知、特定反馈 | 特效、音效 |
*NetMulticast 不严格需要 Ownership,但只发给已有 ActorChannel 的连接。
Reliable vs Unreliable
| 特性 | Reliable | Unreliable |
|---|---|---|
| 送达保证 | ✅ 保证送达 | ❌ 可能丢包 |
| 顺序保证 | ✅ 有序 | ❌ 可能乱序 |
| 丢包处理 | 重传 | 丢弃 |
| 带宽开销 | 较大(缓存、重传) | 较小 |
| 适用场景 | 关键操作(技能释放、状态变更) | 高频低价值(特效、位置微调) |
| 潜在风险 | 大量 Reliable → 队列满 → 断连 | 视觉不同步 |
RPC 验证(Validation)
UFUNCTION(Server, Reliable, WithValidation)
void ServerUseItem(int32 SlotIndex);
bool AMyActor::ServerUseItem_Validate(int32 SlotIndex)
{
// 返回 false → 断开该客户端连接(作弊检测)
return SlotIndex >= 0 && SlotIndex < MaxSlots;
}
void AMyActor::ServerUseItem_Implementation(int32 SlotIndex)
{
// 实际逻辑
}
RPC 的执行规则矩阵
和直觉还是比较契合的
Server RPC 调用 ServerFunction():
| 调用位置 | Actor Owner | 结果 |
|---|---|---|
| Client(AutonomousProxy) | 本连接 | ✅ 发送到服务器执行 |
| Client(SimulatedProxy) | 其他连接 | ❌ 丢弃 |
| Server(Authority) | - | ✅ 本地直接执行 |
Client RPC 调用 ClientFunction():
| 调用位置 | Actor Owner | 结果 |
|---|---|---|
| Server(Authority) | 有 Owner | ✅ 发送到 Owner 客户端执行 |
| Server(Authority) | 无 Owner | ❌ 丢弃 |
| Client | - | ✅ 本地直接执行(已到达目标) |
NetMulticast 调用 MulticastFunction():
| 调用位置 | 结果 |
|---|---|
| Server(Authority) | ✅ 服务器本地执行 + 发送到所有相关客户端 |
| Client | ✅ 仅本地执行,不发送 |
总结
★ Server RPC 必须由 AutonomousProxy(PlayerController/Pawn 的 Owner)发起
★ Reliable RPC 不要高频调用(>10次/秒),否则缓冲区溢出断连
★ Unreliable RPC 在带宽不足时会被跳过
★ 蓝图中 "Run on Server" = Server RPC
★ 蓝图中 "Run on Owning Client" = Client RPC
★ 蓝图中 "Multicast" = NetMulticast RPC