1. 多人 FPS 网络同步到底在同步什么1.1 先搞清楚 FPS 网络同步的三大痛点做多人 FPS 最难的不是开枪手感不是地图设计而是网络同步。单人模式里随便写个 AI 行为树玩家在本地开枪、本地命中、本地结算怎么调都爽。一旦切到多人所有逻辑都要重新审视玩家 A 看到的敌人在 B 看到的位置吗玩家 B 屏幕里那个人到底是多少血量两个人同时开枪谁的命中生效FPS 网络同步要解决的核心问题归结起来就三个。第一是权威性。玩家打出的每一发子弹命中结果由谁判定如果由开枪的那个客户端自己判定外挂改一下内存就能无限爆头游戏规则直接崩塌如果由服务器判定又会引出延迟带来的挫败感——你明明看到准星压在敌人身上服务器却回传“没打中”。权威性决定了公平性的天花板也是网络同步一切讨论的起点。第二是一致性。多个客户端看到的游戏世界必须大体相同。同一个敌人的位置、血量、当前武器、换弹状态在不同玩家的屏幕上不能差距过大否则对枪时双方看到的场景完全不一样胜负就成了“谁的屏幕更诚实”的抽奖。一致性听起来是基本要求真正做起来却非常烧钱因为每个属性同步都有带宽成本同步得越频繁、越精确网络压力就越大。第三是流畅性。FPS 玩家对延迟极其敏感超过 80ms 的输入延迟就能明显感觉“飘”。鼠标点了、子弹没出这种体验在竞技游戏里约等于劝退。网络同步不能为了绝对准确而牺牲响应速度必须让本地操作立刻有反馈哪怕是“假反馈”也得先让玩家爽到再让服务器慢慢纠正。这三个问题互相牵扯没有任何一个能单独解决。权威性要求服务器承担更多判定逻辑这会增加等待时间一致性要求同步更多状态数据这会抬高带宽流畅性要求客户端做预判渲染这又可能和权威结果冲突。做多人 FPS本质上就是在这些矛盾里反复横跳找到一个玩家最能接受的平衡点。1.2 状态同步与事件同步两种模型的取舍UE5 默认给的是状态同步思路服务器作为权威定期把 Actor 的属性变化广播给客户端。角色的位置、旋转、血量、动画状态位都是状态的副本。每个客户端本地维护一份副本服务器更新后推过来客户端再渲染。这种模型的好处是容易理解只要服务器属性一变复制系统自动找到需要更新的连接把数据送出去。缺点是带宽消耗大很多属性其实没变化但为了保险也被同步了一遍。另一条路线是事件同步有时也叫消息同步像格斗游戏里的回滚式网络同步就偏向这个方向。客户端或服务器通过发送离散的“事件”来推进逻辑而不是持续同步每个属性。格斗游戏里一个“出拳”事件就代表一个完整的动作逻辑双方只需要把事件广播出去各自本地演算。FPS 也能用事件同步处理部分内容比如“扣动扳机”本身是一个事件“换弹完成”也是一个事件但子弹飞行轨迹、命中位置这些依赖连续位置状态纯事件同步覆盖不了完整的 FPS 逻辑。在 UE5 里做 FPS我不会一开始就纠结“我到底选哪种模型”而是先看游戏规模。如果只是 4 人小队合作 PvE状态同步加适量事件同步就够用如果要做 20 人以上的 PvP就要提前规划属性复制的粒度把高频变化状态和低频变化状态拆开处理。角色的位置是高频状态几乎每帧都在变血量是低频状态只有受伤时才变化。针对不同属性设置不同的同步频率和网络优先级这是后期优化带宽的关键手段也是从“能跑”走向“能上线”的分水岭。1.3 从玩家体验倒推设计目标我接手多人 FPS 项目时习惯先把玩家体验拆成几个“感觉点”再反推技术方案。因为网络同步的目标不是“数据同步”而是“玩家觉得公平、跟手、不卡”。开镜手感要求玩家按下右键屏幕立刻开镜不能等服务器确认再切视角所以开镜动画和 FOV 变化必须走本地预测。射击手感要求按下左键枪口火光、枪声、后坐力立刻反馈所以射击表现要本地先行命中结果再走服务器裁决。击杀反馈可以延迟几十毫秒飘血伤害数字晚一点跳出来问题不大但不能完全丢失。敌人移动必须平滑画面上的敌人不能一卡一顿地瞬移所以要用插值来填补网络包之间的空档。伤害判定必须服务器说了算但要结合开枪者的网络延迟做补偿不然延迟 80ms 以上的玩家永远打不中移动目标。把这些体验目标列出来再回头去读 UE5 的复制文档思路会清晰得多。网络同步不是为了同步本身是为了让玩家觉得“这游戏真跟手、真公平”所有技术选型都应该围绕这个目标展开。2. 复制系统核心原理别只盯着同步频率2.1 Actor 复制、属性复制、RPC 的分工UE5 的网络复制体系分三层Actor 复制、属性复制和 RPC。Actor 复制是基础管道。一个 Actor 设置了bReplicates true它才会被纳入复制系统。服务器负责创建和销毁 Actor客户端只是接收实例化的指令。没有 Actor 复制后面的一切都不存在。属性复制是 State 的传递。Actor 上标记了Replicated的属性会在变化时被服务器推送到相关客户端。比如血量CurrentHealth服务器把它改成 80所有相关客户端收到新值后更新本地状态。这里要注意区分Replicated和ReplicatedUsing前者是“服务器改了我就收”后者是“收的同时在客户端自动触发一个回调”。FPS 里血条 UI 刷新、受击飘血这类表现逻辑强烈建议用ReplicatedUsing配合OnRep_函数否则客户端只拿到数值还要自己轮询监听额外增加复杂度。RPC 是 Event 的传递。Server 函数用于客户端向服务器发送请求比如“我按下了开火键”Multicast 函数用于服务器向所有客户端广播事件比如“手雷爆炸了”Client 函数用于服务器向指定客户端单独下发信息比如“你被击杀了切换到观察者视角”。RPC 适合用来传递瞬时事件而不是持续状态。我见过不少新人把位置更新写成Server_SendMove这种每帧调用的 RPC结果带宽直接爆炸。位置这类高频状态应该走属性复制加插值RPC 只负责扣扳机、换弹、投掷这些低频离散事件。这里还有一个容易踩的坑RPC 的执行条件是“握着有效连接”。在 Listen Server监听服务器模式下Host 玩家调用 Server RPC本质上是本地直接执行不走网络。在 Dedicated Server 模式下所有玩家都是纯客户端Server RPC 必须真的经过网络层。上线前一定要在 Dedicated Server 环境测试一遍很多人只在 PIE 模式里玩结果发布到 Linux 服务器后各种 RPC 失灵排查半天发现根本没有客户端调用到服务器。2.2 Replicated、RepNotify 和连接相关性属性复制标记看着简单实际用起来有讲究。Replicated表示“我要同步这个属性”但同步时机、同步优先级、是否只在部分连接上同步都另有一套规则。先说ReplicatedUsing。给属性加上它并写一个OnRep_函数名的回调客户端收到新值时会自动触发回调。这比客户端自己轮询属性要优雅得多而且天然适合驱动 UI。比如UPROPERTY(ReplicatedUsing OnRep_Health) float Health; UFUNCTION() void OnRep_Health();服务器修改 Health 后客户端自动执行OnRep_Health在函数里更新血条、播放受击特效、判断是否死亡。这种做法把“数据变化”和“表现反馈”解耦结构清晰也方便维护。然后是连接相关性。UE5 的复制系统不会把每个 Actor 都同步给所有客户端而是根据相关性判断“这个客户端需不需要知道这个 Actor”。默认基于距离剔除离玩家太远的 Actor 不复制也可以用IsNetRelevantFor自定义规则。FPS 里常见需求是“队友位置必须同步但敌人的弹药数量不需要同步”这时就要对属性做分类或者自定义相关性规则。盲目关闭距离剔除会导致带宽失控20 个玩家互相看到所有子弹和所有道具网络包瞬间塞满。我实测过一个简单场景8 人团队死斗地图里有 200 个可拾取弹药盒。默认距离剔除下每人周围只有 5 到 8 个弹药盒需要同步带宽毫无压力。一旦把剔除关了所有 200 个弹药盒全部同步给每个人属性更新量直接翻了 20 多倍。这个案例说明复制的第一原则不是“同步什么”而是“不同步什么”。2.3 网络优先级、更新频率和带宽控