从”普通程序员能听懂”到”能改 UE 网络源码”——基于源码逐行考证,六架构全覆盖:
网络 / 同步 / RPC / 通信 / 分发 / 网关,附移动端优化 / 大世界分发 / 调试工具链三大专题

  • 文档版本:1.0(2026-08-26)
  • 源码快照:本地仓库 d:\Project\GameDevelop\UnrealEngine,UE 5.9 移动优化 fork,HEAD 6cea9bd20f8f
  • 写作原则:文中所有”源码考古”块引用的文件路径、行号、函数名均在本仓库中实际验证过;行号只用于定位,请以 路径:函数名 为准
  • 姊妹文档:本文是《Nanite 虚拟几何体完全教程》与《Virtual Shadow Maps 完全教程》的第三篇——同一写作体系,但网络是 CPU 侧、跨进程的世界(前两篇是 GPU 侧单进程),读者无需先读前两篇
  • 符号说明Replication=复制,Bunch=束,Packet=数据报,NetGUID/RepGraph/Changelist/Beacon/Iris 不翻译,Dormancy=休眠,Relevancy=相关性

第 0 章 导读:这份文档怎么读

TL;DR|这份文档把 UE 网络架构拆成六个子系统讲透:网络(邮局:连接与通道)、
同步(信件抄送:属性复制)、RPC(电话:远程函数调用)、通信(安检流水线:
PacketHandler 与包格式)、分发(分区快递员:谁该收到什么)、网关(门卫与换乘站:
登录/Beacon/旅行)。三个深度档位:30 秒看懂、半天学会调参、1 天读源码。

0.1 目标读者与前置知识

目标读者是普通程序员:写过 C++、用蓝图或代码摆弄过 Actor、知道”服务器和客户端”是什么但没深挖过 UE 的网络层。每个网络术语第一次出现都配一句直觉解释。

前置知识三个:

  1. UDP 与 TCP 的区别:UDP 快但会丢包乱序,TCP 可靠但慢(重传+拥塞控制)。UE 用 UDP 打底、自己实现可靠性——理解这一点是全文档的钥匙;
  2. 权威服务器:多人游戏里”谁说了算”——服务器是唯一真相,客户端只能建议;
  3. Actor 与蓝图:UE 的场景对象与脚本基础。

与姊妹文档的关系:Nanite/VSM 讲的是”一帧怎么画出来”,本文讲的是”多台机器怎么玩同一个游戏”——但思维方式一脉相承:理解分层、理解数据结构、理解决策链

0.2 三条阅读路径

通道 读什么 耗时 读完能做什么
🚀 30 秒通道 每章 TL;DR + 第 2 章六类比 + 图 F1 10 分钟 跟人讲清六架构分工、复制与 RPC 的区别、C/S 权威模型
📚 标准通道 TL;DR + 正文 + 全部图 + 第 9~12 章 6~8 小时 完整数据流;会调 CVar 优化带宽;能设计属性复制方案;会用 NetTrace 排障
🔬 源码考古通道 全部 + 源码考古块 + 附录 A 对照 1~2 天 敢打开 Engine/Private/ 对照着读,能定位任意子系统入口,能改网络源码

0.3 块标记约定

与姊妹文档完全一致:

  • **TL;DR**(每章开头):30 秒读完本章,3~5 句、必须含类比名、不含源码路径;
  • **类比实验室**(正文小节):用普通程序员已懂的东西讲陌生概念;
  • **源码考古**(每章末尾,折叠块):贴真实源码(≤25 行/段),逐行翻译。引用格式:路径:行号(函数名)
  • **避坑**(正文穿插):版本差异、公开资料与当前代码不一致、易错点。

0.4 一张图看懂全局

图 F1 UE 网络全链路总览(六架构分工;此图贯穿全文,后文各章是对它的展开)
游戏逻辑层(World Tick:TickDispatch 收 → Actor Tick → TickFlush 发,LevelTick.cpp:1926-1938) ① 收包 网络架构(第 3 章) Socket 收 UDP 数据报 UNetConnection::ReceivedPacket 包序号/ACK 处理(NetPacketNotify) Channel 分发(DispatchPacket) Bunch 重组(ReceivedNextBunch) ② 拆包验包 通信架构(第 4 章) PacketHandler 入向流水线 解密 / 解压 / 验签 无状态握手验证(StatelessConnect) DDoS 检测兜底 ③ 游戏逻辑 第 1 章(世界观) Actor Tick / 移动 / 伤害 服务器权威计算 客户端:预测 + 输入上报 (网关架构第 8 章:登录后进游戏) ④ 复制决策 分发架构(第 7 章) ServerReplicateActors(决策链四步) NetUpdateFrequency 门控 优先级排序 / 相关性判定 RepGraph:空间网格替代挨个查 Dormancy:休眠的 Actor 不派件 ⑤ 序列化出包 同步+RPCA(第 5/6 章) FRepLayout:只发变化的属性 NetSerialize 量化压缩 ProcessRemoteFunction(RPC 分发) FOutBunch 打包(可靠/部分标志) ⑥ 出厂到客户端 通信+网络(第 4/3 章) PacketHandler 出向流水线 加密 / 压缩 / 打包 令牌桶限速(QueuedBits) UDP 发出

客户端:收包 → 应用状态(ReceiveProperties→OnRep)

客户端 ACK / 输入 / 纠偏确认 → 服务器下一帧(拥塞控制:FNetworkCongestionControl)

第 3 章
第 4 章
第 1/8 章
第 7 章
第 5/6 章
第 4/3 章
左侧竖轴:客户端进门(网关架构 第 8 章:Hello→Challenge→Login→Welcome→Join)

读图方法:一帧的服务器旅程——① 收到客户端的包(网络层认路)→ ② 拆包验包(通信层安检)→ ③ 游戏逻辑跑起来(世界观)→ ④ 决定”谁该收到什么”(分发层决策)→ ⑤ 把变化序列化成束(同步+RPC)→ ⑥ 出厂发回客户端(通信+网络)。底部回环是下一帧的燃料:客户端的 ACK 与输入。六块 = 六章,图会随阅读展开。

0.5 章节地图

章节 一句话
开篇 第 0~1 章 怎么读 + UE 网络世界观(权威服务器、两大手段)
概念篇 第 2 章 六架构速览(每个架构一个类比)
机制篇 第 3~8 章 网络 / 通信 / 同步 / RPC / 分发 / 网关 六大架构逐章拆解
专题篇 第 9~11 章 移动端优化 / 大世界分发 / 调试工具链
调优篇 第 12~13 章 CVar 速查与常见坑 / 局限与展望
附录 A~D 源码地图、术语表、参考资料、自测题

第 1 章 网络世界观:权威服务器与两大手段

TL;DR|UE 网络的世界观是”服务器说了算“:一切游戏状态以权威服务器为准,客户端只能
“建议”(发输入)并”先斩后奏”(本地预测)。四种运行模式本质只有两类——服务器与客户端。
同步靠两大手段属性复制 = 信件抄送(状态定期送达)与 RPC = 电话(事件即时触发)。
一句话:服务器是真相,客户端是建议者

1.1 两种世界观:状态同步 vs 帧同步

多人游戏同步有两种主流哲学:

  • 帧同步(Lockstep):所有客户端跑同一套确定性模拟,只同步”输入”——大家吃同样的输入、算同样的结果。优点是带宽极小;缺点是任何非确定性都会导致分歧(浮点差异、随机数、物理),且 RTT 高的玩家拖慢所有人(要等输入)。
  • 状态同步(State Sync):服务器跑权威模拟,把状态变化广播给客户端;客户端自行渲染(可预测)。带宽更高,但鲁棒:客户端掉帧、作弊、非确定性都不影响服务器。

UE 选的是状态同步(官方文档措辞:”The server is where the game actually happens”)。理解这个选择就理解了 UE 网络的一切设计:服务器是真相源,客户端是”远程操控的视图”。

P2P vs 权威 C/S:UE 引擎层没有原生 P2P——所谓”P2P 游戏”(如大厅邀请制)是某客户端以 ListenServer 身份建主机,配合 OnlineSubsystem 会话实现的。权威服务器仍然是唯一真相源。

1.2 权威模型:服务器是唯一真相

权威服务器(Authoritative Server)的含义:

  1. 服务器运行”真正的游戏”——伤害计算、移动结果、拾取判定全在服务器;
  2. 客户端发来的东西是建议(Request),不是事实——客户端说”我跑到 (100,50) 了”,服务器验证后说”行”或”不对,你在 (98,50)”;
  3. 客户端为了手感预测(Prediction)——先本地模拟,服务器纠偏时再改正(第 8.7 章)。

类比实验室:先斩后奏
想象你向老板汇报工作:你先按自己的判断把活干了(预测),老板每周五核对一遍(服务器纠偏),
错了就改(纠偏/回滚),对了就记下(确认)。老板是唯一权威——你的判断只是建议。
UE 的角色移动就是这样:客户端本地先跑(手感顺滑),服务器每帧校验(真相),
不一致时发”纠偏包”把客户端拽回正确位置(ClientAdjustPosition)。

1.3 四种模式:本质只有两类

ENetModeEngineBaseTypes.h:978-996),枚举注释原话:”every mode less than NM_Client is a kind of server“(小于 NM_Client 的都是服务器变体):

模式 是什么 谁在跑游戏逻辑
NM_Standalone 单机(无网络) 本机(唯一真相)
NM_DedicatedServer 专用服务器 无渲染的服务器进程
NM_ListenServer 主机兼玩家 服务器进程 + 本地玩家
NM_Client 客户端 只跑本地预测/渲染

关键推论UNetDriver::IsServer()(NetDriver.cpp:2594)= ServerConnection == NULL——没有”连出去的连接”就是服务器。Dedicated 服务器没有本地玩家、NetServerMaxTickRate 限帧(NetDriver.h:875-888);Listen 服务器同时是客户端(有本地玩家);Standalone 根本没有 NetDriver。

1.4 Actor 的网络角色

不是所有 Actor 都参与网络——Actor 的网络身份由几个属性决定(Actor.h:593-914):

属性 含义
bReplicates 参与复制(服务器→客户端)
bAlwaysRelevant 对所有连接始终相关(不看距离)
bOnlyRelevantToOwner 只发给拥有者(PlayerController/Pawn 常用)
bNetTemporary 一次性复制(生成后不更新,如子弹特效)
bNetStartup 关卡加载时存在(Level 里的静态 Actor)
NetUpdateFrequency 每秒最多复制几次(默认 100)
NetPriority 带宽紧张时谁优先(默认 1.0)
NetDormancy 休眠策略(第 7.8 章)

避坑NetUpdateFrequency/NetPriority 从 UE 5.5 起公开访问已废弃
(Actor.h:903-914 带 UE_DEPRECATED 注释),必须用 SetNetUpdateFrequency()/
GetNetUpdateFrequency() 访问器。旧教程直接赋值会编译告警。

复制的最小单位是属性UPROPERTY(Replicated) 标记的属性,配合 DOREPLIFETIME 宏注册——服务器变了它,客户端自动收到。这是第 5 章的主线。

1.5 两大手段对比:什么时候用哪个

属性复制(Replication) RPC(远程函数调用)
类比 信件抄送 电话
语义 状态(”这个值现在是 X”) 事件(”这件事发生了”)
方向 服务器 → 相关客户端 双向(Server/Client/Multicast)
频率 按 NetUpdateFrequency 节流 每次调用立即发送(可攒批)
可靠性 天然可靠(最新值覆盖旧值) 默认不可靠,bReliable 修饰
数据量 只发变化的(changelist) 参数全量序列化
适用 位置、血量、装备、游戏状态 开火、开门、换武器、聊天

决策口诀:**”是状态还是事件?”**——血量是状态(复制);”被打了一枪”是事件(RPC)。二者可以配合:RPC 触发瞬间事件,属性复制跟进持续状态。

1.6 一帧的生命周期:收 → 想 → 发

图 F2 一帧的服务器/客户端网络生命周期(锚点:LevelTick.cpp:1926-1938)
服务器一帧:
  TickDispatch(收包)  ← 处理客户端发来的输入/请求(LevelTick 早期)
  Actor Tick(想)      ← 游戏逻辑跑起来(移动/伤害/AI)
  TickFlush(发包)     ← ServerReplicateActors 决定并发出复制(LevelTick.cpp:1930 附近)

客户端一帧:
TickDispatch(收包) ← 收到服务器的复制(属性/RPC)
Actor Tick(想) ← 本地预测 + 渲染
TickFlush(发包) ← 发出输入(移动上报)/ Server RPC

帧序保证:收包永远在发包之前 → 服务器先看到上一帧的输入,再决定本帧的真相

UNetDriver::TickFlush(NetDriver.cpp:1097)里:Iris 优先InternalIrisUpdateTransactional,:1141),否则 legacy ServerReplicateActors(:1159)。第 3~7 章全部围绕”TickFlush 里发生了什么”展开。

1.7 小结

这一章记住三句话:

  1. 服务器是真相,客户端是建议者(状态同步 + 权威模型);
  2. 四种模式本质两类:服务器(没有 ServerConnection)与客户端;
  3. 两大手段:属性复制传状态、RPC 传事件。

下一章,六个架构各配一个类比,建立全书地图。

第 2 章 六架构速览(每个架构 = 一个类比)

TL;DR|UE 网络拆成六个子系统,每个配一个你早就懂的东西:网络 = 邮局体系
通信 = 安检流水线同步 = 信件抄送RPC = 电话分发 = 分区快递员
网关 = 门卫与换乘站。这一章只立类比不给代码——每个架构在源码里的落点
在第 3~8 章展开,末尾给”概念 → 源码”预告表。

2.1 网络架构 = 邮局体系

定义:连接与通道体系——UNetDriver(邮局总局)管着所有 UNetConnection(邮路),每条邮路上若干 UChannel(邮袋:控制袋/语音袋/数据袋/Actor 袋),数据以 Bunch(信件)为单位流动,NetGUID(门牌号)标识收件对象。

图 F3 邮局体系层级(第 3 章展开)
  邮局总局 UNetDriver(服务端一份,管所有邮路)
  ├─ 邮路 UNetConnection(每玩家一条;客户端只有一条 ServerConnection)
  │   ├─ 邮袋 UChannel#0 Control(握手/控制消息)
  │   ├─ 邮袋 UChannel#1 Voice(语音)
  │   ├─ 邮袋 UChannel#2 DataStream(Iris 数据流,本 fork 特性)
  │   └─ 邮袋 UChannel#N Actor(每个被复制的 Actor 一个,动态开/关)
  │        └─ 信件 Bunch(属性变化 / RPC / 生成信息)
  │             └─ 门牌号 NetGUID(标识"这封信发给哪个对象")
  └─ 限速:令牌桶 QueuedBits(每帧恢复带宽配额,超额等下一帧)

它在这条流水线里的位置:一切的骨架——数据从哪进(ReceivedPacket)、往哪出(FlushNet)、走哪条邮袋(Channel 分发)、多快(令牌桶)。

2.2 通信架构 = 安检流水线

定义:PacketHandler 体系——每个连接一条”安检流水线”(组件链:无状态握手 → 加密 → 压缩 → Oodle),出包时依次”盖章、封装、称重”,进包时反向”验真、解密、拆封”。连接建立前还有一道”验明正身”:无状态握手用 cookie 挑战应答防 UDP 放大攻击。

类比实验室:机场安检
出包 = 行李托运:安检(StatelessConnect 验身份)→ 盖章(加密)→ 装箱(压缩)→ 称重(限速)。
进包 = 到达提取:拆箱 → 验章 → 过检。所有行李走同一条传送带(PacketHandler 流水线),
顺序不能乱——这就是”组件链”的含义。

它在这条流水线里的位置:网络层(第 3 章)与游戏层之间的一层”翻译与安检”——第 4 章。

2.3 同步架构 = 信件抄送 + 改动日记

定义:属性复制体系——服务器为每个类型建一张 FRepLayout(”这封信格式怎么排”的类型蓝图),为每个对象记一本 changelist 改动日记(只记”哪个属性变了”,不记值),为每个连接备一份 FRepState 收件档案(上次发到哪)。只在变化时发差值,首帧发全量,客户端收完触发 OnRep 通知

类比实验室:公司周报抄送
服务器每周给所有分公司(连接)发周报:周报只写”这周改了什么”(changelist),
不重发全文;新分公司入职第一周收到”全部历史”(首帧全量);分公司收到周报后
通知各科室(CallRepNotifies 触发 OnRep)。只发差异,是带宽的生命线

它在这条流水线里的位置:第 5 章——“⑤ 序列化出包”的前半(状态部分)。

2.4 RPC 架构 = 电话

定义:远程函数调用——ProcessRemoteFunction 是总机:先查”这个号能不能打”(销毁/撕裂/拦截),再分流 Iris 新线路或 legacy 老线路;四种线路(Server 上行 / Client 下行 / Multicast 广播)+ Reliable 可靠修饰。Server RPC 在服务器本地调用时直接本地执行(不绕电话线)。

类比实验室:电话与总机
你拨号(调用 RPC)→ 总机接线员(ProcessRemoteFunction)查三件事:
① 这个号存在吗(Actor 活着吗)② 这个号打得通吗(Channel 开了吗,没开先”强制复制”
把 Actor 通道建起来)③ 走哪条线(本地直拨 / 专线 / 广播)。电话可以挂
(不可靠 RPC 丢了就算了)也可以要求回执(bReliable 可靠 RPC 保证送达)。

它在这条流水线里的位置:第 6 章——“⑤ 序列化出包”的后半(事件部分)。

2.5 分发架构 = 分区快递员

定义:复制决策体系——服务器每帧跑四步决策链(准备连接 → 按频率筛候选 → 按优先级排序 → 按相关性派件);Replication Graph 把 O(N×M) 挨个检查换成预建的空间网格(每个 viewer 只查自己所在格子);不动的 Actor 休眠(Dormancy)停止派件,动了再唤醒。

类比实验室:分区快递员
传统方式:每个快递员每天挨家挨户问”这家人要不要收件”(O(N×M))。
RepGraph 方式:城市划成网格,每个快递员只负责自己格子里的住户(空间网格),
长期没快递的住户标记”休眠”(Dormancy)——不问了,有包裹了再叫醒(FlushNetDormancy)。

它在这条流水线里的位置:第 7 章——“④ 复制决策”整块。

2.6 网关架构 = 门卫与换乘站

定义:入口与旅程体系——门卫是 GameMode(PreLogin → Login → PostLogin 验票发门卡),带外专线是 Beacon(游戏服务器与匹配/大厅服务的旁路连接),大巴换乘是 SeamlessTravel(无缝换图),进门后 PlayerController 是玩家在服务器上的替身,角色移动靠先斩后奏(客户端预测 + 服务器纠偏)。

类比实验室:机场航站楼
安检(StatelessConnect 握手)→ 值机(GameMode Login)→ 登机口(PlayerController 生成)
→ 转机(SeamlessTravel 无缝换图)→ 飞机晚点纠偏(客户端预测被服务器纠正)。
还有一条”员工通道”(Beacon):机场与航空公司内部系统之间的专线,不走旅客流程。

它在这条流水线里的位置:第 8 章——从”连上服务器”到”进游戏玩起来”的全部旅程。

2.7 六架构串起来:一帧的旅程 + 落点预告

对照图 F1 走一遍:客户端进门(网关,Ch8)→ 每帧收包拆包(网络+通信,Ch3/4)→ 游戏逻辑(Ch1)→ 服务器决策发什么(分发,Ch7)→ 序列化状态与事件(同步+RPC,Ch5/6)→ 出厂(通信+网络,Ch4/3)→ 客户端应用状态触发 OnRep(Ch5)→ ACK 与输入回流(Ch3 拥塞控制)。

架构 类比 核心类/函数 章节
网络 邮局体系 UNetDriver / UNetConnection / UChannel / FNetPacketNotify Ch3
通信 安检流水线 FPacketHandler / HandlerComponent / StatelessConnectHandlerComponent Ch4
同步 信件抄送 FRepLayout / FRepChangelistState / FRepState / CallRepNotifies Ch5
RPC 电话 ProcessRemoteFunction / FObjectReplicator / QueueRemoteFunctionBunch Ch6
分发 分区快递员 ServerReplicateActors / UReplicationGraph / Dormancy Ch7
网关 门卫与换乘站 AGameModeBase / AOnlineBeacon / SeamlessTravel / 客户端预测 Ch8

第 3 章 网络架构:连接与通道

TL;DR|UE 网络的骨架:NetDriver=邮局总局管着所有 Connection(邮路),每条邮路上
若干 Channel(邮袋),数据以 Bunch(信件) 为单位流动。客户端连服务器要过
六关握手(Hello→Challenge→Login→Welcome→Netspeed→Join);之后每条邮路用
14 位包序号 + ACK 历史位图保证可靠,用令牌桶限速。

3.1 UNetDriver:邮局总局

UNetDriverNetDriver.h:809)是网络的总管家,头文件自带官方架构文档(:34-324 的注释)。核心职责:

1
2
3
4
// NetDriver.h 头注释(节选翻译):
// UNetDrivers 负责管理一组 UNetConnection 以及它们之间共享的数据。
// 服务端 NetDriver 维护一个 NetConnection 列表,每个连接代表一个在游戏中的玩家。
// 客户端 NetDriver 只有一个 NetConnection,代表到服务器的连接。
  • 连接管理:服务端 ClientConnections(每玩家一条)+ FConnectionMap(IP→连接,:980);客户端单个 ServerConnection(:970);
  • 通道工厂ChannelDefinitions/ChannelDefinitionMap(可配置通道类型表,:1038-1044,来自 BaseEngine.ini 的 +ChannelDefinitions);
  • 五个职责虚函数(贯穿全书的钩子):
    • InitConnect / InitListen —— 客户端/服务端启动;
    • TickDispatch —— (每帧最早);
    • TickFlush —— (每帧最晚,内部 Iris 优先否则 ServerReplicateActors);
    • ServerReplicateActors —— 复制决策(第 7 章);
    • ProcessRemoteFunction —— RPC 总机(第 6 章)。

3.2 连接:UNetConnection

UNetConnectionNetConnection.h:283,继承 UPlayer——连接天然关联玩家)是一条端到端邮路。核心成员(:283-380 区域):

成员 职责
Channels[] 通道表(每槽一个 UChannel,槽号 = ChIndex)
OutReliable[] / InReliable[] 每通道的可靠序列计数
PacketNotify 包级可靠/ACK(FNetPacketNotify)
SendBuffer 发送缓冲(攒包)
PackageMap 对象↔NetGUID 映射
Handler PacketHandler 流水线(第 4 章)
QueuedBits 令牌桶(带宽限速)

InitBase(NetConnection.cpp:488)里创建 UPackageMapClientInitHandler()——一条连接建好时,网络身份(PackageMap)与通信管道(Handler)同时就位

3.3 通道体系:UChannel

UChannelChannel.h:63)是双向通道基类:InRec/OutRec 可靠收/发队列、InPartialBunch 部分束重组缓冲。

通道槽位(EChannelType,Channel.h:25-36;注册于 BaseEngine.ini:1841-1844):

ChIndex 通道 谁开 用途
0 Control 客户端开(bClientOpen) 握手与控制消息(NMT_*)
1 Voice 双方 VoIP 语音
2 DataStream 双方 Iris 数据流(本 fork 特性)
3+ Actor 服务器开(bServerOpen) 每个被复制 Actor 一个,动态分配

CHTYPE_MAX = 8(本 fork,上游 7)——多出的 1 位为 Iris 预留。Actor 通道是服务器主动开的(服务器决定谁值得复制),这呼应第 7 章的”分发架构”。

3.4 连接状态机与握手协议

连接状态(EConnectionState,NetConnection.h:94-101):USOCK_Invalid → Closed → Pending → Open → Closing。握手期间处于 Pending,完成后 Open。

游戏层握手六关(消息定义 DataChannel.h:173-248,官方流程注释 NetDriver.h:80-183):

图 F4 连接层握手时序(六消息)
 客户端(UPendingNetGame)                服务器(UWorld::NotifyControlMessage)
     │                                          │
     │ ① NMT_Hello(版本/特性位/加密token)       │
     │ ──────────────────────────────────────▶   │
     │                                          │ 版本/特性校验
     │ ② NMT_Challenge(防重放 cookie)          │
     │ ◀──────────────────────────────────────   │
     │ ③ NMT_Login(URL/玩家ID/平台)            │
     │ ──────────────────────────────────────▶   │
     │                                          │ GameMode::PreLogin → WelcomePlayer
     │ ④ NMT_Welcome(地图名/游戏名)             │
     │ ◀──────────────────────────────────────   │
     │ ⑤ NMT_Netspeed(上报带宽)                │
     │ ──────────────────────────────────────▶   │
     │ 加载地图 → TravelCompleted                │
     │ ⑥ NMT_Join                                │
     │ ──────────────────────────────────────▶   │
     │                                          │ SpawnPlayActor → 生成 PlayerController
     │                                          │ → 进入 Actor 通道复制阶段

避坑:这里有两层握手,别混:① 底层 StatelessConnect 无状态握手(第 4 章,
cookie 挑战应答防放大攻击)在 PacketHandler 里;② 上层游戏层握手(本节的 NMT_* 六关)
走 ControlChannel。老教程经常把两层混为一谈——记住”先过安检(无状态握手),再走门卫
(游戏层握手)”。

服务端用 EClientLoginStateNetConnection.h:116-155)+ IsClientMsgTypeValid 校验消息顺序——客户端不能跳关(没 Hello 就 Login 直接拒绝)。

3.5 包级可靠性:14 位序号 + ACK 历史

UE 用 UDP,可靠性自己造。FNetPacketNotify::WriteHeaderNetPacketNotify.cpp:203)的打包头:

图 F5 包头发送布局(FNetPacketNotify::WriteHeader,14-bit Seq + 14-bit AckedSeq + 4-bit HistoryWordCount + 历史位图)
 UDP 数据报(packet)
 ┌──────────┬──────────┬──────────┬──────────────────────┬──────────────┐
 │ Seq 14bit │ AckedSeq 14bit │ HistWC 4bit │ ACK 历史位图(每 word 16 位,1~16 word)│ 数据(bunch 流)│
 └──────────┴──────────┴──────────┴──────────────────────┴──────────────┘
  本包序号      确认到哪号       历史 word 数    哪些包被确认(滑动窗口)
 (乱序检测用)   (丢包检测用)
  • Seq(14 位):本包序号,接收方检测乱序/重复(配合 PacketOrderCache 乱序缓存修正,net.DoPacketOrderCorrection);
  • AckedSeq + 历史位图:告诉对端”你的包我收到哪了、哪些丢了”——丢包由发送方重传可靠数据
  • 另有扩展包头:1 bit 是否有 JitterClockTime + 10 bit 抖动时间、1 bit 服务端帧耗时(供 ping 计算)。

束(Bunch)层可靠性FOutBunch/FInBunchDataBunch.h:29-204)带 bReliable(可靠束,保证送达且有序)、bPartial(大束分片)等标志。部分束重组在 ReceivedNextBunchDataChannel.cpp:769-948)——可靠束必须连续序号到达(ChSequence),不可靠束可以跳。

3.6 带宽管理:令牌桶与自适应频率

令牌桶NetConnection.cpp:5110-5145):每帧按 NetSpeed(默认 2600 bps,下限 1800,InitBase 按 URL LAN/Internet 选项)恢复配额 DeltaBits = NetSpeed * DeltaTime * 8;发包时 QueuedBits += 包字节数*8IsNetReady() 检查饱和——饱和时服务器停止继续发(bIgnoreSaturation 参数)。

自适应更新频率FNetworkObjectInfoNetworkObjectList.h:34-109)的 OptimalNetUpdateDelta 根据实际发包间隔动态调整(net.UseAdaptiveNetUpdateFrequency)——不动的 Actor 自动降频

拥塞控制(本 fork 已含):FNetworkCongestionControlTrafficControl.h:78-98)基于 BDP(带宽×RTT) 计算在途字节上限,net.EnableCongestionControl(默认 0)开启后由 IsReadyToSend 门控发包。

3.7 源码考古:NetDriver 骨架与 TickFlush 的帧位置

源码考古:想看真东西的人进。

① 帧循环锚点LevelTick.cpp:1926-1938

1
2
3
4
// Update net and flush networking.
BroadcastPreTickFlush(RealDeltaSeconds);
BroadcastTickFlush(RealDeltaSeconds); // note: undilated time is being used here
BroadcastPostTickFlush(RealDeltaSeconds);

这段代码在干什么UWorld::Tick所有 Actor Tick 组跑完之后广播 TickFlush——
UNetDriver::InitBase(NetDriver.cpp:2434-2435)把自己绑到这些事件上。收包(TickDispatch)
在帧前、发包(TickFlush)在帧后
——服务器总是”先看输入、再想逻辑、后发真相”。

② TickFlush 的双路径NetDriver.cpp:1097-1159

1
2
3
4
5
6
7
8
9
10
11
12
13
void UNetDriver::TickFlush(float DeltaSeconds)
{
...
if (ReplicationSystem)
{
InternalIrisUpdateTransactional(DeltaSeconds); // Iris 新复制系统(本 fork 已带骨架)
}
else if (ClientConnections.Num() > 0)
{
ServerReplicateActors(DeltaSeconds); // legacy 复制(第 5/7 章主角)
}
...
}

这段代码在干什么Iris 优先,legacy 兜底——ReplicationSystem 存在就走新系统,
否则走 ServerReplicateActors。本 fork 的 Iris 是”骨架已装、默认不启用”(见第 13 章),
本文后面所有机制章讲的都是 legacy 路径——读懂 legacy 是读懂 UE 网络的必修课

③ 通道注册表,BaseEngine.ini:1841-1844(节选):

1
2
3
4
+ChannelDefinitions=(ChannelName=Control, ClassName=/Script/Engine.ControlChannel, StaticChannelIndex=0, bServerOpen=false, bClientOpen=true, ...)
+ChannelDefinitions=(ChannelName=Voice, ClassName=/Script/Engine.VoiceChannel, StaticChannelIndex=1, ...)
+ChannelDefinitions=(ChannelName=DataStream, ClassName=/Script/Engine.DataStreamChannel, StaticChannelIndex=2, ...)
+ChannelDefinitions=(ChannelName=Actor, ClassName=/Script/Engine.ActorChannel, StaticChannelIndex=-1, bServerOpen=true, bClientOpen=false, ...)

这段代码在干什么:通道配置化——类型、槽位、谁主动开都在 ini 里声明,引擎启动时
LoadChannelDefinitions(NetDriver.cpp:801)建成映射表。注意 Actor 通道
StaticChannelIndex=-1(动态分配)且 bServerOpen=true(服务器主动开)——
“服务器决定谁值得被复制”从这里就定死了


第 4 章 通信架构:PacketHandler 与包格式

TL;DR|通信架构管”数据怎么编”:每个连接一条 PacketHandler 安检流水线——出包时
压缩、加密、打包;进包时验真、解密、重组。无状态握手(StatelessConnect)用 cookie
挑战应答”验明正身”,防 UDP 放大攻击;DDoS 检测在外围兜底。可靠性不在这里
——
它在连接层(第 3.5 章),别被旧教程带偏。

4.1 分层模型:数据报 → 束 → 消息 → 游戏语义

单位 谁持有 可靠性
传输 UDP 数据报(Packet) Socket / UNetConnection 无(UDP)
通信 PacketHandler 组件链 UNetConnection::Handler 部分组件可加(已弃用)
通道 Bunch(束) UChannel(InRec/OutRec) 束级可靠(bReliable)
控制 NMT_* 控制消息 UControlChannel 可靠
游戏 属性/RPC UActorChannel / FObjectReplicator 属性天然可靠、RPC 可选

关键理解:UE 用 UDP 是因为游戏数据”最新的才重要”(旧位置包可以丢),但”命令类”数据(开火、切换状态)必须可靠——所以可靠性做在束层而不是包层:FOutPacketTraits(数据报元数据)与 FOutBunch(束标志)是两套东西。

4.2 PacketHandler:安检流水线

FPacketHandlerPacketHandler.h)是发送/接收两向的组件流水线每个连接一个。组件基类 HandlerComponent(:739)的四个虚接口:

接口 方向 含义
Incoming / Outgoing 连接内 正常收发数据(握手完成后)
IncomingConnectionless / OutgoingConnectionless 连接外 无连接数据(握手前的包!)

关键机制:握手完成前(组件 RequiresHandshake),所有包缓冲在 BufferedPacket 里,FPacketHandlerHandshakeComplete 委托触发后才放行——没验明正身,数据不进门

配置装配UPacketHandlerProfileConfigPacketHandlerProfileConfig.h,PerObjectConfig)按 NetDriver 从 ini 读取组件列表([<NetDriverName> PacketHandlerProfileConfig] Components=...)。

避坑:老教程里的 FPacketHandlerProfileManager(4.x 静态管理器)在 5.x 已删除
被上面的 UPacketHandlerProfileConfig 取代。见到旧类名按”旧资料”处理。

4.3 无状态握手:验明正身

StatelessConnectHandlerComponentStatelessConnectHandlerComponent.cpp)是连接建立的第一道门——基于 DTLS 思想的挑战应答:

图 F6 无状态握手(cookie 挑战应答,防 UDP 放大攻击)
 客户端                                  服务器(无状态——不记录任何连接!)
   │  ① InitialPacket(请求连接)            │
   │ ───────────────────────────────────▶   │
   │                                       │ 服务器不分配内存,只发 cookie
   │  ② Challenge(cookie = HMAC(客户端IP+secret))│
   │ ◀───────────────────────────────────   │
   │  ③ Response(回显 cookie)             │
   │ ───────────────────────────────────▶   │
   │                                       │ 校验 cookie 通过 → 才创建连接
   │  ④ Ack                                 │
   │ ◀───────────────────────────────────   │
   │  握手完成 → 数据放行(BufferedPacket 清空)│

为什么”无状态”:服务器在验证通过前不分配任何连接资源——攻击者伪造源 IP 发一堆连接请求,服务器只回 cookie 不回内存(64 字节 secret + 20 字节 cookie),UDP 放大攻击(DDos)被掐死在门口。握手版本到 v4(EHandshakeVersion,含 SessionClientId / CL 版本早期拒绝)。

4.4 加密与压缩

  • FEncryptionComponentEncryptionComponent.h):加密组件抽象,EnableEncryption/DisableEncryption/SetEncryptionData;驱动流程在控制通道的 NMT_EncryptionAck 消息;
  • Oodle 压缩插件:本 fork 开关是 net.OodleServerEnableMode / net.OodleClientEnableModeOodleNetworkHandlerComponent.cpp:169-174,默认空串)+ ini 段 bEnableOodle
  • 可靠性组件已弃用ReliabilityHandlerComponentUE_DEPRECATED(5.3) 标记——可靠性由束层与连接层承担,别在 PacketHandler 里找可靠传输

避坑:上游 UE 5.x 曾宣传的 r.ServerUseOodle CVar 在本 fork 不存在(grep 全库无匹配)——
Oodle 配置一律走 net.OodleServerEnableMode/ClientEnableMode + ini。

4.5 安全兜底:DDoS 与 RPC 滥用检测

Net/Core 模块提供外围防护(Net/Core/Misc/DDoSDetection.h):按时间窗口统计包速率,超限丢包/断连;RPCDoSDetection 专门防”客户端疯狂调 Server RPC”;违规触发 NMT_SecurityViolation 控制消息。

4.6 源码考古:PacketHandler 的骨架

源码考古:想看真东西的人进。

PacketHandler.h:739 附近(HandlerComponent 基类,节选):

1
2
3
4
5
6
7
8
9
10
11
class HandlerComponent
{
public:
// 以下四个虚接口是组件挂进流水线的"接口位"
virtual void Incoming(FBitReader& Packet) = 0; // 入向:收到数据
virtual void Outgoing(FBitWriter& Packet) = 0; // 出向:发出数据
virtual void IncomingConnectionless(FBitReader& Packet) = 0; // 无连接入向(握手期)
virtual void OutgoingConnectionless(FBitWriter& Packet) = 0; // 无连接出向(握手期)
virtual bool RequiresHandshake() const { return false; } // 是否需要握手完成才放行
...
};

这段代码在干什么:组件 = 实现了四个接口的”传送带工位”——入向加工(解密/验签/解压),
出向加工(压缩/加密/打包)。RequiresHandshake() 返回 true 的组件(如加密)会让
PacketHandler 在握手完成前缓冲所有数据——这就是”没验明正身不进门”的实现
组件链顺序由 Profile 配置决定,加工顺序固定。

② 无状态握手的 cookieStatelessConnectHandlerComponent.cpp(要点):

1
2
3
// 服务器不保存握手状态,一切验证都基于 cookie 本身:
// cookie = HMAC(客户端地址 + ServerSecret),客户端必须原样回显
// 服务器校验通过后才真正创建连接(此前零资源分配)

这段代码在干什么:防放大攻击的全部秘密——验证前零状态、零分配。攻击者伪造一万个
源 IP 发 InitialPacket,服务器回一万个 cookie(每包 ~20 字节),但不创建任何连接对象
攻击者的资源消耗 = 服务器消耗,放大攻击失效。这是 UE 网络的第一道安全闸。


第 5 章 同步架构:属性复制

TL;DR|同步架构管”状态怎么抄送”:服务器为每个类型建一张 FRepLayout(类型蓝图)
每个对象记一本 changelist 改动日记、为每个连接备一份 FRepState 收件档案——
只在属性变化时发差值,首帧发全量,客户端收完触发 OnRep 通知
类比:周报抄送——只写”这周改了什么”,不重发全文

5.1 问题与心智模型

服务器的状态要复制给所有客户端,三个问题:复制什么(类型知道有哪些属性)、复制哪些(对象知道哪些变了)、复制给谁(连接知道上次发到哪)。FRepLayout 体系用三张表回答。

5.2 三张表(本章核心)

图 F7 三张表:类型蓝图(FRepLayout)/ 改动日记(FRepChangelistState)/ 收件档案(FRepState)
 FRepLayout(每类型一份,全局共享)          FRepChangelistState(每对象一份,全局共享)
 ┌────────────────────────────┐            ┌────────────────────────────┐
 │ Parents:顶层属性           │            │ ChangeHistory[64]:环形日记 │
 │   Health(COND_None)       │            │   [0]: {Health, Ammo}       │
 │   Ammo(COND_InitialOnly)  │            │   [1]: {Health}             │ ← 只记"哪个
 │   Inventory(动态数组)     │            │   [2]: {}                   │   属性变了"
 │ Cmds:叶子命令(展开子树)    │            │ StaticBuffer:shadow 数据   │
 │ BaseHandleToCmdIndex:      │            │ CompareIndex:本帧比较标记   │
 │   handle → cmd 索引表       │            └────────────────────────────┘
 └────────────────────────────┘                        │ 每连接从
                                                       │ HistoryEnd 拉取
 FRepState(每对象 × 每连接一份)                        ▼
 ┌────────────────────────────┐            ┌────────────────────────────┐
 │ FSendingRepState:          │            │ FReceivingRepState:       │
 │   LastChangelistIndex       │            │   StaticBuffer(shadow)   │
 │   LastCompareIndex          │            │   RepNotifies(待触发OnRep)│
 │   ChangeHistory[32]         │            │   GuidReferencesMap        │
 └────────────────────────────┘            └────────────────────────────┘
  • FRepLayoutRepLayout.h:1880-1909):属性树——Parents 是 DOREPLIFETIME 注册的顶层属性,Cmds 是展开后的叶子命令(结构体成员递归展开、动态数组按元素展开、有原生 NetSerialize 的结构体只生成单条命令),BaseHandleToCmdIndex 是”属性句柄 → 命令索引”的译码表。每类型一份,所有对象共享FRepLayout::CreateFromClass/CreateFromStruct/CreateFromFunction,:1189);
  • FRepChangelistState(:433):每对象一份、跨连接共享——ChangeHistory[64] 环形 changelist 缓冲(只记 handle 集合)、StaticBuffer(上次比较的 shadow 数据)、CompareIndex(本帧比较过的标记,防重复比较);
  • FRepState(:671):每对象 × 每连接——发送侧(FSendingRepState:LastChangelistIndex、ChangeHistory[32])与接收侧(FReceivingRepState:StaticBuffer、RepNotifies)。

优化机制FReplicationChangelistMgr(:501)用 LastReplicationFrame 保证同帧多连接只比较一次(第一个连接比较,其余连接从共享 changelist 拉取)——这就是”服务器 100 连接复制同一个 Actor 只花一次比较成本”的由来。

5.3 服务器侧入口链:三条跳板

1
2
3
UActorChannel::ReplicateActor(DataChannel.cpp:3599)     ← 通道级:打包一个 Actor 的全部更新
→ FObjectReplicator::ReplicateProperties_r(DataReplication.cpp:1927) ← 对象级:主复制对象 + 子对象
→ FRepLayout::ReplicateProperties(RepLayout.cpp:1972) ← 类型级:按 changelist 写出属性

ReplicateProperties_rDataReplication.cpp:2007)里两件关键事:

1
2
3
4
5
6
const ERepLayoutResult UpdateResult = FNetSerializeCB::UpdateChangelistMgr(
*RepLayout, SendingRepState, *ChangelistMgr, Object,
Connection->Driver->ReplicationFrame, RepFlags, bForceCompare);
// ① 更新 changelist:比较 shadow 数据,把变化的属性 handle 写进日记(每帧每对象一次)
const bool bHasRepLayout = RepLayout->ReplicateProperties(...);
// ② 按 changelist 写出:从上次发到的位置起,把变化的属性序列化进 bunch

5.4 变化追踪:只记 handle,不记值

FRepLayout::ComparePropertiesRepLayout.cpp:1777)——头注释点破关键(RepLayout.h:1142-1148):changelist 只记录”哪些 handle 变了”,不含值(值在 shadow 里,发的时候才读)。逐属性比较在 CompareParentProperties(:1514):

  • PushModel 分支WITH_PUSH_MODEL):只遍历 MARK_PROPERTY_DIRTY 标记过的属性(游戏代码主动声明”我改了”),不逐字节比较——省 CPU 但要求代码纪律;
  • 非 PushModel:逐属性 IsPropertyDirty 比较 shadow 与当前值——服务器每帧全量比较所有复制属性(CPU 成本换代码简单)。

首帧全量UActorChannel::ReplicateActorRepFlags.bNetInitial = true(DataChannel.cpp:3800)→ bForceInitialDirty(:3825)→ 所有属性强制入 changelist;COND_InitialOnly 属性只在首帧比较(之后不再比较)。

避坑FRepChangedPropertyTracker位置变了——它在 NetCore 模块
Net/Core/PropertyConditions/RepChangedPropertyTracker.h),且已退化为
“条件激活状态跟踪器”(脏属性跟踪改由 changelist 承担)。旧教程(5.0 时代)
把脏跟踪讲成它的职责——以本仓库为准。

5.5 序列化与共享:只发差异,还只序列化一次

SendProperties_rRepLayout.cpp:2767)逐属性写入:

1
2
// This property changed, so send it
Cmd.Property->NetSerializeItem(Writer, Writer.PackageMap, const_cast<uint8*>(Data.Data));

共享序列化(Shared Serialization):同帧多个连接复制同一个 Actor 时,第一个连接序列化后的字节缓存复用GNumSharedSerializationHit 统计命中),后续连接直接拷贝共享缓冲——序列化成本 O(1) 次/帧/对象,不管多少连接

接收侧FRepLayout::ReceiveProperties_r(:3789)按流中内嵌的 handle 逐条反序列化到对象内存,同时对比 shadow,把变化的属性压入 RepState->RepNotifies(供 OnRep 触发)。

5.6 自定义序列化设施

NetSerialization.h 提供官方序列化工具(:72-127 头注释给出模板约定):

设施 用途
NetSerialize / NetDeltaSerialize 自定义序列化(TStructOpsTypeTraits::WithNetSerializer=true 声明)
FVector_NetQuantize* 位置量化(例:FVector_NetQuantize10 = 10 位尾数 ≈ 0.1 精度)
SafeNetSerializeTArray_* 带长度上限的数组序列化(防恶意客户端撑爆内存)
FFastArraySerializer 数组 delta 同步:只发增删改的元素,不发整个数组(NetDeltaSerialize 驱动)

类比实验室:快递称重压缩
位置数据是”大件”——FVector_NetQuantize 像真空压缩袋(32 位 float → 量化定点,
精度换体积);FFastArraySerializer 像”只寄新买的书,不重寄整柜书”(数组 delta)。
这两个是带宽优化的第一梯队武器(第 9 章专题再用)。

5.7 客户端接收链:从包到 OnRep

1
2
3
4
5
6
7
8
9
ReceivedBunch(DataChannel.cpp:3111)
→ ProcessBunchInternal(:3270)
→ FObjectReplicator::ReceivedBunch(DataReplication.cpp:984)
→ RepLayout::ReceiveProperties(属性落地 + shadow 对比)
→ 字段循环:属性 or RPC(CastField 区分)
→ PostReceivedBunch(:1592):
PostNetReceive() ← 对象级收尾钩子
CallRepNotifies() ← 逐个触发 OnRep_X(RepLayout.cpp:4661)
PostRepNotifies() ← 全部 OnRep 之后

GUID 未映射:属性引用了客户端还没加载的对象(NetGUID 未解析)→ 进 UpdateUnmappedObjects 队列(DataReplication.cpp:2461),等映射后补 PostNetReceive(:2525)——**复制可以”迟到但不缺席”**。

5.8 源码考古:三张表的关键结构

源码考古:想看真东西的人进。

① changelist 只记 handleRepLayout.h:1142-1148(头注释):

1
2
3
// 改动追踪器(FRepChangedPropertyTracker)与 changelist 的职责划分:
// changelist 只记录"哪些属性句柄在本帧发生了变化",
// 不记录变化前后的值——值在 shadow 数据里,发送时读取。

这段代码在干什么:注释直接回答”为什么带宽小”——日记只写”谁变了”,值不重复存
一帧 100 个属性只有 3 个变了 → changelist 3 个 handle(每 handle ~1-2 字节),
发送时只序列化这 3 个属性的值。

② 变化比较的 PushModel 分支RepLayout.cpp:1514-1638(要点):

1
2
3
4
5
6
7
8
9
10
11
if (bUsePushModel)
{
// PushModel:只检查游戏代码显式标记"脏"的属性(MARK_PROPERTY_DIRTY)
PushProperties = PushModelState->GetDirtyProperties();
...
}
else
{
// 非 PushModel:逐属性比较 shadow 与当前值(CPU 全量扫描)
CompareProperties_r(SharedParams, StackParams);
}

这段代码在干什么:两种脏追踪策略的岔路口——PushModel 靠”主动申报”(省 CPU,贵在
代码纪律:每次改属性都要 MARK_PROPERTY_DIRTY),默认路径靠”被动比较”(省心,贵 CPU)。
大世界服务器属性多时 PushModel 收益显著(第 10 章专题)。

③ OnRep 触发RepLayout.cpp:4661(要点):

1
2
3
4
// 对 RepNotifies 里每个变化的属性:
// 查属性的 RepNotifyFunc(UPROPERTY(ReplicatedUsing=OnRep_X) 声明的函数名)
// → Object->FindFunction(RepNotifyFunc) → ProcessEvent 调用
// 这就是客户端"收到属性变化后自动跑一段代码"的机制

这段代码在干什么ReplicatedUsing=OnRep_Health 的魔力来源——属性落到客户端后,
引擎查函数、调用它。OnRep 是客户端钩子(服务器自己改属性不触发 OnRep),
常用来做”属性变化后的表现”(血条更新、声音播放)。

第 6 章 RPC 架构:远程函数调用的全链路

TL;DR|RPC 是电话:四种线路(Server 上行 / Client 下行 / Multicast 广播)× Reliable
可靠修饰。总机是 ProcessRemoteFunction:先查”能不能打”(销毁/撕裂/拦截),再分流
Iris 新线路或 legacy 老线路;参数按函数级 RepLayout 序列化,不可靠多播先攒批再限流发送。
Server RPC 在服务器本地调用时直接本地执行——不绕电话线。

6.1 RPC 与属性复制的分工

第 1.5 章的决策口诀:”状态用复制,事件用 RPC“。RPC 的声明方式:

1
2
3
UFUNCTION(Server, Reliable)  void ServerFireWeapon();       // 客户端→服务器(可靠)
UFUNCTION(Client) void ClientShowHitmarker(); // 服务器→拥有者
UFUNCTION(NetMulticast) void MulticastExplosion(); // 服务器→所有相关客户端

四种类型由 UFunction 的 FunctionFlags 决定:FUNC_NetServer | FUNC_NetClient | FUNC_NetMulticast | FUNC_NetReliable

6.2 四种类型矩阵

Server(上行) Client(下行) Multicast(广播)
方向 客户端 → 服务器 服务器 → 拥有者 服务器 → 所有相关客户端
谁执行 服务器 拥有该 Actor 的客户端 所有客户端 + 服务器本地
典型用途 移动上报、开火请求 击杀回执、UI 通知 爆炸、开门动画
权限校验 服务器验证(防作弊) 拥有者校验 无(只收)
本地行为 服务器本地调用 = 直接执行 服务器调用不本地执行 服务器调用立即本地执行

关键特殊性:Multicast 在服务器上立即本地执行(调用者端直接 ProcessEvent,不绕网络)——服务器不需要”广播给自己”;Client RPC 只发给拥有者连接Actor->GetNetConnection())。

6.3 发送链:ProcessRemoteFunction 的总机逻辑

UNetDriver::ProcessRemoteFunctionNetDriver.cpp:8053)是 RPC 的唯一总机,顺序检查:

1
2
3
4
5
6
7
8
9
10
ProcessRemoteFunction(每步处理掉就 return)
① 前置检查:IsActorBeingDestroyed → 丢弃;GetTearOff → 丢弃;SendRPCDel 委托可拦截
② Iris 路径:ReplicationSystem->SendRPC 成功即返回(Iris 启用时绝不回退 legacy)
③ RepDriver 拦截:ReplicationDriver->ProcessRemoteFunction 返回 true 则结束
④ Multicast 广播:遍历 ClientConnections
Actor->IsNetRelevantFor 过滤(可靠 RPC 允许发给通道存活但不可相关的)
BuildSharedSerializationForRPC(多连接共享参数序列化)
每连接 InternalProcessRemoteFunctionPrivate
⑤ 单播:Actor->GetNetConnection() → InternalProcessRemoteFunction
→ ProcessRemoteFunctionForChannelPrivate

通道级发送 ProcessRemoteFunctionForChannelPrivate(:3152)的四个关键动作:

  1. Channel 未开 → 先强制复制(:3179-3220):Ch->SetForcedSerializeFromRPC(true); Ch->ReplicateActor(); —— RPC 能”逼”服务器先开 Actor 通道(连 Actor 都没复制过就收到它的 RPC?不可能——所以先复制一遍,把通道建起来);
  2. 可靠性Bunch.bReliable = (Function->FunctionFlags & FUNC_NetReliable)(:3236);
  3. 参数序列化GetFunctionRepLayout(Function)->SendPropertiesForRPC(...)(:3303)——函数参数也走 RepLayout(每个 UFunction 一份函数级布局);
  4. 队列 vs 立即发(:3326-3341):不可靠的 Multicast 走 QueueRemoteFunctionBunch(攒批),其余直接 Ch->SendBunch

6.4 Multicast 排队与限流

FObjectReplicator::QueueRemoteFunctionBunchDataReplication.cpp:2293):不可靠多播 RPC 攒进 RemoteFunctions 队列,下次属性复制时随 bunch 发出;限流 net.MaxRPCPerNetUpdateDataReplication.cpp:39,默认值写作时核实)——同一 RPC 每帧调用太多次直接丢弃多余的(”开火音效每秒 100 次?发 20 次够了”)。

避坑:UE 5.x 已没有独立的 UFunctionChannel(老教程的核心概念)——RPC 直接在
ActorChannel 的属性流里传输(ReadFieldHeaderAndPayload 循环里用 Cast 区分)。
功能没变,架构变了:**RPC 是”特殊字段”,不是”特殊通道”**。

6.5 接收链:从束到执行

1
2
3
4
5
6
7
8
FObjectReplicator::ReceivedBunch(DataReplication.cpp:984)
→ 属性先落地(ReceiveProperties)
→ 字段循环:CastField<FStructProperty> = 属性 / Cast<UFunction> = RPC
→ ReceivedRPC(:1323):
校验:FUNC_NetServer 且非服务器 → 拒绝(客户端不能执行 Server RPC!)
ReceivePropertiesForRPC(:1415):反序列化参数
未映射 GUID → PendingLocalRPCs 延迟(等对象加载后补执行)
ForwardRemoteFunction → CallProcessEventForReceivedRPC 执行

安全边界:客户端收到 FUNC_NetServer 的 RPC 直接拒绝(:1351-1361)——Server RPC 只能在服务器执行,这保证了”客户端只能建议,不能命令”。

6.6 陷阱与边界

  1. RPC 先于属性到达:客户端收到”捡起武器”的 RPC 时武器对象可能还没复制过来(NetGUID 未映射)——引擎的解法是延迟队列(PendingLocalRPCs),映射后补执行;
  2. 不可靠 RPC 会丢:不修饰 bReliable 的 RPC 丢了就丢了(游戏逻辑不能依赖它);”命令类”(换弹、交易)必须 Reliable;
  3. Server RPC 的验证Server_Validate 函数(WithValidation)让服务器在执行前先验证参数——防作弊的第一道闸(”你说你开了一枪,射程内真有敌人吗?”)。

6.7 源码考古:ProcessRemoteFunction 全分支

源码考古:想看真东西的人进。

NetDriver.cpp:8053-8198(要点结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void UNetDriver::ProcessRemoteFunction(...)
{
// ① 前置检查
if (Actor->IsActorBeingDestroyed()) return;
if (Actor->GetTearOff()) return;
if (SendRPCDel && !SendRPCDel(RPCCallInfo, ...)) return;

// ② Iris 新路径
if (ReplicationSystem && ReplicationSystem->SendRPC(...)) { return; }

// ③ RepDriver(RepGraph)拦截
if (ReplicationDriver && ReplicationDriver->ProcessRemoteFunction(...)) { return; }

// ④ Multicast 广播(服务器→所有相关客户端)
if (Function->FunctionFlags & FUNC_NetMulticast)
{
for (UNetConnection* Connection : ClientConnections)
{
if (Actor->IsNetRelevantFor(...) || (bReliable && Channel->OpenAcked))
{
InternalProcessRemoteFunctionPrivate(...); // 每连接发送
}
}
return;
}

// ⑤ 单播(服务器→拥有者 / 客户端→服务器)
UNetConnection* Connection = Actor->GetNetConnection();
InternalProcessRemoteFunction(...);
}

这段代码在干什么:RPC 的”总机接线表”——五步分流。注意 ④ 的过滤条件:
不可靠多播只发给相关连接,可靠多播可以发给”通道还开着但已不相关”的连接
(保证”开门”这类命令不漏发)。SendRPCDel 委托是游戏代码的拦截点
(做自定义 RPC 过滤/统计)。

② Channel 未开先强制复制(:3179-3220,要点):

1
2
3
4
5
6
7
if (Channel == nullptr)
{
// 通道不存在(Actor 还没复制过)→ 强制先复制一次,把通道建起来
Ch->SetForcedSerializeFromRPC(true);
Ch->ReplicateActor();
...
}

这段代码在干什么:RPC 的”先遣队”——服务器要给客户端发 RPC,但对应 Actor 的通道
还不存在(比如刚生成的对象立刻收到 RPC)——引擎强制复制一遍把通道建好,RPC 随后
到达。这就是”RPC 能创建通道”的机制,也是为什么 RPC 参数里引用的对象必须是
“可能已经复制过”的(否则 NetGUID 没映射,参数进延迟队列)。


第 7 章 分发架构:决策链、Replication Graph 与 Dormancy

TL;DR|分发架构回答”谁该收到什么、什么时候“:服务器每帧跑四步决策链(准备连接 →
按频率筛候选 → 按优先级排序 → 按相关性派件)。Replication Graph 把 O(N×M) 挨个检查换成
预建的空间网格(每个 viewer 只查自己格子);休眠(Dormancy)让不动的 Actor 停止派件,
动了再唤醒。类比:分区快递员——只派自己区的件,休眠包裹睡仓库

7.1 问题定义:决策与序列化的分工

第 5 章解决”怎么编”(序列化),本章解决”给谁发、什么时候发“(决策)。Legacy 路径的决策在 ServerReplicateActors 里,RepGraph 是它的可插拔替代品——两者二选一(ReplicationDriver 接口统一)。

7.2 Legacy 决策链:四步

UNetDriver::ServerReplicateActorsNetDriver.cpp:6205):

图 F8 决策链四步(Legacy 路径)
 ServerReplicateActors(每帧,服务器)
  │
  ├─ ① PrepConnections(:5126)
  │    按 NetClientTicksPerSecond 决定本帧 tick 哪些连接(分摊)
  │    没 tick 到的连接:候选 Actor 标记 bPendingNetUpdate(下帧强制补发)
  │
  ├─ ② BuildConsiderList(:5231)
  │    遍历活跃 Actor:NetUpdateFrequency 门控(没到 NextUpdateTime 跳过)
  │    → CallPreReplication(游戏代码预处理钩子)
  │    → 初始休眠的 Actor 直接移除
  │
  ├─ ③ PrioritizeActors(:5456)
  │    每个 Actor × 每个连接算 Priority(GetNetPriority:距离/朝向/重要度)
  │    → 排序(越重要越先发)
  │
  └─ ④ ProcessPrioritizedActorsRange(:5615)
       逐 Actor × 逐连接:
       IsActorRelevantToConnection(:5681)相关性判定(距离/可见/bAlwaysRelevant)
       → 无通道则创建(:5722 CreateChannelByName(NAME_Actor))
       → Channel->ReplicateActor()(:5758,进入第 5 章序列化)
       → 带宽饱和(IsNetReady)→ 停止本帧继续发(ForceNetUpdate 下帧补)
       → 不再相关 → Channel->Close(Relevancy/TearOff)

相关性判定AActor::IsNetRelevantForActorReplication.cpp:388)的优先级:bAlwaysRelevant / 是 ViewTarget 或拥有者 / bNetUseOwnerRelevancy 委托 / bOnlyRelevantToOwner → 最后按距离(IsWithinNetRelevancyDistanceDistSq < GetNetCullDistanceSquared())。

带宽饱和Connection->IsNetReady()(NetConnection.cpp:2731)检查令牌桶——饱和时停止继续发(这是”优先级排序”的意义:重要的一定先发出去)。

7.3 Replication Graph:分区快递员

动机:Legacy 每帧对每个 Actor × 每个连接做相关性检查 = **O(N×M)**——Fortnite 5 万 Actor × 100 连接 = 500 万次检查/帧。RepGraph 把”检查”变成”预建的数据结构查找“。

位置插件Engine/Plugins/Runtime/ReplicationGraph/,本 fork 引擎版已移除)。

类体系ReplicationGraph.h):

节点类 职责
UReplicationGraphNode(:69) 基类:NotifyAdd/RemoveNetworkActor + GatherActorListsForConnection
UReplicationGraphNode_ActorList(:188) 通用列表节点(手工分组用)
UReplicationGraphNode_GridSpatialization2D(:579) 2D 空间网格:动态/静态/休眠三张表
UReplicationGraphNode_GridCell(:535) 每个格子一个(DynamicNode + DormancyNode)
UReplicationGraphNode_DormancyNode(:485) 格子休眠表
UReplicationGraphNode_AlwaysRelevant(:771) 始终相关节点(PlayerController 等)
UReplicationGraphNode_TearOff_ForConnection(:888) 撕裂节点

空间网格分发ReplicationGraph.cpp:6238):每个 viewer 算出自己所在格子 (Location - SpatialBias) / CellSize只 gather 那个格子的 Actor 列表——远处格子的 Actor 根本不进考虑列表。格子 TTL 过期时通知客户端销毁休眠动态 Actor。

主循环ReplicationGraph.cpp:1112 UReplicationGraph::ServerReplicateActors):

1
2
3
4
5
6
7
8
9
PrepareForReplication → 每连接 GatherActorListsForConnection(全局节点+连接节点)
→ ReplicateActorListsForConnections_Default(:1449):
bDormantOnConnection 跳过(:1523)
ReadyForNextReplication 帧频门控(:1539,ReplicationPeriodFrame 换算)
距离剔除(:1571)/ 距离缩放(:1591)
StarvationFactor 饥饿补偿(:1614,总被饿死的 Actor 提高优先级)
休眠候选优先(:1633)
Algo::Sort 排序
→ ReplicateSingleActor(:2044):复用/创建 UActorChannel → ReplicateActor

帧驱动ReplicationPeriodFrame = NetServerMaxTickRate / NetUpdateFrequency(:1083)——RepGraph 按而非时间调度,服务器 tick 率 30 时 NetUpdateFrequency 100 的 Actor 每 3 帧发一次。

避坑:RepGraph 头注释明说”用 RepGraph 时 IsNetRelevantFor/GetNetPriority
不再被使用“(ReplicationGraphTypes.h)——决策完全由图结构承担。
另外本 fork 的开关是 **Net.RepDriver.Enable**(NetDriver.cpp:7999),
上游的 net.EnableReplicationGraph 不存在。

7.4 Dormancy:休眠的 Actor 停止派件

五状态EngineTypes.h:3645):DORM_Never(永不休眠)/ DORM_Awake(默认,随时可睡)/ DORM_DormantAll(全连接休眠)/ DORM_DormantPartial(按连接)/ DORM_Initial(初始休眠,等唤醒)。

Legacy 链路

1
2
3
4
5
6
7
ShouldActorGoDormant(NetDriver.cpp:5434):条件检查(休眠设置/无通道/时间)
→ UActorChannel::StartBecomingDormant(DataChannel.cpp:4632):bPendingDormancy=true
→ UChannel::Tick(:550-557):bPendingDormancy && ReadyForDormancy → BecomeDormant
ReadyForDormancy(:4604):所有 replicator 无新属性(bLastUpdateEmpty)
且无未 ACK 的 changelist、通道已 OpenAcked
+ net.DormancyHysteresis 迟滞(DataChannel.cpp:114,防抖)
→ BecomeDormant(:4595):Dormant=true; Close(Dormancy)

唤醒AActor::FlushNetDormancyActor.cpp:3120)——DORM_Initial → DORM_DormantAll、重新注册、走 RepDriver 或 legacy 逐连接清休眠标志。**唤醒条件是”对象变了”**(游戏代码主动调 FlushNetDormancy,或 RepDriver 检测到属性脏)。

RepGraph 侧bDormantOnConnection 优先跳过(:1523);DormancyNode 按需把休眠 Actor 拉回列表(ConditionalGatherDormantDynamicActors)。

7.5 启用与切换

1
2
3
4
5
6
7
DefaultEngine.ini:
[/Script/Engine.Engine]
+ReplicationDriverClassName="/Script/MyGame.MyReplicationGraph"
(ReplicationDriver.h:13-14 注释:在引擎 ini 配置复制驱动类)

NetDriver.cpp:1798-1799:
InitReplicationDriverClass(); SetReplicationDriver(UReplicationDriver::CreateReplicationDriver(...));

Net.RepDriver.Enable=1(NetDriver.cpp:7999)+ 自定义 RepGraph 类 → 替换 legacy 决策链。

7.6 源码考古:决策链与休眠判定

源码考古:想看真东西的人进。

① 相关性 + 建通道NetDriver.cpp:5681-5758(要点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const bool bIsRelevant = Actor->IsActorRelevantToConnection(...);   // 相关性判定
...
if (bIsRelevant)
{
UActorChannel* Channel = ActorInfo->Channel;
if (Channel == nullptr)
{
// 相关但没通道 → 创建(这就是 Actor 通道动态分配的时机)
Channel = Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::None);
Channel->SetChannelActor(Actor, ...);
}
Channel->ReplicateActor(...); // 进入第 5 章序列化
}
...

这段代码在干什么:决策链的”派件动作”——相关 → 建通道 → 复制三步。
注意通道是服务器主动创建的:客户端永远不会主动申请 Actor 通道(它只能被动收)。
这也是”服务器权威”在分发层的体现。

② 休眠判定NetDriver.cpp:5434-5560(要点):

1
2
3
4
5
if (IsActorDormant(ActorInfo, WeakConnection)) continue;      // 已休眠(此连接)
if (ShouldActorGoDormant(Actor, Channel, ...))
{
Channel->StartBecomingDormant(); // 进入休眠流程(等最后一包发完)
}

这段代码在干什么:休眠的两个判定点——已休眠的直接跳过(省掉相关性检查),
该休眠的开始休眠(等所有连接都收到最后一包后真正关通道)。Dormancy 是
“停止派件”的开关,能省的不只是带宽,还有每帧的检查成本。

③ RepGraph 的网格 gatherReplicationGraph.cpp:6238-6290(要点):

1
2
3
4
5
6
7
8
9
for (const FNetViewer& CurViewer : Params.Viewers)
{
int32 CellX = (ClampedViewLoc.X - SpatialBias.X) / CellSize; // 格子坐标
int32 CellY = (ClampedViewLoc.Y - SpatialBias.Y) / CellSize;
if (UReplicationGraphNode_GridCell* CellNode = GetCell(CellX, CellY))
{
CellNode->GatherActorListsForConnection(Params); // 只 gather 自己格子!
}
}

这段代码在干什么:O(N×M)→O(格子内 Actor × viewer) 的核心——每个 viewer 只查询
自己所在格子的 Actor 列表
。网格大小(CellSize)是 RepGraph 最重要的调参:
格子太小 → 每帧跨格抖动(Actor 在边界反复进出);太大 → 网格退化回全量检查。


第 8 章 网关架构:登录、Beacon、旅行与预测

TL;DR|网关架构管”怎么进门、换乘、验票“:门卫是 GameMode(PreLogin→Login→PostLogin),
带外专线是 Beacon(游戏服务器与匹配/大厅服务的旁路连接),大巴换乘是 SeamlessTravel
(无缝换图)。进门后 PlayerController 是玩家在服务器上的替身,角色移动靠先斩后奏
(客户端预测 + 服务器纠偏)。注意:**UE 引擎层没有”网关服务器”**——入口职责分散在
GameMode/Beacon/Session/旅行四件套里。

8.1 网关的定位:引擎里没有”网关服务器”

搜索整个引擎:没有名为 Gateway 的服务器代码。UE 的”网关”是职责分散的入口体系

职责 承担者 章节
验明正身(防放大攻击) StatelessConnect 无状态握手 Ch4
验票进门(游戏层登录) GameMode / GameSession 8.2
带外专线(与匹配/大厅通信) Beacon 8.3
找房间(会话匹配) Session(插件化) 8.4
换乘(换图/无缝旅行) ServerTravel / SeamlessTravel 8.5
进门后的身份 PlayerController(替身) 8.6
进门后的行为 客户端预测 + 纠偏 8.7

8.2 登录链:GameMode 验票

AGameModeBaseGameModeBase.h:47,头注释:”只在服务器上实例化,客户端不存在“)的四个登录函数(:286-338):

1
2
3
4
5
6
客户端 NMT_Login 到达(第 3.5 章第③关)
→ GameMode::PreLogin(同步校验:踢人/黑名单/人数上限)
→ GameMode::PreLoginAsync(异步校验:调后端 API 验证)
→ GameMode::Login(生成 PlayerController,绑定 Pawn)
→ GameMode::PostLogin(/ OnPostLogin 委托:通知其他玩家"新人来了")
→ 服务器回 NMT_Welcome → 客户端加载地图 → NMT_Join → SpawnPlayActor

AGameSessionGameSession.h:78/155/183):ApproveLogin(选项/唯一 ID 校验)、KickPlayerRegisterServer(向在线服务注册)。

避坑:登录有两层:连接层握手(NMT_Hello→Join,第 3.5 章,UWorld::NotifyControlMessage
处理)与游戏层登录(GameMode 四个函数,8.2 节)。写 PreLogin 时你处理的是
游戏层——连接层的版本校验早就在 NMT_Hello 时做完了。

8.3 Beacon:带外专线

AOnlineBeaconOnlineBeacon.h:67AActor + FNetworkNotify):游戏服务器与专用服务(匹配、大厅、QoS)之间的旁路连接——不走玩家登录流程。

Beacon 握手(DataChannel.h 内注释的官方流程):NMT_BeaconWelcome → NMT_BeaconJoin → NMT_BeaconAssignGUID → NMT_BeaconNetGUIDAck——Beacon 有自己独立的控制消息流。

Lobby 大厅范例Engine/Plugins/Online/OnlineFramework/Source/Lobby/):ALobbyBeaconHost(:24)/ ALobbyBeaconClient(:58)/ ALobbyBeaconState(:171)——Epic 官方的”分房间/大厅”参考实现:玩家先进大厅(Beacon 连接),凑满人后一起旅行进游戏服务器。

8.4 Session:找房间(已插件化)

IOnlineSession 接口(Create/Find/Join/DestroySession)位于插件OnlineSessionInterface.h);引擎侧 UOnlineSessionOnlineSession.h)已退化为空壳基类(StartOnlineSession 等均为空虚函数)。

避坑:旧教程(UE4 时代)以 UOnlineSession 为主讲会话——现在会话功能全部在
OnlineSubsystem 插件
里(Steam/EPIC/自定义平台各自实现)。引擎只管”给会话留接口”。

8.5 旅行与关卡分发:换乘与下车

  • 硬旅行UWorld::ServerTravelWorld.h:4226)——服务器换图,所有客户端断开重连(清空世界);
  • 无缝旅行SeamlessTravel(:4238)+ FSeamlessTravelHandler(:194)——预加载新图、保留指定 Actor(玩家/控制器),换图不闪断;
  • 关卡流送ULevelStreamingbShouldBeLoaded/Visible 通过 RPC 下发APlayerController::ClientUpdateLevelStreamingStatusPlayerController.h:1450)——服务器说”你该加载 3 号关卡了”;
  • World Partition:服务器 IsServerStreamingEnabledWorldPartition.cpp:1377-1401)做权威校验,客户端用 UWorldPartitionStreamingSourceComponentWorldPartitionStreamingSourceComponent.h:16)按自己位置自主流式加载——大世界分发(第 10 章专题)的伏笔。

8.6 PlayerController:玩家的替身

APlayerController玩家在服务器上的化身PlayerController.h:489 NetConnection 持有者):

  • 唯一性:头注释(:841)”PlayerController 是唯一能承载 RPC 的 Actor 类型“——实际含义:它天然 bNetOwner,是玩家输入与服务器之间的官方通道;
  • Pawn 绑定链ClientRestart(:1153,客户端重建 Pawn)→ ServerAcknowledgePossession(:1481,客户端确认”我控制它了”)→ ClientRetryClientRestart(:1470,失败了重试);
  • SetPawn 三件套Controller.h:246-249):SetPawn(本地/服务器逻辑变更)、SetPawnFromRep(复制到达时执行 + RepNotify)、SetPawn_Direct(无条件直设)——**”服务器决定你控制谁,客户端跟随”**。

8.7 客户端预测:先斩后奏

角色移动是 UE 网络里最精妙的机制(CharacterMovementComponent.h):

图 F9 客户端预测与服务器纠偏(先斩后奏)
 客户端                                            服务器
   │  ① 玩家按 W(本地立即响应)                      │
   │     ReplicateMoveToServer 打包输入              │
   │     SavedMoves 记录"我这样动过"(:3181)        │
   │ ───────────── ServerMove(输入) ─────────────▶   │
   │                                                │ ② ServerMove_Implementation(:10106)
   │                                                │    权威模拟 → 算出正确位置
   │ ◀──────────── ClientAdjustment(纠偏包) ──────── │ ③ 位置不一致才发纠偏
   │  ④ ClientAdjustPosition(:11242)               │
   │     OnClientCorrectionReceived(:11419)        │
   │     截断 SavedMoves:确认的部分删掉             │
   │     不一致的部分回滚重放                        │
   │  ⑤ 若一致:什么都不发生(预测成功,丝滑)        │

关键数据FNetworkPredictionData_Client_Character(:3160)的 SavedMoves(未确认的移动缓冲)+ LastAckedMoveFSavedMove_Character(:2988)是最小移动单元(输入+时间戳)。纠偏频率受 NetworkMinTimeBetweenClientAdjustments(:855-868)控制——别每帧纠偏,给客户端一点容错

为什么这样设计:预测 = 延迟补偿——不预测的话,客户端按 W 到画面动之间有整整一个 RTT 的延迟(100ms 以上),手感像踩泥。先斩后奏让”本地立即动、服务器确认对错”。

8.8 源码考古:GameMode 登录四函数

源码考古:想看真东西的人进。

GameModeBase.h:286-338(节选):

1
2
3
4
5
6
7
8
9
10
/** 玩家尝试进入时调用(同步校验:人数上限/黑名单) */
virtual void PreLogin(const FString& Options, const FString& Address, ...);
/** 异步校验版本(调后端 API 验证,必须回调完成) */
virtual void PreLoginAsync(const FString& Options, const FString& Address,
const FUniqueNetIdRepl& UniqueId, const FOnPreLoginComplete& OnComplete);
/** 通过校验后创建 PlayerController */
virtual APlayerController* Login(UPlayer* NewPlayer, ENetRole InRemoteRole,
const FString& Address, const FString& UniqueId, FString& ErrorMessage);
/** 玩家完全进入后调用(通知其他玩家/初始化个人状态) */
virtual void PostLogin(APlayerController* NewPlayer);

这段代码在干什么:登录四函数的顺序就是”门卫流程”——先拦(PreLogin)→ 异步验票
(PreLoginAsync,可对接账号服务)→ 发门卡(Login 建 PlayerController)→ 欢迎入住
(PostLogin)
。注意 PreLoginAsync 是必调回调的(”must call the delegate”)——
异步流程下服务器会挂起等待,写错会导致玩家永远卡在”进入中”。


第 9 章 专题一:移动端网络优化

TL;DR|移动网络的敌人是延迟、抖动、带宽、弱网。四板斧:量化压缩(FVector_NetQuantize
等)、条件复制(COND_* 只发需要的人)、频率分层(重要对象高频、装饰对象低频)、
服务器降 tick(NetServerMaxTickRate)。另外移动端弱网场景(切后台/重连)有专门应对。

9.1 移动网络的账本

维度 桌面 移动 对策
延迟 20-60ms 60-200ms(蜂窝网络波动大) 客户端预测 + 服务器纠偏
抖动 大(信号切换/拥塞) 缓冲、纠偏容错
带宽 富裕 受限(流量套餐) 量化压缩、条件复制
连接稳定性 弱(切后台/隧道/地铁) 重连机制、NetPing

9.2 分层调参:频率分层

1
2
3
4
角色(自己)        NetUpdateFrequency = 100   每帧,不可省
角色(他人) = 20-30 视觉可接受
投射物/道具 = 15-30
装饰/远物 = 5-10 或 COND_InitialOnly 只发首帧

移动端原则能少发就少发——同一场景,桌面 100Hz 的 Actor 在移动端 20Hz 视觉几乎无差(加上插值),带宽省 80%。

9.3 带宽压缩三板斧

  1. 量化FVector_NetQuantize10/100(10/100 位精度)替代 float 向量——位置精度 0.01/0.001 级足够游戏使用,体积从 96 位降到 30-50 位;
  2. FastArrayFFastArraySerializer 只发数组增删改(掉落物列表、玩家列表)——否则整个数组每帧全量;
  3. 条件复制COND_InitialOnly(只首帧)、COND_OwnerOnly(只拥有者)、COND_SimulatedOnly(只模拟端)——**每个属性问一遍”真的需要发给所有人吗”**。

9.4 服务器 Tick 率

NetServerMaxTickRateNetDriver.h:875-888,BaseEngine.ini [OnlineSubsystemUtils.IpNetDriver] NetServerMaxTickRate=30):服务器每帧的复制决策上限。30 tick 的服务器 = 每秒 30 次复制决策(不是 60)——移动端对延迟敏感度低(玩家在手机上玩),30 是常见选择;竞技类可以 60。

避坑:网上资料说的 r.DedicatedServerMaxTickRate CVar 在本 fork 不存在——正确配置
NetServerMaxTickRate(UNetDriver 属性 + BaseEngine.ini)。见到前者按旧资料处理。

9.5 实战调优路线图(五步法)

1
2
3
4
5
① 看数字    stat net(带宽/包数统计)、NetTrace 抓复制明细
② 看对象 找出 NetUpdateFrequency 最高的 10 个 Actor —— 它们就是带宽大户
③ 分层 按"重要度"分三层定频率(9.2 表)
④ 压缩 位置量化 → 数组 FastArray → 属性 COND_* 检查
⑤ 定案 真机测试(4G/弱网),观察抖动与纠偏频率

9.6 移动端避坑清单

  1. 切后台:App 进后台 UDP 断流——需要重连逻辑(NMT_NetPing 心跳检测 + 服务器保留连接);
  2. 弱网纠偏风暴:抖动大时服务器频繁发纠偏——调大 NetworkMinTimeBetweenClientAdjustments
  3. 流量敏感:不要用可靠 RPC 发高频数据(音量/位置),可靠 RPC 的重传在弱网下会积压——高频用属性复制(最新值覆盖),低频命令才用可靠 RPC;
  4. Ping 显示net.PlayerFacingPingType(NetConnection.cpp:66)控制玩家可见 ping 的计算方式(RoundTripExclFrame 默认)。

第 10 章 专题二:大世界多人分发实战

TL;DR|大世界多人分发的账:Legacy O(N×M) 检查在 100 人 × 5 万 Actor 下爆掉。解法:
RepGraph 空间网格(viewer 只查自己格子)+ Dormancy 分层休眠(远物/静态物睡仓库)+
AlwaysRelevant 清单(PlayerController 等始终相关)+ World Partition 配合(客户端
自主流式加载)。本章给出一套从”能跑”到”能扛”的落地路线。

10.1 成本模型:O(N×M) 的账

Legacy:每帧对每个活跃 Actor × 每个连接做相关性检查。100 连接 × 5 万 Actor = 500 万次检查/帧——即使每次检查 100ns 也要 500ms(远超帧预算)。RepGraph 把检查变成格子查找:每个 viewer 只看自己格子(几十~几百个 Actor)→ 检查量降 2-3 个数量级

10.2 RepGraph 落地

1
2
3
4
5
6
7
8
9
10
11
12
自定义 UReplicationGraph 子类(MyReplicationGraph):
├─ 全局节点:
│ AlwaysRelevant(PlayerController/GameState/所有"必须人人可见"的对象)
│ GridSpatialization2D(其余全部:格子半径 = 网络相关性距离)
│ CellSize 建议:最小 = 相关性距离(避免跨格抖动)
│ DormancyNode(休眠对象归位)
└─ 每连接节点:
AlwaysRelevant_ForConnection(每帧重建:自己的 PC/ViewTarget/Pawn)

DefaultEngine.ini:
[/Script/Engine.Engine]
+ReplicationDriverClassName="/Script/MyGame.MyReplicationGraph"

网格尺寸调参CellSize = 网络相关性距离(通常 100-500m)。太小 → Actor 在格子边界反复进出(抖动+重复复制);太大 → 格子内 Actor 太多(退化)。

10.3 Dormancy 策略

对象类型 建议 Dormancy 理由
PlayerController/Pawn DORM_Never 永远要同步
静态道具(箱子/门) DORM_DormantAll 不动就睡
动态但低频(资源点) DORM_DormantPartial 按连接睡
大世界远景装饰 DORM_Initial 等玩家靠近再唤醒(FlushNetDormancy)

唤醒时机:游戏代码在”对象被交互/状态变化”时调 FlushNetDormancy——别在 Tick 里无条件调(那等于永不休眠)。

10.4 与 World Partition 配合

  • 服务器 IsServerStreamingEnabled 权威校验;客户端 WorldPartitionStreamingSourceComponent 按位置自主加载(第 8.5 章);
  • 流送边界上的 Actor:进入流送区的 Actor 触发”新 Actor 复制”(首次全量)——流送高峰帧会有复制洪峰,配合 Dormancy 让新区域的对象”先休眠后唤醒”;
  • HLOD 替代:远处用简化网格(视觉),但网络复制的是逻辑对象——HLOD 不参与复制(网络只看逻辑 Actor)。

10.5 实测调参路线图

1
2
3
4
5
6
① 抓基线    NetTrace 抓 60 秒,统计每连接带宽/包数/最大 Actor 数
② 建图 按 10.2 搭 RepGraph 骨架(先 AlwaysRelevant + Grid)
③ 看尖峰 大世界移动时观察复制洪峰(流送边界/新区域)
④ 休眠优化 静态对象 DormantAll + 交互时唤醒
⑤ 微调 CellSize、ReplicationPeriodFrame 换算、StarvationFactor
⑥ 定案 100 人压测:带宽/CPU/丢包三指标达标

第 11 章 专题三:调试工具链

TL;DR|网络排障四件套:NetTrace(复制明细追踪,抓”谁发了什么”)、stat net
(实时带宽/包统计)、net 控制台命令(连接/通道/休眠状态)、包捕获(Wireshark 解密
后的完整包流)。最后一节给”症状 → 定位”决策树。

11.1 NetTrace:复制明细追踪

FNetTraceNet/Core/Trace/NetTrace.h):Unreal Insights 的网络追踪——抓取每个属性/RPC 的序列化明细(谁、什么时候、多少字节)。用法:-trace=net 启动或运行时开启,然后在 Insights 里看 Network 视图——**”这个属性为什么每帧都在发?”的直接答案**。

11.2 控制台与统计

命令/统计 看什么
stat net 实时带宽、包数、复制调用次数
net.ShowConnections 各连接状态(模式/延迟/带宽)
net.ShowChannels 各通道状态(开放/队列深度)
net.Dormancy 相关 休眠统计
net.EnableCongestionControl 拥塞控制开关(第 3.6 章)

11.3 症状 → 定位表

症状 大概率原因 检查项
高带宽 高频复制对象太多 / 未量化 / FastArray 未用 NetTrace 找字节大户 → 9.3 三板斧
高延迟(服务器视角) 服务器 tick 率低 / 逻辑重 stat net + 服务器帧耗时
抖动(位置回跳) 预测/纠偏频率不匹配 / 复制频率过低 客户端预测调试(p.VisualizeCorrections)
RPC 丢失 不可靠 RPC 在网络抖动下丢弃 确认语义(丢了可接受?)或加 Reliable
对象不同步 NetGUID 未映射 / 相关性判定错误 NetTrace 看是否发送;客户端日志看 PendingLocalRPCs
卡登录 PreLoginAsync 未回调 / 版本不匹配 服务器日志 NMT_* 顺序

11.4 包捕获:终极手段

Wireshark 抓 UDP 包(端口 7777 默认)——但默认加密+Oodle 压缩后内容不可读。调试环境做法:关闭加密(net.AllowEncryption=0)与压缩,用 Wireshark 的 UE 解析器看结构;生产环境用 NMT_SecurityViolation 日志定位恶意客户端。


第 12 章 平台支持与性能调优

TL;DR|CVar 速查与常见坑。核心旋钮:net.EnableCongestionControl(BDP 拥塞控制)、
NetServerMaxTickRate(服务器决策频率)、Net.RepDriver.Enable(RepGraph 开关)、
net.UseAdaptiveNetUpdateFrequency(自适应频率)。调优三板斧:看统计 → 抓 NetTrace → 单旋钮调参。

12.1 CVar 速查表(写作时以 grep 核实默认值)

连接与包(NetConnection.cpp):

  • net.EnableCongestionControl = 0(BDP 拥塞控制,:105)
  • net.DisableBandwithThrottling = 0(:101,关掉令牌桶——测试用)
  • net.DoPacketOrderCorrection = 1(乱序修正,:88)
  • net.PlayerFacingPingType = RoundTripExclFrame(:66)

复制与分发

  • Net.RepDriver.Enable = 1(RepGraph 开关,NetDriver.cpp:7999)
  • net.UseAdaptiveNetUpdateFrequency(NetDriver.cpp:452)
  • net.MaxRPCPerNetUpdate(DataReplication.cpp:39)
  • net.DormancyHysteresis(DataChannel.cpp:114)

安全与通信

  • net.AllowEncryption(NetDriver.cpp:457)
  • net.OodleServerEnableMode / net.OodleClientEnableMode(Oodle 插件,默认空)
  • net.MagicHeader / net.VerifyMagicHeader(StatelessConnect 握手,:235/:303)

服务器NetServerMaxTickRate(BaseEngine.ini [IpNetDriver] 默认 30,注意这不是 CVar 而是配置属性

12.2 调优工作流

1
2
3
4
5
6
7
1. 看数字    stat net + NetTrace(带宽构成/字节大户/复制频率)
2. 看日志 连接日志(NMT_* 顺序)、通道日志(打开/关闭原因)
3. 调参数 每次只动一个旋钮:
带宽高 → 9.3 三板斧(量化/FastArray/COND_*)
延迟高 → NetServerMaxTickRate、NetUpdateFrequency 分层
抖动大 → NetworkMinTimeBetweenClientAdjustments
人数多 → RepGraph + Dormancy(第 10 章)

12.3 常见坑清单

  1. 属性忘注册UPROPERTY(Replicated) 了但没 DOREPLIFETIME——属性永远不复制;
  2. RPC 忘加 Reliable:”换弹”这类命令不可靠 = 偶尔不换弹;
  3. NetUpdateFrequency 设 0:Actor 永不复制(除非 ForceNetUpdate)——bNetTemporary 的 Actor 尤其容易踩;
  4. 客户端改复制属性:客户端本地改 Replicated 属性会被服务器覆盖——“改了等于没改”的经典 bug;
  5. OnRep 在服务器也跑:误以为 OnRep 服务器也触发(不触发——只在客户端)——服务器逻辑写在函数本体;
  6. RepGraph 与 Legacy 混用:开了 Net.RepDriver.Enable 但没配 ReplicationDriverClassName——空转;
  7. 预测量大:预测逻辑与非预测逻辑混写,纠偏时回滚错对象——预测代码只改”预测专用”状态。

第 13 章 局限、替代方案与展望

TL;DR|UE 网络的局限:权威服务器的延迟代价(每个动作都过服务器)、UDP 的不可靠
(靠自研可靠性补)、序列化 CPU 成本(大世界属性多时显著)、带宽天花板(量化只能缓解)。
方向:Iris(Epic 重写的复制系统,本 fork 已带骨架但默认关闭)、帧同步/回滚(不适合
RPG 但适合格斗/即时策略)、第三方平台(Photon/PlayFab 提供托管与大厅)。

13.1 局限与对策

局限 表现 对策
权威延迟 每个动作 RTT 后才被承认 客户端预测(移动);”乐观执行+回滚”模式
UDP 不可靠 丢包丢状态 束层可靠 + ACK 重传(引擎已做);关键数据 Reliable
序列化 CPU 大世界属性多时服务器 CPU 高 PushModel(只序列化脏的)、共享序列化、量化
带宽天花板 100 人 × 高频对象超限 RepGraph + Dormancy + 条件复制 + 频率分层
单服务器上限 一张地图一个服务器进程 分地图/分频道(Beacon+旅行)、世界分区

13.2 Iris:Epic 的重写

为什么重写:legacy 复制系统是”历史包袱”——属性比较与序列化耦合、通道与复制耦合、难以做全量化状态快照。IrisEngine/Source/Runtime/Net/Iris/,官方文档标 Experimental):

  • 全量化:属性先 Quantize 再 Serialize(量化管线 NetSerializers);
  • PushModel 原生:脏标记是设计核心而非可选优化;
  • ReplicationBridge:复制与通道解耦(DataStreamChannel 承载数据流,本 fork 的 CHTYPE_MAX=8 与 DataStream 通道(BaseEngine.ini:1842)就是为它预留的);
  • 本 fork 现状UNetDriver::ReplicationSystem 成员存在(NetDriver.h:2546 附近)、TickFlushInternalIrisUpdateTransactional 分支(:1141)、ProcessRemoteFunction 的 Iris 分支(:8112)——骨架已装、默认不启用-UseIrisReplication 或项目设置切换)。

避坑:本文所有机制章(第 3~8 章)讲的都是 legacy 路径——它是当前默认且文档/社区
主体;Iris 只在必须提到的分支出场。读 Iris 教程请认准”Experimental”标记。

13.3 替代路径

方案 适合 不适合 UE 支持
状态同步(本文) RPG/射击/开放世界 确定性要求极高的竞技 ✅ 原生
帧同步/回滚 格斗/即时策略 大世界/非确定性内容 ❌ 无原生(社区方案)
第三方平台(Photon/PlayFab) 快速原型/托管大厅 深度定制 通过 OnlineSubsystem 对接

13.4 演进时间线与阅读进阶

时间 里程碑
2004 UE2 网络层(当前架构的祖先:Driver/Connection/Channel 概念)
UE4 (2014) 属性复制成熟、RPC 体系、Replication Graph 引入(Fortnite 驱动)
5.0~5.3 RepGraph 插件化、PushModel、网络改造(DataChannel 合并、FunctionChannel 移除)
5.4~5.9 Iris 实验性集成(本 fork 已带骨架)、拥塞控制(BDP)、打包头无条件化
未来 Iris 转正(官方路线图)、全量化状态复制

进阶路线:Epic 官方文档(Networking and Multiplayer → Programming Network Multiplayer Games → Replication Graph → Iris)→ 知乎/CSDN 中文深挖(见附录 C)→ 本仓库源码(附录 A 指路)→ Squanch 博客 UE4 Networking 系列(未能访问核实,仅线索)。

附录 A 源码地图

全部路径基于本仓库快照(2026-08-26,HEAD 6cea9bd20f8f)。行号只用于定位;代码演进后以函数名为准。
阅读顺序建议:先 NetDriver.h(架构文档+总入口)→ NetDriver.cpp(每帧主循环)→ 按六架构分组深入。

A.1 网络架构(连接与通道)

文件 关键内容
Classes/Engine/NetDriver.h UNetDriver :809(架构注释 :34-324)、ChannelDefinitions :1038、ClientConnections :974、ServerConnection :970
Private/NetDriver.cpp LoadChannelDefinitions :801、TickFlush :1097(Iris 优先 :1141)、ServerReplicateActors :6205、ProcessRemoteFunction :8053、Net.RepDriver.Enable :7999
Classes/Engine/NetConnection.h UNetConnection :283、EConnectionState :94、EClientLoginState :116、Channels/OutReliable
Private/NetConnection.cpp InitBase :488、令牌桶 Tick :5110、FlushNet :2412、WritePacketHeader :2870、ReceivedPacket :3247、DispatchPacket :3609、net.* CVar 区 :60-248
Classes/Engine/Channel.h UChannel :63、EChannelType :25-36(CHTYPE_MAX=8)
Private/DataChannel.cpp UChannel/UControlChannel/UActorChannel 实现(本 fork 合并):ReceivedNextBunch :769、ReceivedBunch :3111、ReplicateActor :3599、ProcessBunchInternal :3270、StartBecomingDormant :4632、ReadyForDormancy :4604、BecomeDormant :4595
Public/Net/DataChannel.h 控制消息定义 NMT_* :173-248(DEFINE_CONTROL_CHANNEL_MESSAGE)
Public/Net/DataBunch.h FOutBunch/FInBunch :29-204(bReliable/bPartial 标志)
Public/Net/NetPacketNotify.h + Private/Net/NetPacketNotify.cpp 包级可靠:WriteHeader :203(14-bit Seq + AckedSeq + 历史位图)
Classes/Engine/GameNetworkManager.h Listen 服动态带宽 :54-71
Public/Net/TrafficControl.h FNetworkCongestionControl :78-98(BDP 拥塞控制)
Classes/Engine/NetworkObjectList.h FNetworkObjectInfo :34-109(NextUpdateTime/OptimalNetUpdateDelta)

A.2 通信架构(PacketHandler)

文件 关键内容
Runtime/PacketHandlers/PacketHandler/Public/PacketHandler.h FPacketHandler、HandlerComponent :739(Incoming/Outgoing/IncomingConnectionless/OutgoingConnectionless/RequiresHandshake)
PacketHandler/Classes/PacketHandlerProfileConfig.h UPacketHandlerProfileConfig(按 NetDriver 装配组件)
PacketHandler/Public/EncryptionComponent.h FEncryptionComponent(Enable/Disable/SetEncryptionData)
Engine/Public/PacketHandlers/StatelessConnectHandlerComponent.h/.cpp 无状态握手(cookie 挑战应答、EHandshakePacketType 六种包型、net.MagicHeader :235)
Runtime/PacketHandlers/ReliabilityHandlerComponent/ 已 UE_DEPRECATED(5.3)
Net/Core/Public/Net/Core/Misc/DDoSDetection.h DDoS 防护
Plugins/Compression/OodleNetwork/Source/Private/OodleNetworkHandlerComponent.cpp net.OodleServerEnableMode/ClientEnableMode :169-174

A.3 同步架构(属性复制)

文件 关键内容
Engine/Public/Net/RepLayout.h FRepLayout :1880(Parents/Cmds/BaseHandleToCmdIndex)、FRepChangelistState :433、FReplicationChangelistMgr :501、FRepState :671、树生成规则注释 :1091-1152
Engine/Private/RepLayout.cpp CompareProperties :1777、CompareParentProperties :1514、ReplicateProperties :1972、SendProperties_r :2767、ReceiveProperties :3789、CallRepNotifies :4661
Engine/Private/DataReplication.cpp FObjectReplicator:ReplicateProperties_r :1927、ReceivedBunch :984、ReceivedRPC :1323、QueueRemoteFunctionBunch :2293、PostReceivedBunch :1592、net.MaxRPCPerNetUpdate :39
Net/Core/Public/Net/Core/PropertyConditions/RepChangedPropertyTracker.h FRepChangedPropertyTracker(本 fork 位置,条件激活跟踪)
Classes/Engine/NetSerialization.h NetSerialize/NetDeltaSerialize 模板、FVector_NetQuantize、SafeNetSerializeTArray_*
Net/Core/Classes/Net/Serialization/FastArraySerializer.h FFastArraySerializer :408(数组 delta 同步)
CoreUObject/Public/UObject/CoreNet.h UPackageMap :190、FNetBitWriter/FNetBitReader :383-439

A.4 RPC 架构

文件 关键内容
Private/NetDriver.cpp ProcessRemoteFunction :8053(Iris 分支 :8112 / RepDriver 拦截 :8141 / Multicast :8148 / 单播 :8191)、ProcessRemoteFunctionForChannelPrivate :3152(SetForcedSerializeFromRPC :3179)
Private/DataReplication.cpp ReceivedRPC :1323、ReceivePropertiesForRPC :1415、ForwardRemoteFunction :1442、校验拒绝 :1351-1361
Private/PackageMapClient.cpp UPackageMapClient(对象↔NetGUID)

A.5 分发架构(决策链 + RepGraph + Dormancy)

文件 关键内容
Private/NetDriver.cpp ServerReplicateActors_PrepConnections :5126 / BuildConsiderList :5231 / PrioritizeActors :5456 / ProcessPrioritizedActorsRange :5615、IsActorRelevantToConnection :5386、ShouldActorGoDormant :5434、FlushActorDormancyInternal :4894
Private/ActorReplication.cpp IsNetRelevantFor :388、GetNetPriority :48、IsWithinNetRelevancyDistance :383
Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraph.h UReplicationGraphNode :69 / ActorList :188 / GridSpatialization2D :579 / GridCell :535 / DormancyNode :485 / AlwaysRelevant :771 / UReplicationGraph :920
Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp ServerReplicateActors :1112、ReplicateActorListsForConnections_Default :1449(bDormantOnConnection :1523 / StarvationFactor :1614)、网格 Gather :6238、ReplicationPeriodFrame :1083
Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraphTypes.h FGlobalActorReplicationInfo :983、FConnectionReplicationActorInfo :1395
Classes/Engine/ReplicationDriver.h UReplicationDriver :47-60(CreateReplicationDriver 工厂)、ReplicationDriverClassName 注释 :13-14
Classes/Engine/EngineTypes.h DORM_* 枚举 :3645

A.6 网关架构(登录/Beacon/旅行/预测)

文件 关键内容
Classes/GameFramework/GameModeBase.h AGameModeBase :47、PreLogin/PreLoginAsync/Login/PostLogin :286-338
Classes/GameFramework/GameSession.h ApproveLogin :78、KickPlayer :155、RegisterServer :183
Plugins/Online/OnlineSubsystemUtils/Public/OnlineBeacon.h AOnlineBeacon :67(AActor + FNetworkNotify)
Plugins/Online/OnlineFramework/Source/Lobby/ ALobbyBeaconHost :24 / ALobbyBeaconClient :58 / ALobbyBeaconState :171(大厅范例)
Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h IOnlineSession(Create/Find/Join/DestroySession,插件化)
Classes/Engine/World.h ServerTravel :4226、SeamlessTravel :4238、FSeamlessTravelHandler :194
Classes/GameFramework/PlayerController.h NetConnection :489、ClientRestart :1153、ServerAcknowledgePossession :1481、ClientUpdateLevelStreamingStatus :1450、”唯一 RPC Actor”注释 :841
Classes/GameFramework/Controller.h SetPawn/SetPawnFromRep/SetPawn_Direct :246-249
Classes/GameFramework/CharacterMovementComponent.h FNetworkPredictionData_Client_Character :3160、FSavedMove_Character :2988、NetworkMinTimeBetweenClientAdjustments :855-868
Private/Components/CharacterMovementComponent.cpp ReplicateMoveToServer :8925、ServerMove_Implementation :10106、SendClientAdjustment :2337、ClientAdjustPosition :11242、OnClientCorrectionReceived :11419

A.7 Iris 与 NetCore 支撑

文件 关键内容
Runtime/Net/Iris/ ReplicationSystem(Experimental,本文不展开)
Private/Net/Experimental/Iris/DataStreamChannel.h UDataStreamChannel(fork 新增,StaticChannelIndex=2)
Net/Core/Public/Net/Core/ PushModel(PushModel.h)、NetTrace(Trace/NetTrace.h)、量化序列化(Serialization/QuantizedVectorSerialization.h)、RPCDoSDetection
Core/Public/Misc/EngineNetworkCustomVersion.h FEngineNetworkCustomVersion(AcksIncludedInHeader=8、JitterInHeader=14、Latest=44)

附录 B 术语表(中英对照)

英文 中文(本文用词) 一句话解释 首次出现
NetDriver (不译) 网络总局:管理所有连接与共享数据 Ch2/3
NetConnection (不译) 一条端到端连接(邮路) Ch2/3
Channel 通道 连接内的通信槽(邮袋):Control/Voice/DataStream/Actor Ch2/3
Bunch 通道内传输的数据单元(信件) Ch2/3
Packet 数据报 UDP 传输单元(含包头+多束) Ch3
NetGUID (不译) 对象的网络门牌号(对象↔ID 映射) Ch2
PackageMap (不译) NetGUID 映射表(对象身份译码器) Ch3
Replication 复制 属性状态从服务器同步到客户端 Ch1
RPC (不译) 远程函数调用(电话) Ch1
Changelist (不译) 改动日记(只记”哪个属性变了”) Ch5
RepLayout (不译) 类型的复制蓝图(属性树) Ch5
RepState (不译) 每对象×每连接的复制状态 Ch5
CallRepNotifies (不译) 客户端收到属性后触发 OnRep 通知 Ch5
OnRep (不译) ReplicatedUsing 声明的客户端回调 Ch5
PushModel (不译) 主动脏标记(MARK_PROPERTY_DIRTY)替代逐属性比较 Ch5
FastArraySerializer (不译) 数组 delta 同步(只发增删改) Ch5
NetSerialize (不译) 自定义序列化(量化等) Ch5
FVector_NetQuantize (不译) 位置量化压缩 Ch5
Server/Client/Multicast RPC (不译) 三种 RPC 方向 Ch6
Reliable 可靠 bReliable 修饰:保证送达与顺序 Ch6
Relevancy 相关性 “这个连接该不该收到这个 Actor” Ch7
NetUpdateFrequency (不译) 每秒最大复制次数 Ch1
NetPriority (不译) 带宽紧张时的发送优先级 Ch1
NetSpeed (不译) 连接带宽上限(令牌桶) Ch3
QueuedBits 令牌桶 带宽配额记账 Ch3
RepGraph (不译) Replication Graph:可插拔分发框架 Ch7
GridSpatialization2D (不译) RepGraph 的空间网格节点 Ch7
Dormancy 休眠 停止复制的状态(DORM_* 五态) Ch7
FlushNetDormancy 唤醒 对象变化时解除休眠 Ch7
PacketHandler (不译) 每连接的组件流水线(安检) Ch4
HandlerComponent (不译) 流水线组件基类 Ch4
StatelessConnect 无状态握手 cookie 挑战应答(防放大攻击) Ch4
Handshake 握手 连接建立前的验证流程 Ch3/4
NMT_* (不译) 控制通道消息(Hello/Challenge/Login/…) Ch3
Beacon (不译) 游戏服务器与专用服务的带外连接 Ch8
Session 会话 找房间/进房间(插件化) Ch8
SeamlessTravel 无缝旅行 换图不断开连接 Ch8
ServerTravel 服务器旅行 服务器换图(客户端重连) Ch8
LevelStreaming 关卡流送 按需加载关卡(RPC 下发状态) Ch8
World Partition (不译) 大世界分区流送 Ch8
Prediction 预测 客户端先本地模拟(先斩后奏) Ch8
SavedMove (不译) 未确认的移动缓冲单元 Ch8
Correction 纠偏 服务器把客户端拉回正确位置 Ch8
ClientAdjustPosition (不译) 纠偏 RPC Ch8
Iris (不译) Epic 的新复制系统(Experimental) Ch13
NetTrace (不译) 复制明细追踪工具 Ch11
DDoS 分布式拒绝服务 放大攻击防护 Ch4

附录 C 参考资料

Epic 官方(dev.epicgames.com)

  1. Networking and Multiplayer in Unreal Engine —— 网络体系总览(Actor 复制/RPC/RepGraph/Iris 导航),有官方中文版;
  2. Programming Network Multiplayer Games for Unreal Engine(5.2)—— C++ 网络编程权威指南:C/S 权威模型、可靠与不可靠取舍;
  3. Actor Replication —— 复制两大手段(属性 vs RPC)的基础页;
  4. Replication Graph in Unreal Engine(5.7)—— RepGraph 节点体系与配置方法(注意:文档基于 5.7,本 fork 的 RepGraph 在插件目录);
  5. Introduction to Iris in Unreal Engine(5.8)—— Iris 新复制系统(仍标 Experimental)。

中文社区

  1. 知乎《使用虚幻引擎4年,我想再谈谈他的网络架构【经验总结】》(zhuanlan.zhihu.com/p/105040792)—— 717 赞高赞长文:状态同步本质、”同步数据也许会迟到但永远不会缺席”;
  2. 知乎《UE4网络同步思考(一)—经典同步方案》(p/56548096)—— C/S vs P2P、UDP 融合 TCP 优点、带宽四板斧(相关性/优先级/成员标记/合包);
  3. 知乎《UE5-Iris 网络复制系统技术分析(架构层次)》(p/1996687209709991513)—— Iris 三层架构与 -UseIrisReplication 切换;
  4. 知乎《UE5中的网络同步能力和同构服务器框架(上)》(p/621339344)—— 同构服务器框架概念;
  5. CSDN《UE4网络架构学习笔记》(blog.csdn.net/qqQQqsadfj/article/details/123256433)—— 四模式与属性复制三步曲;
  6. CSDN《解析虚幻联网机制和游戏中如何实现网络同步》(blog.csdn.net/X_Bai01/article/details/146400016)—— WithValidation 防作弊、NetDebug 调试。

博客

  1. Squanch(squanch.blog)UE4 Networking 系列(Phil Kauffold)—— 写作时未能访问核实,仅线索;内容基于 UE4/5.0,类名有差异。

注意:以上社区资料基于 UE4/UE5.0~5.2,与当前源码的差异(如 FunctionChannel 移除、
RepGraph 插件化、NetServerMaxTickRate 命名)请以本文正文的避坑块为准。

附录 D 自测练习

入门级

  1. 用三句话 + 一个类比向同事解释 UE 网络架构(要求用上”服务器权威”和”UDP”)。
  2. 属性复制和 RPC 的区别是什么?”玩家掉血”和”玩家开火”分别该用哪个?
  3. 四种运行模式(Standalone/Dedicated/Listen/Client)中,哪些是”服务器”?

进阶级

  1. 画出客户端连接服务器的完整握手时序(六关 NMT_*),标注每一关是谁发给谁、干什么。
  2. 解释”changelist 只记 handle 不记值”为什么能省带宽,并说明首帧为什么必须全量。
  3. 一个 50 人服务器 × 2 万活跃 Actor 的场景,Legacy 决策链每帧做多少次相关性检查?RepGraph 空间网格(每格子 100 个 Actor)后大约多少次?
  4. RPC 的 Multicast 在服务器本地调用时会发生什么?为什么这样设计?
  5. 客户端预测的”先斩后奏”流程是什么?SavedMoves 在纠偏时被怎样处理?

源码级

  1. NetDriver.cpp:8053(ProcessRemoteFunction)找到五个分支,说出每个分支的进入条件。
  2. RepLayout.h:1880(FRepLayout)找出 Parents/Cmds/BaseHandleToCmdIndex 三个成员,解释它们的关系。
  3. NetPacketNotify.cpp:203(WriteHeader)对照图 F5,推导一个纯 ACK 包(无数据)的最小头部大小。
  4. NetDriver.cpp:5434(ShouldActorGoDormant)找到休眠的三个条件,解释”无新属性”为什么是必要条件。
  5. 实验:开一个 ListenServer + 两个客户端,用 stat net 观察带宽,把某个 Actor 的 NetUpdateFrequency 从 100 调到 10,观察 stat net 与行为变化。