ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

UE5专用服务器架构与网络同步实战:从入门到部署避坑

UE5专用服务器架构与网络同步实战:从入门到部署避坑 项目第一个多人原型做出来的时候我犯了一个特别常见的错误直接用UE5默认的监听服务器在局域网里测几个人跑得挺欢一到公网上就原形毕露——延迟高、掉线、各种不同步开枪打不中人敌人瞬移。这个问题其实不是网络环境差而是我压根没搞清楚UE5里服务器和客户端到底是什么关系什么逻辑该跑在服务器上什么逻辑只能在客户端跑。后来我推倒重来花了大概三周把项目迁到Dedicated Server专用服务器架构上才真正理解了UE5的网络同步。这篇内容不是官方文档翻译是我自己从零搭DS、拆同步、压带宽、上云部署这一路踩出来的经验。如果你正准备做多人联机游戏或者已经在做但总觉得同步逻辑很别扭这篇文章应该能帮你少走不少弯路。首先要弄明白的不是高级技巧而是专用服务器到底在管什么。1. 先搞清楚专用服务器到底在管什么1.1 监听服务器和专用服务器的本质区别UE5里有多人联机的两种服务器形态监听服务器Listen Server和专用服务器Dedicated Server。监听服务器说白了就是主机玩家的电脑既跑游戏、充当服务器本地画面照常渲染其他玩家连进来。它的好处是部署简单、不需要额外开销几个人局域网随便玩完全够用。坏处也明显主机玩家天然有优势网络波动会同时影响服务器和主机的游戏体验而且主机一退整局游戏直接崩。专用服务器则完全不一样。它是一个不渲染画面、没有本地输入、也不显示任何UI的独立逻辑进程。整个世界的碰撞、伤害计算、AI决策、状态变更都在这台服务器上跑然后通过网络把游戏状态同步给各个客户端。所有客户端本质上都是“远程操作员”只是把他们看到的画面渲染出来并且把输入发回服务器等结果。从架构上讲DS才是多人游戏的标准答案。它是权威源能防止客户端作弊、保证所有玩家看到的是一致的游戏世界。代价就是你需要额外买一台云服务器并且网络同步逻辑要从第一天就规划好——这一点后面会反复强调。1.2 为什么不能拿客户端逻辑当服务端逻辑很多人刚开始都会犯一个思维惯性代码写起来“看起来就像单机一样”我在哪儿都能跑。但是UE5的DS环境里很多客户端API是没有意义的。比如UGameplayStatics::GetPlayerController(GetWorld(), 0)在客户端上能拿到第一个本地玩家控制器在DS上返回的是空指针。再比如角色武器特效、摄像机抖动这类功能如果一股脑跑在服务器上只会白白增加带宽和逻辑开销——因为服务器根本没有渲染也不需要播放特效。还有权限问题。默认情况下客户端可以直接调用服务器上的某些函数Server RPC但服务器不会自动信任客户端的输入。角色移动、开火、交战这类关键逻辑一个合格的架构是客户端发请求服务器校验服务器准了再广播结果。客户端本地最多做预测预测错了必须能被服务器修正回来。如果客户端自己把血量改了、把位置改了DS不认账那这游戏就是给别人送作弊器。这也是我后来把项目迁到DS之后最直观的感受写每个函数之前先问一句“这个逻辑应该跑在哪个端”。这个习惯比任何架构设计都重要。1.3 DS模式下每个Gameplay类各管哪一块UE5的Gameplay框架里每个类在DS模式下的存在范围和职责都不一样我直接整理成一张速查表类服务器上是否存在客户端上是否存在核心职责AGameModeBase / AGameMode存在唯一不存在游戏规则、出生、胜负判定只在服务器上跑AGameStateBase存在存在同步全局共享状态比分、时间、阶段复制给所有人APlayerState存在存在同步每个玩家独立的状态昵称、分数、队伍复制给所有人APlayerController存在存在仅所属客户端输入映射、UI逻辑服务器与客户端各持有一份APawn / ACharacter存在存在同步实战逻辑、移动、攻击具体复制策略看需求AActor服务器产生按Relevancy复制动态物体、Pickup、弹道这张表值得贴在工位上。绝大多数同步问题的根源都能在这张表里找到不是把某个变量复制漏了就是把一个只该在服务器上出现的Actor竟然在客户端也生成了或者反过来客户端生成了一个Pickup服务器不知道两个端就各玩各的。回到开头我迁到DS后的第一件事就是把原本监听服务器里“主机的本地逻辑”全部拆掉按这张表重新归类了一遍Gameplay类的职责。拆完再上DS世界就清楚多了。2. UE5网络同步机制拆解不明原理写了也是白写2.1 属性复制状态同步的地基DS上跑的是世界逻辑但客户端看到的永远是服务器同步过来的“快照”。UE5默认的同步模型是状态同步State Synchronization核心是属性复制Property Replication把一个Actor的某个变量从服务器复制到客户端。最常见的写法是这样的UCLASS() class MYGAME_API AMyCharacter : public ACharacter { GENERATED_BODY() public: UPROPERTY(Replicated) float Health; virtual void GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const override; }; void AMyCharacter::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AMyCharacter, Health); }注意几个关键点。第一标记了UPROPERTY(Replicated)之后还必须在GetLifetimeReplicatedProps里用DOREPLIFETIME声明否则变量不会进入复制通道。第二变量在服务器上修改客户端会被动拿到最新值但如果希望在客户端拿到值时做点自己的处理比如播放受击动画、更新UI血条就要加RepNotifyUPROPERTY(Replicated, ReplicatedUsing OnRep_Health) float Health; UFUNCTION() void OnRep_Health();OnRep_Health在客户端收到新值的那一刻被调用如果你要做特效、做UI反馈放在这里就是最合适的。注意这个回调只在客户端触发服务器上你直接调用逻辑函数就行。提示新手最容易漏掉的是头文件里的声明和实现文件里的实现不一致或者忘了在GetLifetimeReplicatedProps里补充DOREPLIFETIME。只要变量不复制客户端永远拿不到最新值这类Bug看日志往往还看不出明显错误只能逐个变量排查。复制不是越多越好。每个复制的变量服务器都要在网络帧里打包并发送。如果一场对局里腰带上挂了三十个带Replicated的变量服务器每帧都会为每个变量做序列化哪怕值根本没变。所以上线前我会做一轮“复制变量瘦身”能不放Replicated就不放能合并成一个结构体配合自定义NetSerialize更好。很多新手觉得加个Replicated很简单实际上它是最容易把服务器卡成PPT的隐藏炸弹。2.2 RPC与调用权限谁说了算属性复制负责“状态”RPC则负责“事件”。UE5里RPC分三种Server RPC、Client RPC、Multicast RPC。Server RPC客户端发起服务器执行。典型场景是玩家按下开火键客户端告诉服务器“我要开火”服务器验证后真正扣血。Client RPC服务器发起指定某个客户端执行。典型场景是服务器给某个玩家同步一份只有他看得到的UI事件。Multicast RPC服务器发起所有客户端都执行。典型场景是爆炸震屏、全服公告这类所有人必须知道的事件。声明上同样用标记符UFUNCTION(Server, Reliable) void Server_Fire(FVector TargetLocation); UFUNCTION(Client, Reliable) void Client_ShowDamage(); UFUNCTION(NetMulticast, Reliable) void Multicast_Explosion(FVector ExplosionLocation);Reliable和Unreliable的区别要理解清楚。Reliable保证消息一定到达代价是开销更高、可能会重传Unreliable丢包就丢了适合高频事件比如位置更新、开火特效。我的习惯是关键逻辑开火、换弹、血量变化用Reliable纯表现层的高频反馈弹道粒子、武器音效用Unreliable。权限限制最容易踩坑。Server RPC只能由拥有该Actor的客户端调用别人调用会静默失败Multicast RPC只能由服务器调用客户端调用不会有任何效果。这个限制是UE5刻意做的安全设计。实际开发里我见到最多的问题就是在客户端代码里调了一个Multicast想着让所有人都受击结果服务器根本没收到消息BUG还特别难查。排查时不光要看代码还得看日志里有没有“Failed to execute RPC”这类提示。2.3 Actor复制频率与Relevancy为什么服务器人一多就开始卡属性复制解决的是“复制什么”但服务器还要决定“什么时候复制、复制给谁”。这背后有两个关键参数NetUpdateFrequency复制频率和NetPriority网络优先权以及一个核心函数IsNetRelevantFor()。NetUpdateFrequency默认是100意思是每秒尝试把Actor的状态向关心的客户端更新100次。对于一个八人房间场景里如果有几百个动态Actor服务器每秒要处理的复制量就是几百乘以100这个开销很快就会压爆CPU和带宽。别以为服务器只跑逻辑就很闲——多数情况下DS的瓶颈恰恰在复制。Relevancy是另一个隐藏杀手。UE5服务器在判断“某个Actor是否需要发给某个客户端”时会先走一遍Relevancy检查。默认情况下距离太远、所在关卡不同、客户端关注的优先级不高的Actor都不会复制。但是如果你的项目里大量Actor都设置了bAlwaysRelevant true或者没有正确实现Relevancy检查服务器就会把世界里的所有东西都发给所有人延迟瞬间爆炸。我自己做优化时常用的三板斧一是把无关紧要的Actor的NetUpdateFrequency从100降到10到30二是用DOREPLIFETIME_CONDITION把不需要复制给所有客户端的属性限制成COND_OwnerOnly或COND_SkipOwner三是大量同类型的Actor尽量用FastArraySerializer做批量同步而不是每个Actor各自走独立复制通道。这三招做完服务器的呼吸都顺畅了。3. 编译与部署把DS真正跑起来3.1 编译前的Target配置与工具链准备搞懂了同步机制接下来就是把专用服务器编译出来并部署到云上。这时很多新手会被卡住UE5的项目默认编译出来是客户端怎么变成服务器其实UE5的构建体系是区分Target的。工程目录里会有一个*.Target.cs比如MyGame.Target.cs里面定义了构建的目标类型。要生成专用服务器需要服务器Target一般可以直接用项目的Server构建目标。如果你的工程里还没有专门的Server Target一般不需要自己新建——UE5自带的UAT会把项目当作Game目标和Server目标分别构建前提是你在命令行里指明-server。先确认工具链Windows上要装Visual Studio建议VS2022安装时勾选“使用C的游戏开发”工作负载和对应的Windows SDK如果要部署到Linux服务器还需要在Windows上安装Linux交叉编译工具链。别小看这一步很多人卡在“明明本地能编译怎么一交叉编译就报错”八成就是Linux工具链没装全。3.2 用UAT一键构建专用服务器UE5构建服务器最省心的方法是走RunUATUnreal Automation Tool。打开一个命令行切到引擎目录输入Engine\Build\BatchFiles\RunUAT.bat BuildCookRun -projectD:\Project\MyGame\MyGame.uproject -server -noclient -build -cook -stage -pak -archive -archivedirectoryD:\Builds\DS_Win64简单拆解一下参数-server表示构建服务器目标-noclient意味着不生成客户端包-cook是烘焙内容-stage是整理输出-pak是打包成pak文件-archive是把最终产物拷贝到指定目录。整个流程跑完你会拿到一个包含Server二进制和内容包的目录里面通常有类似MyGameServer-Win64-Shipping.exe这样的可执行文件。构建期间大概率会遇到几个典型报错。比如“Target {Name} is not present”多半是工程路径写错了或者Target文件不完整再比如“Missing Linux cross compile toolchain”那就是交叉编译那把工具没装。我的经验是第一次构建不要加太多参数先跑一个最小化的-build把项目编过再去加-cook -stage -pak这样出错时定位更快。3.3 服务器启动参数与云端部署清单打包完成后就能启动服务器了。最朴素的启动方式是用命令行MyGameServer-Win64-Shipping.exe MyGame -log -PORT7777 -MaxPlayers32 -NOSTEAM-server参数在一些版本里可以省略但显式写上更稳妥。-log会打开一个命令行日志窗口方便你观察服务器状态-PORT7777指定UDP监听端口-MaxPlayers限制最大人数如果你的项目接入了在线平台SDK但当前不想登录在线服务加-NOSTEAM之类的参数可以跳过平台校验。注意部署到云主机时有几件事是硬性要求。一是云平台安全组里放行UDP端口默认7777如果准入门户或查询端口也是7777要一并放行二是启动后从客户端控制台直接输入open IP:7777进行验证三是最好用systemdLinux或Windows服务方式把服务器进程挂后台避免SSH一关服务器就断。对于Linux服务器可以从Windows交叉编译出Linux版本也可以用支持Linux的云构建环境产物和启动方式都类似注意给可执行文件加执行权限并安装好对应的运行库。一个小提醒DS进程本身不渲染游戏画面但对CPU、内存的要求并不低。我见过很多朋友拿1核1G的小主机跑DS结果人一多直接Out of Memory。做服务器预算的时候结合项目复杂度至少按2核4G起跳大世界或者频繁复制大量Actor的项目还得再往上加。4. 卡顿与帧同步的网络优化思路4.1 状态同步的卡顿从哪来聊完部署就得聊真正的硬骨头为什么多人游戏一上线就卡我说的不是那种偶尔的掉帧而是打一局下来全程“肉”、指令延迟感特别重的那种卡。状态同步模式下客户端表现的是服务器状态的一个滞后快照。哪怕网速很好信息从客户端到服务器再返回也要经历两轮往返。如果服务器本身逻辑太重Tick时间太长客户端看到的滞后还会进一步放大。所以卡顿的来源我习惯分成两层网络层的延迟和丢包以及服务器层的CPU瓶颈。前者受物理链路影响后者完全可以靠优化解决。之前我用Unreal Insights连续跑了半小时的服务器日志发现一个很有意思的现象很多Actor的NetUpdateFrequency设置不合理比如把场景里所有Pickup都设成100Hz复制但实际上这些静态拾取物根本没有变化。把这些Actor复制频率降到5Hz之后服务器平均帧耗时直接下降了接近三成。优化不是玄学就是先量化再瘦身。4.2 帧同步思路与UE5的适配如果你做的是策略游戏、体育游戏或者对确定性要求极高的玩法你可能会考虑帧同步Lockstep。帧同步的核心是所有客户端在同一帧接收并处理完全相同的输入序列每个客户端本地跑同样的确定性逻辑最终得到一样的结果。它天然抗高延迟因为只要每帧的输入一致就行。但UE5默认框架并不直接支持帧同步因为Replication、Gameplay Ability System这些组件都是为状态同步设计的。你要在UE5里做帧同步通常得绕过默认的移动复制自己实现输入收集、帧号广播、逻辑确定性校验。有个很现实的问题是浮点精度不同平台的浮点计算可能有细微差异帧同步对这种差异非常敏感一旦两边计算出一个像素的偏差后面会越差越多最终完全不同步。我的个人观点是射击、动作、MOBA这类偏手感、强反馈的游戏踏踏实实做状态同步回合制、战棋、以及一些强策略的玩法帧同步更合适。两种方案没有绝对优劣但选型必须在写代码前定下来。真要在UE5里做帧同步起码要有能力hold住网络协议层和确定性数学否则从状态同步改到帧同步往往是伤筋动骨的大重构。4.3 一套可落地的优化路径这里再分享一套我自己常用的优化路径按顺序做一次别贪多。第一用Unreal Insights抓服务器和客户端的Timeline找出Tick耗时最长的几块。第二审视每个Actor的NetUpdateFrequency和NetPriority把“值没变但也一直在复制”的Actor全部降频。第三检查Relevancy设置别让服务器把不该发给某个客户端的Actor也发过去。第四把高频小对象从独立Actor复制改成FastArraySerializer或自定义NetSerialize减少序列化开销。第五服务器Tick尽量拆分别把所有计算堆在同一个Tick里。第六给PlayerController和Pawn的移动补上客户端预测和服务端修正手感会比“纯服务器权威”舒服很多。如果你已经做了这几步卡顿依然明显那就要考虑架构层面是否选错了复制粒度是不是该把某些实时状态改成快照式同步或者引入专门的网络同步框架。记住一个原则能少传就少传能合并就合并能只在服务器上算的就坚决不发到客户端。5. 常见问题与排查技巧实录5.1 连不上服务器的原因排查把所有基础问题汇总成一张速查表实际排查的时候对着看现象可能原因处理办法客户端输入open IP:7777超时云安全组/防火墙没放行UDP端口放行对应端口规则别只开TCP服务器启动后立刻闪退缺运行库、权限不足、端口被占用检查日志、换端口、用管理员运行客户端报版本不匹配客户端与服务器内容版本不一致重新烘焙并一起发布启动时卡在登录界面在线平台SDK未配置先加参数跳过平台SDK再调试再补充一个很常见的坑用云服务器的时候不要只开安全组端口还要看服务器系统防火墙是否拦了UDP。我自己就遇到过一次安全组放行了、但系统防火墙默认规则挡掉UDP的情况当时排查了整整一下午。5.2 角色瞬移、不同步、延迟高角色瞬移十有八九是复制的移动没配置对。最常见的是忘了把Character的bReplicateMovement设为true或者你自定义的移动逻辑没有走服务器权威路线客户端自己算完位置后服务器不认于是一拉一扯。正确做法是客户端发送输入请求服务器执行移动并回传Transform客户端在本地做预测和插值最终展示服务器修正后的结果。不同步还要检查是不是属性复制漏了。特别是动态生成的Actor、游戏状态变量很多新手只标记了UPROPERTY(Replicated)但忘了在GetLifetimeReplicatedProps里声明或者条件写成了COND_InitialOnly导致后续变化不更新。这类问题在局域网试的时候不容易暴露因为局域网延迟低状态同步的跨度小一上公网就看出差距了。延迟高的优化方向前面已经说过这里再补充一个技巧在高并发场景下尽量不要在RPC参数里传递整个结构体或大量数组能传ID就让客户端自己查表。尤其在Unreliable RPC里传大数据丢包重传会把带宽直接打满。提示如果你遇到“客户端能连上但角色一直站原地不动”先别急着动网络代码用服务器日志确认客户端输入有没有到达服务器再用客户端日志确认Transform复制有没有到达客户端。很多时候是中间某一环没通而不是网络本身的问题。5.3 DS崩溃与环境配置类问题DS本身是无头进程很多在编辑器里跑得好好的蓝图部署到DS上反而崩。原因通常是蓝图里用了UI和渲染相关的节点在DS上被调用或者某个函数在服务器上访问了GetPlayerController但返回了空没有判空直接继续往下走。还有一个我踩过坑的点DS上加载资产和客户端不完全一样特别是World Partition大地图、软引用、异步加载这些服务器如果没有主动加载对应关卡区域客户端收到的事件可能找不到有效Actor直接崩。排查这类问题唯一的思路就是开-log日志配合ds crashed dump看堆栈十有八九是拿到空指针或者访问了不存在的对象。写服务器代码时判空一定要做到位DS上宁可多跳过几步逻辑也别让一个空引用把整台服务器带走。最后说点个人体会。我踩过的最大的坑就是以为自己懂网络同步其实只是看懂了文档。每次改同步逻辑我都会先在DS上真跑一遍加日志打点开Unreal Insights看数据再回去改代码。如果服务器都没跑起来谈优化全是空话。这套流程我现在已经固化成习惯了写逻辑前先想清楚这段逻辑该跑在哪个端写完用局域网连一次观察Simulated Proxy上的表现正不正常最后才上云部署并且顺手看一眼服务器的CPU、内存、带宽曲线。希望这篇能帮你少走弯路把更多时间花在真正有意思的玩法上。
返回列表