UE5 网络同步及Coop实现
UE5 的网络同步一直是很多开发者的坎,尤其是一想到「Coop」这个需求,四个人联机打僵尸、一起开机关门,听起来很爽,写起来却是各种头疼。有人会觉得“同步嘛,勾个复制不就行了”,结果做出来要么是AI瞬移、要么是门在一个人眼里开了在另一个人眼里没开、要么客户端干掉了服务器还一脸无辜。这篇东西,我想把我在UE5里实际做Coop的经验拉通讲一遍。不是复制文档,而是讲你怎么搭一个真正能玩、能稳定同步的多人合作框架。
适合的人群,我默认你已经能画简单的蓝图逻辑、知道类是什么概念,但对网络同步还停留在“听别人说很难”的阶段。我会从架构选型、复制变量、RPC、AI同步这些角度挨个唠,最后把坑列一批出来。这些东西不一定让你立刻变成网络同步大师,但肯定能让你少加班刷夜。
1. 整体设计:Coop项目开搞前,先想清楚这几个关键选择
1.1 客户端-服务器权威,别搞P2P幻想
很多人一听说Coop,第一反应是“大家各连各的不就行了吗”。不行。UE5的网络模型基本就是客户端-服务器模型,服务器是仲裁者,客户端都向服务器汇报、从服务器收状态。这是UE5帮你设好的框架,你最好不要试图去推翻它。
Coop最典型的场景是:玩家ABC一起玩,A把门推开了,B和C必须看到门开了,同时如果门推开时把敌人放进来了,敌人也要对所有人出现。如果每个人本地各自判断,那就会出现A看到门开了但撞到门框,B看到门还关着却已经被敌人打了。所以这件事必须在服务器上算好,再广播下去。
有人说“我有专用服务器”,有人说“我打算让房主做监听服务器”。Coop主流做法其实就是监听服务器,因为房主本身也是玩家,这样不额外消耗服务器成本。监听服务器有一个要命的点:房主自带零延迟,其他人会有几十毫秒延迟,你会觉得房主身法特别好,开枪也快,这没办法,你要在体验设计上做些取舍。
确定“谁是权威”之后,所有gameplay核心状态都要放在服务器。举个例子:血不是客户端上的血条数字,血是服务器上的变量,客户端只是把血条显示出来。位置也是,玩家控制的Pawn的移动最好让服务器校验,因为不管高延迟还是外挂,位置中心在服务器才能保证公平和一致性。
1.2 同步范围与策略:什么必须复制、什么可以不复制
网络同步不是“能复制就复制”。每个同步的变量、每个执行到客户端的RPC,都会有带宽和性能成本。Coop项目里玩家数量少,上限4-8人,但是AI和场景交互可能比混战模式还要多,所以你要分清:
- 必须复制的状态:玩家位置、朝向、血量、武器状态、门的开关、拾取物、怪物血量、任务进度
- 必须本地算的:纯表现效果、粒子、音效触发、动画步伐细节(很多用动画代理ambient)
- 只在服务器算的:伤害判定、寻路AI决策、物品掉落后归属
我在做Coop时经常用一句话自检:这个逻辑如果放在客户端执行,会不会造成不公平或者不同步?如果会,就往服务器放。
有时候“效果”也要走RPC而不是复制变量。比如开门这个事件,门的状态变量放在服务器上没问题,但门打开的声音、动画摇动、灰尘粒子特效应不应该由所有客户端从变量变化里推算出来。正确做法是:变量负责状态,服务器再广播一个Multicast RPC去触发本地效果,两边结合是最好的。
1.3 Coop项目的常用技术架构选型
UE5默认的网络同步架构其实做Coop是够用的,不一定要碰什么第三方插件。核心四件套可以归纳成:
- Actor Replication:凡是需要同步的Actor,起始要把“Replicates”勾上
- Property Replication:把需要同步的变量设置为RepNotify或条件表达式,客户端在变量变化时收到通知
- RPC:三路RPC(Server、Client、Multicast),用来执行瞬时事件和命令
- PlayerController / PlayerState / GameState / GameMode:这套框架在多人里分工明确,谁管理谁都要心里有数
Combat类Coop和科幻射击项目,很多团队为了手感会把“瞄准时的相机抖动、命中反馈”做在本地,因为那只影响自己;但“我打到敌人的一击”必须上报服务器,再由服务器决定敌人掉多少血。我在很多项目里都踩过同一个坑:刚开始为了图省事,直接复制敌人的“当前血量”变量,想象中客户端能显示血量变化,后来发现伤害计算放本地很容易导致血量不一致,服务器那边敌人已经死了,客户端还在显示残血。
你可以把这套关系想成公司:服务器是老板,所有重要决定必须老板批;客户端是业务员,能自己定喝什么咖啡,但签合同必须上报。这个比喻虽糙,但能帮你理顺逻辑。
2. 核心细节:UE5网络同步的关键概念拆解
2.1 Actor复制与变量复制:分清楚谁在管“状态”
刚才说了勾选Replicates,但Actor复制不等于所有内容都会被同步。开启Replicates后,还需要哪些变量要打上复制标记。最常用的有:
- 直接勾Replicated
- 用ReplicatedUsing条件(RepNotify),变量变了会自动调用一个事件,适合在客户端做UI更新和效果触发
举个例子,敌人血量变量:
- 服务器上敌人的Blood变量标记为RepNotify
- 服务器扣减Blood后,RepNotify在所有客户端都会触发,客户端在RepNotify里刷新血条
- 但扣血的计算逻辑,只允许在服务器执行,客户端本地打怪物时先发RPC到服务器
这里有个关键点你可能不知道:RepNotify在服务器本地也触发。意思就是说如果有一个节点直接连到Anim或UI,服务器端也会跑一遍。有些人不加判断会导致UI重复刷新,或者服务器上也播放了受伤动画,表现其实没问题,但一旦涉及AI逻辑就要小心,因为你可能不知不觉在服务器和客户端各跑一遍。
如果某个变量只想从服务器往下同步,不想让客户端改,用“复制到所有”即可。但如果你想让一个人拥有这个Actor(比如玩家身上的角色),可以使用Owner Only复制,也就是只有拥有者客户端会收到变量更新。其他客户端看这个玩家时,如果很多属性是Owner Only,那他们看不到准确状态。常见的处理方式:
- Pawn的持有者只有PlayerController,所以玩家摄像机的FOV、瞄准偏移等属性用Owner Only没毛病
- 但血条是全队可见,就得复制到所有
- 武器开火时你本地做枪口火焰表现,队友看不到也很糟糕,所以开火事件适合用Multicast,而不是仅Owner
使用复制条件时,最好写一个自动判断逻辑,比如“这个属性只有当Actor复制的条件是某玩家才复制”。UE5在Details面板里会有一个“Replication Condition”,常选项是None、Owner Only、Simulated Only、Autonomous。这些概念容易晕,你要记住:“Simulated”指的是其他人视角上的模拟角色,不是你自己;“Autonomous”严格来说指客户端拥有所有权时PlayerController控制的Pawn;服务器上的Pawn没有这个分类。
2.2 三种RPC:Server、Client、Multicast,用错就事故
RPC是网络同步的“短信”。的延时策略决定了谁执行,谁不执行。新手最常犯的错误就是把服务端逻辑放在ServerRPC函数体里,然后调用者也是Server,结果本地跑一遍、服务器再跑一遍。
我建议把RPC的权限规则刻在脑子里:
- Server:客户端调用、服务器执行。适合客户端“请求逻辑”——比如我按F开门,按F这个操作发生在客户端,之后发Server RPC让服务器开门
- Client:服务器调用、指定某客户端执行。适合服务器“通知某个玩家”——比如你掉线了、你被踢了、你解锁了什么成就
- Multicast:服务器调用、所有客户端执行。适合广播事件——比如播一个全场景动画、全队看到怪物爆炸
这三种RPC有一个限制,大多数人不注意:RPC只能从“拥有这个Actor的客户端”调用Server,或者从“服务器”调用Client/Multicast。换句话说,如果某个AI怪物不是某个玩家拥有的,那么客户端小组件上是不能直接调用这个AI怪物的Server RPC的。你想 打怪,就要做一个玩家可控制的Actor(比如Pawn)来中转RPC,或者把逻辑放在GameMode/PlayerController上。
具体到Coop里的开枪逻辑通常是:
- 玩家在本地PawnA客户端按下开火键,生成射击视觉效果
- PawnA作为拥有者调用Server RPC到服务器
- 服务器这边校验武器有没有子弹、是否在冷却,然后对目标进行射线检测
- 服务器扣除子弹和敌人血量,通过RepNotify让所有客户端同步血条
- 服务器播放开火Multicast,让其他玩家听到枪声/看到枪口火花
这里建议你不要在开火本地就立刻扣减自己的子弹数,因为RPC有延迟,会造成你明明开了五枪,服务器只收到四枪。这种“客户端的预体验”可以用,但要记住服务器最后强制校正。如果你做的是休闲向Coop,可以容忍偶尔的漂移;如果是硬核射击,就要做Reconciliation。
2.3 PlayerState、GameMode与GS的分工
除了变量和RPC,Coop项目里最常见的混乱是:不知道哪个类的职责是什么。
| 类 | 作用 | Coop场景典型用途 |
|---|---|---|
| GameMode | 服务器专用逻辑,不复制 | 任务条件判断、胜负判定、刷怪规则 |
| GameState | 复制到全房间 | 游戏总时间、总任务进度、幸存人数 |
| PlayerState | 复制到全房间 | 每个玩家的名字、得分、击杀数、携带道具 |
| PlayerController | 持有者客户端可见、可复制 | 输入、相机控制、客户端UI |
| Pawn | 复制 | 玩家的物理角色、血量、武器状态 |
在Coop里,经常有“主机大厅”和“游戏内房间”的概念。GameState要尽量保存公共状态,这样所有客户端可以看到“第一波清理完成”“第二波开启”。PlayerState存每个玩家的状态,每当有人加入,他也能通过PlayerState看到已有玩家的进度和分数。
有一个很多项目翻车的点:GameMode不会复制到客户端。你在GameMode里写了个开门逻辑,客户端判断不了,因为它没有GameMode的本地副本。所以凡是客户端需要读取的数据,要么放GameState,要么放PlayerState,要么专门做一个复制Actor。如果你发现自己写蓝图时连到了GetGameMode,然后你在客户端上测试它返回空,那并不是Bug,而是它的设计本就不往客户端复制。
2.4 Pawn的复制配置与连接处理
在Coop里,玩家加入后最核心的问题是:“我能不能控制我的小人和别人能不能看到我的小人。”这里有两个决定因素:
- Possess:PlayerController接管一个Pawn
- SetActorReplication和Pawn的Replicates属性
通常做法:在GameMode的Login和PostLogin里,服务器Spawn一个事先设计好的PlayerPawn,然后让PlayerController Possess它。这个Pawn的Replicates勾选上。这样服务器拥有该Pawn并复制给所有其他客户端。其他客户端上的该Pawn会变成“Simulated Proxy”,它不能输入,只做模拟运动和接收状态。而客户端自己那一份是“Autonomous Proxy”,它通过PlayerController和自己的Pawn相连。
很多人在这里不知道要开启“Enable Input”的位置。如果输入在Possess之后才绑定,你还需要检查Client侧的Controller是否已经正确赋值。一个常见的坑是,在服务器Spawn和Possess完成后,客户端上的首帧数据还没同步过去,导致你控制不了角色或相机乱飞。很多时候解决方法很简单:把Possess延后到BeginPlay后的一小帧,或者使用PlayerController的ClientRestart来强制刷新。
睡眠中的角色也会有问题。如果Pawn是异步加载出生,或者你把它Spawn在远离游戏区域的位置,服务器可能会暂不复制。这时你看到的客户端上是空的。调试窗口打开NetDriver的状态,检查Spawn权限和网络连接模式非常有用。
3. 实操:从零搭一个“开门+刷怪”Coop关卡
3.1 项目与地图初始设置
选一个Blank模板,建议使用第三人称或第一人称模板,目的是有Pawn。
网络联机前的项目设置:
- 在Project Settings -> Maps & Modes,把Default GameMode设置为自定义的BP_CoopGameMode
- 在BP_CoopGameMode的Default Pawn Class设置成你的BP_PlayerPawn
- 地图中放好出生点PlayerStart,并开启
Allow Multiplayer相关设定(测试时在PIE设置Number of Players为2-4)
测试联机有几个方法:
- 直接PIE设置Net Mode为Play As Listen Server、Number of Players选4
- 另开编辑实例或打包客户端用命令行连接
我通常第一轮先用PIE监听服务器模式,跑3-4个人物窗口同时测试,提高迭代速度。注意:关闭“Use Single Process”的话,每个窗口各是一个进程,状态独立,用来测网络更好。不过PIE的多进程启动会占用大内存,如果机器只有16G,建议先单进程多Player测试逻辑,再打包两三个客户端在外面连。
3.2 创建可复制的Pawn:以玩家角色为例
打开你的BP_PlayerPawn细节面板:
- Replicates勾选
- Replication Movement组件(CharacterMovementComponent)会自动处理位移同步
- 你自定义的变量比如HP、Ammo全部设为Replicated
- 不需要复制的组件属性(比如弹簧臂的相机角度)保持默认
如果你需要角色具有“Crouch”、”Jump“这些能力,CharacterMovement已经自带同步,不需要你自己写。但如果你在Tick里直接设置Pawn的Location,会导致服务器和客户端移动发生冲突,所以尽量不要手动SetActorLocation来移动,除非是瞬移或特殊技能。
蓝图示例:定义一个变量CurrentHealth类型Float,设置为ReplicatedUsing。
[CurrentHealth] (RepNotify) OnRep_CurrentHealth: - 刷新HUD血条 - 播放受伤音效 - 如果CurrentHealth <= 0,播放死亡动画并调用Server_DeathProcess注意:“调用Server_DeathProcess”这里,应当由服务器判断,不能在客户端直接执行。所以你可以用客户端先播放表现,再由服务器处理逻辑。
3.3 实现一个可开门/开关门的Coop机关
还是前面说的“门”的例子。我们来做一个双人合作才能开的机关门:两个玩家各自站在对应的触发器上,门才能打开。
设计如下:
- 门Actor叫BP_CoopDoor,Replicates勾选
- 门有一个
bIsOpen变量,RepNotify - 两个压力板Trigger分别与门绑定,每个Trigger记录“被哪个玩家踩住”
- 只有当两个Trigger都有玩家时,服务器才打开门
你可能会问:压力板为什么也要同步?因为如果一个客户端上的玩家踩到了板子,另一个客户端看不到,那就很怪。所以Trigger的IsOverlappingActor状态要在服务器上判断。每个Trigger可以用ServerRPC去上报“我踩上来了”。
具体蓝图流程:
- 玩家Pawn的碰撞盒Overlap到Trigger,Pawn的拥有者(也就是本客户端)调用
Server_FootOnPlate到服务器 - 服务器记录该Trigger已被触发,尝试检测另一Trigger是否也有人
- 如果齐全,服务器设置
bIsOpen = true打开门,同时播放一下开门动画 - 所有客户端通过
bIsOpen的RepNotify更新门的开闭状态;在RepNotify里播放门的动画、音效
这里有一个常见问题:如果门打开是一次性动画,你可以用Boolean开关,但如果涉及门在动画过程中需要插值运动,更优雅的方法是直接同步门的TargetYaw或者OpenProgress浮点值。幅度同步的好处是,即使客户端加入时看了第一帧,他也能把门设置到正确位置。
3.4 刷怪逻辑与AI同步
Coop很大程度是刷怪打AI。刷怪逻辑放服务器上。比如按任务进度刷出5只NPC僵尸。
在GameMode里写一个SpawnWave(int32 EnemyCount),服务器Spawn出AI,AI的Pawn类同样勾选Replicates。UE5的AI移动同步大多依赖AIController和NavigationSystem。如果你给AIController开启bRunAIWithNoController或者混用行为树,要小心AI客户端上的表现。
实际经验告诉我:AI Pawn复制到客户端时,它的Movement可能完全不可靠,因为AI控制器在服务器上有完整的寻路数据,客户端只是预测。如果你发现AI在客户端上一跳一跳,很可能是AI Pawn的复制条件错了,比如你把它设成了OwnerOnly,但AI没有Owner,于是客户端永远接收不到位置。正确做法是让AI Pawn复制给所有客户端,并且Replication Conditions用None。
另外,AI的“感知”系统不要在客户端上运行。比如客户端在本地用AI感知判断“敌人看到了我”,然后播放警报,这就会造成每个人看到不同AI行为。应该把AI感知放服务器,如果需要客户端表现,比如看到敌人进入警戒状态,就用Multicast发个RPC或通过RepNotify让客户端播放警惕动画。
3.5 双人合作的任务进度同步
Coop里任务进度可以精分成几个阶段,比如:
- 阶段1:清理区域内的敌人
- 阶段2:两个玩家同时按开关开门
- 阶段3:夺取保险箱道具并带到撤离点
- 阶段4:全体存活进入撤离直升机
建议用一个GameState变量CurrentObjective来同步当前任务编号。GameState天生具备全房间复制能力,且GameState不会因为玩家中途加入就被重置,所以非常适合存放阶段进度。
GameState: CurrentObjective (ReplicatedUsing) OnRep_CurrentObjective: 客户端UI任务面板刷新 如果阶段切换,播放UI提示和音乐服务器在判定“敌人清完了”“门开了”“道具到了撤离点”后,自增CurrentObjective。凡是要显示给所有人的信息都从这个变量走,不要每个客户端各自算进度。
4. 常见问题与排查技巧实录
4.1 客户端表现不一致:状态的权威性从根上没拧对
现象
在自己窗口测试一切正常,四个人加入后,明明门开了,某个客户端依然显示门是关的。
排查步骤
- 先确认门Actor的
Replicates是否勾选。新手有一半的问题是这里 - 确认
bIsOpen变量复制。直接在打开门的时机打印日志,看看RepNotify是否在客户端触发 - 检查是否把打开门的逻辑放在了某客户端里只执行了一次。如果是在本地执行的
SetActorRotation,其他客户端可能不会同步 - 检查属性replication条件。如果你误开了
OwnerOnly,而门没有Owner,那全客户端都不更新
经验
“门只开一次”这种机制,用纯Boolean确实够,但如果你将来要做插值门(缓慢开启),我建议把一个Float类型的“OpenProgress”复制,每帧让服务器向目标值插值。这样不但所有客户端都能看到一致的开门角度,中途加入的玩家也能看到正确的门状态。我之前项目也是图省事直接同步一个“开门动画时间”,后来发现有些客户端动画已经结束但门State还没同步,观感很怪。
4.2 玩家操作不上报:RPC没被调用或没权限
现象
按下开门键没有反应,服务器日志什么都没有。
排查步骤
- 检查调用的RPC是否标记为
Server,函数放在Pawn里而不是Controller里,可能导致权限不正确 - 当你在客户端调用
Server_OpenDoor时,必须通过拥有这个Pawn的那个客户端调用。如果一个Pawn没有Owner,那么它上面的ServerRPC会静默失败 - UI里按下的按钮事件绑在Widget上,而Widget上没有直接获得Pawn的引用,你需要先
GetPlayerPawn,或者通过PlayerController拿到Pawn再调用 - 联网权限:只有拥有者能调用Server,如果你想让别人也能触发(比如开门机关所有人能按),应该做成一个Actor如“门开关”,所有人都可以拥有?其实不行,开关没有拥有者。正确做法是把这个开关做成一个所有客户端都不会拥有,但是通过Pawn来调用ServerRPC的交互模式
经验
我给所有玩家交互物件设计一套通用的“InteractableI接口”。玩家Pawn上有Server_Interact,参数是场景中某个Actor的引用。服务器收到后判断双方距离是否在范围内,如果符合就执行互动。用这个方法处理按钮、门、拾取、终端机,能省好多套重复逻辑。
4.3 AI在部分客户端瞬移或延迟
现象
在同一台机器上测,AI很流畅,但远程客户端看到的AI会卡顿和瞬移。
根源
AI Pawn的移动通常不受CharacterMovementComponent直接驱动,而是由AIController每帧MoveTo目标点,然后服务器通过移动复制的数据发给客户端。如果服务器帧率出现波动,或复制频率设置的过低,客户端就会看到一小段延迟后的大跳变。
优化
- 确认AI Pawn勾选了
Replicates - 确认移动模式使用
CharacterMovementComponent,不要直接用SetActorLocation - 调高
Net Update Frequency:默认值是100,如果AI个数不多可以调到50-60,反而降低带宽 - 用
NetDormancy或预判插值,让客户端侧的AI看起来平滑
经验
另一类卡顿是动画问题。AI移动用动画蓝图的话,你其实不需要把动画State完整复制,只要同步位置速度,动画蓝图本地也能推理出走路跑步。不要在网上监听“浓缩后的AI状态变量”,最容易出问题。
4.4 开局有一个玩家看不到其他玩家的角色
现象
主机和玩家A都正常,玩家B进入游戏却看不到玩家A的Pawn,或者看到的全是T-Pose。
排查步骤
- 玩家A和B的Pawn是否都是服务器Spawn的,玩家B的客户端加入时间是否是在Pawn复制之前
- 你就需要重新执行一次“LateJoin”逻辑:GameMode在PostLogin时,服务器应该向新加入玩家同步所有已有的其他Pawn。通常引擎会自动处理,但如果你的Pawn是手动Spawn且没有放在正确级别中,有可能没进入复制列表
- 检查packages是否都是可复制的,比如蓝图中某些Actor的static mesh加载较慢,加入者可能等不到Mesh创建
- 如果你用了动态生成的Mesh或Material实例,尽量不要用本地异步生成逻辑接近复制节点
经验
T-Pose很多是因为骨骼网格体或动画资源没有完全加载,客户端已经开始显示这个角色。比较稳的做法是让Pawn的视觉Mesh默认加载,用Attachment方式挂在某个Socket上时,SetActorHiddenInGame先隐藏,等资源加载完毕在客户端上再显示。或者指定bReplicateMovement和动画Asset都在CDO里,不要运行时去远程加载。
4.5 网络抖动带来的掉落与拾取不同步
Coop里捡东西是玩坏最多的系统。你看到地上有个药包,走过去E拾取,药包已经消失,但队友看到你身上还是没药。
常规思路:
- 药包Actor的
bIsAvailable变量复制到所有,拾取后服务器设false - 客户端在本地播放拾取动画
- 服务器发Client RPC给成功拾取者,告诉他背包加了物品
这样最理想。但很多团队想实现“谁快谁得”的竞争拾取,必须用服务器判先后,不能本地直接拾取再上报。我做过一个“拾取加血”项目,为了让手感好,我让本地拾取后立刻加血条显示,但服务器鉴定后才真正计算有没有拾取到。如果服务器判定无效,客户端再回滚。这样虽然有一瞬间假加血,但整体网络体感舒服很多。
4.6 调试网络同步的工具与习惯
UE5里有很多调试命令你值得用:
- 控制台:
NetShowDebug,显示Actor同步状态、RPC、网络负载 - 控制台:
NetTrace,追踪复制调用链路 - 编辑器:Outliner里打开“Replicated”图标显示
- 打印RPC调用日志:
LogNet很高频,建议开启LogNetPlayerController或LogNetFastTArray
我自己的习惯是在关键变量的RepNotify里加一个前缀日志,标明当前是服务器还是客户端(HasAuthority),这样一眼能看出到底在哪个环节没同步。如果是双进程测试,记得把日志输出到文件,再对比两端调用顺序,大多数问题十分钟内能定位。
5. 常用网络同步模式速查表
下面这张表总结了我做Coop时最常见的同步选型。
| 场景 | 方案 | 原因 |
|---|---|---|
| 玩家位置/朝向 | CharacterMovementComponent自带复制 | 简单、可靠、延迟补偿成熟 |
| 玩家血量/道具 | PlayerState或Pawn变量RepNotify | 全房间可见且可以断线重连恢复 |
| 当前关卡进度 | GameState变量RepNotify | 所有客户端读同一份来源 |
| 单人交互(按按钮) | Server RPC(请求)+ Multicast(广播) | 避免状态只存在一方 |
| 多人同时交互 | 服务器检测并广播结果 | 避免竞争条件导致不同步 |
| AI行为树 | 服务器完全运行,客户端仅模拟表现 | AI决策不复制,性能省很多 |
| 掉血伤害判定 | 服务器射线/碰撞检测后同步结果 | 防止外挂和作弊,保证一致 |
| 武器开火特效 | Multicast RPC(或自定义同步) | 让任何视觉事件所有客户端都触发 |
| UI提示 | GameState复制事件,客户端监听 | 确保所有人都看到相同进度 |
| 个人成就/解锁 | PlayerState复制 | 每个人看到他自己的是玩家专属状态,其他人也可读取 |
这个表可以作为你设计功能时的“模板问答”。每次都要问自己:这是状态还是事件?这个状态是所有人都要看,还是只有Owner看?这个事件是服务器发起的还是客户端发起的?
6. 实战最后一公里:小优化与个人体会
做到了前面这些,其实Coop的基本骨架已经是好的。最后再说几个肉眼可见的体验优化点。
第一,Net Update Frequency和LOD。玩家角色默认能保持很流畅的同步,但讲究射击反馈的项目,要把玩家角色和重要敌人设为最高优先级,保证位置的更新速率足够。像门、开关这类的交互物件不需要高频同步,可以用Interp方式更新位置,减少复制流量。
第二,动画同步。很多人喜欢用Set Actor Location直接调整门、滑动板等物体,如果你每帧都在复制整条位置流,没有必要的。要记住,UE5的复制频率有限,如果你给一个门设了较高的网络更新频率,就会大幅占用带宽。门开关动画尽量用Multicast一次性触发,而不是每帧同步它的旋转。
第三,延迟补偿。Coop不太像对战竞技那么严格,射击手感还是要照顾。在服务器开火检测时,可以稍微往回反推客户端的位置,进行命中检测。这个在UDK时代叫ClientSideHitScan,在UE5里也有相关网络预测配置。
第四,内存与性能。AI数量一旦多,复制压力集中。你可以对AI使用“休眠式复制”:
- 当玩家距离AI很远时,服务器不让AI Move Tick那么频繁,复制频率降低
- 超过一定范围干脆关闭该AI的RPC和属性复制,视觉上因为距离远你会发现不了
- 玩家离得近了再苏醒
这都是NetPrioritize和NetDormancy的用途。做四人合作时,你机器上能跑30个AI,还得再四个人流畅,就得从这些地方扣成本。
我个人做了几个网络项目后最大的体会,是心理健康比技术更重要。网络同步的Bug有时候就是不出现,一到三四个客户端一起跑,总是偶发卡死、瞬移、不同步。你不要恐慌,先把节点情况拆成“服务器日志”和“客户端日志”,再对照状态。很多问题不是一次改对的,而是多跑几轮、多看几次日志,渐渐发现规律。
最后给自己一句提醒:在Coop项目里,“看起来正确”不等于“网络正确”,更不等于“可维护”。你前期花一天时间把职责理清楚、把复制方案定下来,后期省下的调试时间可能是十倍。我经验中所有网络同步的背上噩梦,都是因为刚开始少想了一个“谁在不同机器上会看到什么状态”。
如果看完你正准备开动,我建议你现在就去把一个最简单的Pawn复制跑通,再往上面加门和AI。先解决“能看到别人在动”这个基本问题,Coop才有意义。剩下的,都是打磨。