1. 开局先搞清楚:这两个类到底管什么
先讲个我自己的经历。有一次接手一个联机合作项目,玩家进游戏大厅之后要选角色、选难度、带道具,然后开始一局比赛,打完回大厅继续下一局。当时项目里有个老哥,把玩家选的角色和道具直接存在关卡里的某个Actor身上,结果每次换关卡数据全没了,联机时服务器和客户端的数据还对不上,一堆玩家反馈“我明明选了A角色,进游戏变成B了”。后来排查了半天,问题就出在数据放错了地方。
这就是GameInstance和GameMode最核心的职责区分问题。简单说,GameInstance管的是“整个游戏进程”的事,GameMode管的是“当前这一局”的事。数据放错地方,轻则逻辑混乱,重则联机直接翻车。
这个话题展开讲,能聊的东西其实非常多。GameInstance和GameMode是UE4里两个最常用但又最容易被低估的类。很多人学了蓝图之后就往Character里塞各种逻辑,结果项目越做越乱。实际上,搞懂这两个类的定位、生命周期、网络行为以及它们之间的配合方式,你的项目架构会清晰一大截。这篇文章我把自己踩过的坑和总结的经验全写出来,涉及生命周期、网络复制、全局数据管理、规则控制,还有外接设备映射和查询/物理模拟器的边界问题,尽量把每个点都说透。
1.1 先理解两者的定位差异
很多人上来就背概念:GameInstance是全局的,GameMode是每局的。听起来很简单,但一写代码就分不清了。
我用一句话总结:GameInstance是你游戏程序的“进程级”对象,GameMode是你“当前关卡”的“规则集”。
什么叫“进程级”?就是你的游戏从启动到退出,整个进程存在期间它都在。不管你怎么切换关卡、加载地图、进菜单、回大厅,GameInstance始终活着,它持有的数据不会因为关卡切换而丢失。
什么叫“规则集”?GameMode定义了当前地图怎么玩:玩家怎么出生、有多少人、能不能暂停、用什么PlayerController、用什么Pawn、胜利条件是什么。关卡一切换,GameMode就销毁了,换成新关卡的GameMode。
如果你把“玩家选了哪个角色”放在GameMode里,玩家从大厅进战斗地图,GameMode一换,数据就没了。如果你把“全局音量设置”放在GameMode里,每次进新关卡音量就重置了,玩家肯定骂娘。
理解了这层定位差异,你再去思考数据放哪,正确率至少提升80%。
1.2 GameInstance的定位:整个游戏的“持久内存”
GameInstance活在引擎启动到引擎关闭这段时间里,跨关卡、跨地图、跨一切加载流程。它是UE4里少有的“全局单例”性质的类,所有玩家、所有关卡、所有UI都能访问它。
所以它天然适合做这几类事情:
- 全局配置数据:画面设置、音量、语言、按键绑定
- 跨关卡传递的玩家数据:玩家ID、角色选择、道具栏、金币数
- 全局服务和管理器:网络会话管理器、存档管理器、音频管理器、外部设备映射管理器
- 大厅与关卡之间的中间数据缓存:比如匹配成功后,把匹配结果存到GameInstance,再切地图
但GameInstance有一个很关键的坑:它不参与网络复制。服务器上的GameInstance和客户端上的GameInstance是两个完全独立的对象,彼此之间不会自动同步任何数据。
这一点太多人踩坑了。有人把玩家血量上限存在GameInstance里,想着全局统一好管理,结果联机时服务器和客户端各存各的,数据完全对不上。正确做法是:GameInstance里存“局外数据”,局内实时数据走Actor复制或GameMode的服务器权威逻辑。
1.3 GameMode的定位:单局比赛的“裁判规则”
GameMode只在服务器上存在,客户端没有GameMode。它是纯服务器权威类,负责制定规则、裁决胜负、管理系统流程。
GameMode在关卡加载时创建,关卡卸载时销毁,生命周期绑定在单个关卡上。它适合做这些事情:
- 设置默认Pawn、PlayerController、PlayerState、HUD
- 控制玩家出生点、复活逻辑、初始道具
- 管理比赛状态:准备中、进行中、结束
- 判定胜负条件、计分规则
- 生成AI、刷怪、控制比赛节奏
GameMode没有复制函数,它不需要复制,因为它的逻辑只跑在服务器上。客户端那边通过PlayerController、PlayerState、GameState来间接感知游戏规则。
这里有一个很容易搞混的概念:GameMode和GameState的区别。GameMode是服务器上的规则制定者,GameState是服务器上状态的中继广播器。GameMode定了“这局谁赢了”,GameState负责把这个结果同步给所有客户端。所以GameState可以复制,GameMode不行。
2. 实战核心:数据放哪、规则放哪,两者怎么配合
现在进入真正的实战环节。光知道定位还不够,你得知道具体怎么用才能把项目做得清爽、稳定、不翻车。这一章我把GameInstance和GameMode最常见的配合场景拆开讲,每一步都配上思路和原因。
2.1 GameInstance全局数据管理的实用套路
先讲GameInstance里最常用的数据管理方式。我这里不扯那些复杂的框架,就说一个最实在的落地做法:
把GameInstance当作你整个游戏的“客户端数据总线”。所有需要在关卡之间传递、保留的数据,统一通过GameInstance读写。
比如一个典型的游戏流程:玩家打开游戏,进入主菜单,选择“开始游戏”,进入角色选择界面,选好角色后进入战斗关卡,打完战斗回到主菜单或者进入结算界面。
这个过程里,玩家选的角色、难度设置、携带的道具、当前金币数,全部都应该存在GameInstance里。
我一般的做法是:在GameInstance里定义一组变量,用蓝图和C++都行,然后暴露Get/Set接口或者直接用引用。注意,这里有一个关键点:GameInstance本身在蓝图中很容易访问,直接Get GameInstance节点就能拿到,然后Cast成你的自定义GameInstance子类。
但蓝图里频繁访问GameInstanceCast一次就要浪费一点点性能,而且代码很啰嗦。我更推荐的做法是:做一个静态函数或者蓝图函数库,内部封装好对GameInstance的访问,这样外部调用就一行代码,干净利落。
C++写法大致是:
UCLASS() class UMyGameInstance : public UGameInstance { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "SaveData") void SetSelectedCharacter(int32 NewCharIndex); UFUNCTION(BlueprintPure, Category = "SaveData") int32 GetSelectedCharacter() const; private: UPROPERTY() int32 SelectedCharacterIndex = -1; };然后外部调用:
UMyGameInstance* MyGI = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(this)); if (MyGI) { MyGI->SetSelectedCharacter(2); }用蓝图的话,套路也差不多,但我会额外封装一个“全局管理器接口”,把那些频繁访问的逻辑统一收敛,避免蓝图节点连线连成一锅粥。
再强调一次:GameInstance里不要存局内实时数据。比如玩家当前的HP、子弹数量、buff剩余时间,这些数据属于局内状态,应该由Actor或GameMode管理,通过复制同步到客户端。GameInstance只做“跨局数据”和“全局配置”的存储。
2.2 GameMode规则控制的实用套路
GameMode的核心价值,是把游戏的“玩法规则”集中放在一处管理。一个清晰的项目,GameMode里的逻辑应该像一本规则说明书,而不是一锅逻辑大杂烩。
实战里我习惯把GameMode拆成几个重点区域:
- 玩家连接和出生管理
- 回合/比赛状态机
- 胜负判断与结算
- 出生点与复活逻辑
拿一个最基础的“死亡竞赛”模式举例,GameMode里要处理的东西大致有:
- 玩家加入时,调用RestartPlayer选择出生点并生成Pawn
- 玩家死亡后,延迟几秒再调用RestartPlayer
- 击杀数达到目标后,结束比赛并宣布赢家
这些逻辑全部集中在GameMode里,客户端那边通过监听GameState的变化来响应界面更新。
具体到蓝图/代码,GameMode最常用的重写接口包括:
- InitGame:游戏开始时的初始配置,常用于读取地图配置、生成规则参数
- PostLogin:玩家登录后的处理,新手最容易漏掉这个
- RestartPlayer:重写这里可以自定义玩家的出生逻辑
- Logout:玩家退出时清理数据
- HandleStartingNewPlayer:新玩家加入时初始化
这里面PostLogin和Logout是特别容易被忽略的。如果你需要在玩家加入时给他一个初始道具、绑定一个初始UI,或者玩家退出时保存数据,这两个接口是你的首选。
代码示意:
void AMyGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); // 玩家登录后,给玩家设置初始数据 // 例如:NewPlayer->ServerSetSelectedCharacter(GI->GetSelectedCharacter()); } void AMyGameMode::Logout(AController* Exiting) { Super::Logout(Exiting); // 玩家退出时,保存玩家数据或广播退出事件 }这里面有个细节值得多说一句:PostLogin只在服务器上执行,所以你在里面可以放心地操作服务器权威数据,比如给玩家生成专属的PlayerState并初始化数据。客户端那边无法直接调GameMode,但可以通过PlayerController的RPC来请求服务器执行某些操作。
2.3 两者配合的经典模式:大厅进战斗的数据交接
现在我们做一个最常见的实战场景:大厅选择角色,进入战斗地图,战斗地图的GameMode根据玩家的选择来生成Pawn。
第一步,玩家在大厅里选择角色,这个选择存到GameInstance。第二步,服务器进入战斗地图,加载对应的GameMode。第三步,GameMode在PostLogin或RestartPlayer时,去GameInstance里读取玩家选择,然后生成对应角色。
这个链路里有两个关键点:
第一个关键点:服务器和客户端对GameInstance的读取目标不同。服务器上的GameInstance是服务器版本的,客户端上是客户端版本的。如果是单机游戏,两者是同一个程序,没问题。但如果是联机游戏,客户端的GameInstance数据不会自动同步到服务器。
所以,如果是联机游戏,玩家在大厅选择了什么角色,客户端要把这个信息通过RPC发送到服务器,服务器写入自己的某个变量,然后再由服务器的GameMode来读取和生成。
我见过不少项目在这里翻车:客户端自以为“我已经把角色选择存到GameInstance了”,联机时服务器根本不知道,生成出来全是默认角色。
正确做法是:客户端把选择结果通过RPC传给服务器,服务器把它存在PlayerState或服务器端的GameInstance变量里,等GameMode需要时再读取。
第二个关键点:数据读取的时机。GameMode的PostLogin触发时,玩家已经在服务器上登录了,但Pawn可能还没生成。如果你需要根据玩家选择来生成Pawn,应该在RestartPlayer里处理,或者在PostLogin里先保存数据,等生成Pawn时使用。
这里的套路我总结成一句话:GameInstance负责“带数据过关卡”,GameMode负责“按数据造Pawn”。中间的桥梁是服务器端的会话数据或PlayerState。理解了这句话,你就能应对绝大多数类似场景。
3. 生命周期与网络复制:最容易翻车的地方
生命周期和网络复制是GameInstance与GameMode最大的深水区。很多开发者单机玩得溜,一上联机就各种诡异Bug,十有八九是这两块没吃透。这一章我把生命周期、网络模型以及它们对外接设备映射和物理模拟器查询的影响一起讲清楚。
3.1 GameInstance的生命周期:从启动到退出
GameInstance的创建时机大约是引擎初始化之后、第一个关卡加载之前。它销毁的时机是游戏进程关闭、引擎开始销毁所有对象时。
在这个生命周期里,有几个需要特别注意的节点:
- 游戏刚启动时,GameInstance最先创建,然后是默认地图加载
- 每次切换地图,GameInstance会收到一个通知(比如NotifyPreClientTravel、NotifyPostLoadMap等)
- 游戏退出时,GameInstance最后销毁,所有数据不可再访问
这意味着,如果你有一些数据需要在“切换到新关卡时做一次重新初始化”,你得监听地图加载事件,而不能依赖GameInstance的重建。
我在项目中就经常遇到这种情况:玩家从大厅进入战斗关卡,战斗关卡的GameMode里需要读取大厅里选择的地图参数。这个参数存在GameInstance里没问题,但GameMode创建的时候,GameInstance是否已经准备好数据?大多数情况下是准备好的,因为GameInstance早就活着了,数据也早就写进去了。
但要注意一个反向的场景:玩家直接启动战斗关卡(比如从编辑器直接Play、或者通过命令行启动),这时候GameInstance虽然活着,但它里面没有任何大厅阶段的数据,如果你直接读取未初始化变量,就会出现空数据或默认值。
我的建议是:在GameInstance里给所有“关卡切换需要的数据”设置一个默认值,并且在读取时做校验,发现是非法值就回退默认。别假设数据一定存在。
3.2 GameMode的生命周期:从加载到卸载
GameMode的生命周期绑定在一个关卡上。具体来说,关卡开始加载时,UWorld根据地图配置创建新的GameMode实例;关卡卸载时,GameMode销毁。
这里有一个关键点:UWorld和GameMode的关系。一个UWorld对应一个关卡,GameMode是UWorld的下级对象。当你用OpenLevel切换地图时,旧UWorld销毁、新UWorld创建、GameMode也随之重建。
GameMode有几个生命周期事件值得关注:
- InitGame:关卡初始化时调用,适合读地图配置
- StartPlay:游戏开始逻辑,所有Actor已经生成完毕
- PostLogin:玩家登录后调用
- BeginPlay:GameMode本身也是Actor,BeginPlay也会触发
这里踩坑最多的是InitGame和BeginPlay的执行顺序。InitGame在所有Actor生成之前调用,BeginPlay在Actor生成之后调用。如果你在InitGame里访问场景中的Actor,就会扑空。
我一般在GameMode里这样安排:InitGame里只做配置读取,BeginPlay里做规则初始化,RestartPlayer里做具体的玩家创建,PostLogin里做玩家数据绑定。这样职责清晰,也不会因为顺序问题踩坑。
3.3 网络模式下谁在谁不在
网络模式下,GameInstance和GameMode的存在性有巨大差异,很多人一联机就懵了,因为这个差异。
GameInstance在服务器和每个客户端上都有一个实例,它们彼此独立,不通信、不复制。客户端A的GameInstance和服务器上的GameInstance完全是两个东西,改其中任何一个都不会影响另一个。
GameMode只在服务器上存在,客户端上没有GameMode实例。客户端试图调用GetGameMode时,拿到的是空引用。这一点在蓝图中尤其容易被忽略,很多人直接拖一个Get GameMode节点,结果客户端上一直返回null,还以为是Bug。
那么客户端靠什么获取规则状态?答案是GameState。GameState在服务器上创建,并且会自动复制到所有客户端,客户端可以安全地读取GameState数据。
所以,你的架构思路应该是:
- 服务器上的规则修改走GameMode
- 状态广播和客户端读取走GameState
- 跨关卡持久数据走GameInstance
- 玩家个体数据走PlayerState
这套模型理解了,网络相关的很多坑都能提前避开。
4. 热词实战:外接设备映射与查询/物理模拟器的边界
这一章专门聊两个比较新而且容易混淆的话题。一个是外接设备映射,另一个是查询与物理模拟器的区别。它们表面上看起来和GameInstance、GameMode关系不大,但实际上不管实体外设还是角色控制,最终都要回到这两个类来管理生命周期和数据存储。
4.1 外接设备映射放在哪一层才合理
所谓外接设备映射,简单说就是把外部设备的输入事件映射成游戏内的逻辑动作。比如方向盘、踏板、飞行摇杆、跳舞毯、甚至自定义的串口按钮板,都需要通过映射层转成游戏可识别的输入指令。
为什么要单独聊这个?因为我见过太多项目把设备映射逻辑塞进Character或PlayerController里,导致换关卡时映射失灵、联机时设备数据只在本地生效没人处理同步。
正确思路是:把外接设备的“配置数据”和设备本身的“运行时数据”分开。
外接设备的配置数据,比如“方向盘转角映射到多大转向幅度”“自定义按键对应的游戏功能”,这种与关卡无关的全局配置,适合放在GameInstance里。因为设备配置应该在整个游戏进程中保持一致,不管你在菜单还是战斗中,方向盘的死区设置不应该因换图而重置。
设备映射的运行时状态,比如当前设备连接的端口、实时输入值、校准状态,这些数据如果是局内使用的,放在PlayerController或GameMode管理的管理器里更合理。特别是联机游戏,每个客户端的物理外设数据不应该直接写入服务器权威逻辑,而应该通过客户端输入上报给服务器。
举个例子:一个支持方向盘外设的赛车游戏。方向盘的转角数据每帧在客户端被读取,这个数据属于本地输入,直接作用于本地Pawn的控制逻辑就行;但如果游戏需要服务器做防作弊校验或回放记录,客户端就需要定期上报转角数据到服务器,由服务器信任或校验后再广播给其他客户端。
这个过程中,设备映射的配置从GameInstance读取,设备实时数据在客户端本地进行处理,需要同步的数据再走PlayerController的RPC上报。三个层面各司其职,系统才能稳定可靠。
这样的分层还有一个好处:切换关卡时,GameInstance里的设备配置不丢,玩家不需要重新校准设备。而局内设备运行时状态由GameMode或PlayerController重新初始化,避免旧关卡的脏数据带入新关卡。
4.2 查询与物理模拟器的区别在实战中怎么体现
另一个容易踩坑的概念是“查询”和“物理模拟器”的区别。听着好像都是引擎内部的东西,但在做实际功能时,选错了会直接影响性能表现和联机同步。
查询(Query)在UE4里通常指的是物理查询,比如射线检测、碰撞通道查询、Overlap检测等。它的特点是立即执行、返回结果、不改变世界状态。查询只回答“这里有没有东西、碰到了什么”,它不会推动任何物理对象。
物理模拟器(Simulation)则是指真正的物理模拟,比如刚体运动、碰撞响应、力的作用。它是一帧一帧演算出来的结果,受质量、速度、约束条件等参数影响,结果带有连续时间特性。
为什么在讲GameInstance和GameMode时要提这个区别?因为查询和物理模拟在游戏架构中的归属和使用方式完全不同。
查询适合用于“服务器权威的规则判定”。比如GameMode要判断玩家的攻击有没有打中敌人,服务器可以发出一次射线检测(物理查询),看射线是否命中目标。这个操作成本低、结果明确、便于服务器裁决。
物理模拟适合用于“表现和交互”。比如角色被击飞、物体被炸飞,这种连续物理效果一般交给物理引擎去模拟。服务器可以同步物理模拟的关键事件,但不可能每帧把所有物理体状态全部同步到客户端,否则带宽直接爆炸。
实战中经常出现的问题是什么?有人把“查询”当“模拟”用,比如在客户端上直接模拟了物理击飞效果,然后试图把这个模拟结果同步给服务器。结果就是客户端表现和服务器判定完全不一致,玩家看到自己击飞了敌人,服务器判定根本没打中。
反过来,也有人把“模拟”当“查询”用,比如频繁地生成临时物理体去验证碰撞路径,这会导致性能开销爆炸。正确的做法是:短时间的判定用查询,长时间的物理表现用模拟,两者分开管理。
在GameInstance和GameMode这套架构里,查询逻辑一般放在GameMode或者服务器端的PlayerController里,因为查询结果是服务器裁决依据,需要服务器权威;物理模拟表现一般放在Pawn或Actor的组件里,由物理引擎驱动,通过RPC和复制把重要事件同步出去。GameInstance负责提供全局配置,比如设备的输入映射表、物理灵敏度设置,但这些配置也要落到具体的Pawn或GameMode逻辑中去执行。
4.3 实战示例:外接设备映射 + 查询判定的联动
我们以一个带外接踏板和方向盘的赛车游戏为例,走一遍完整链路。
第一步,设备映射配置存在GameInstance里。玩家在设置界面校准方向盘转角、踏板行程、按键映射,这些数据通过SaveGame持久化到本地,运行时由GameInstance统一管理。
第二步,客户端本地读取设备输入。打到车辆控制层,把方向盘的实时角度、踏板行程读出来,作为本车辆的输入信号。
第三步,本地输入映射到Pawn的移动逻辑。Pawn根据输入驱动车辆模型运动,这个过程可以使用物理模拟器来实现车辆的动态运行,给玩家真实的手感和反馈。但请注意,车辆模拟在客户端本地运行时的结果,不能直接作为服务器的判定数据。
第四步,查询判定放在服务器或本地可信层完成。如果是一个竞速游戏,游戏需要判断“车辆是否压过检查点”,这个可以用物理查询或区域判定来实现,结果由服务器确认,并广播给所有客户端。如果服务器确认检查点通过,GameMode更新比分。
第五步,GameMode作为中央裁决,记录每一辆车的圈数和检查点状态。比赛结束时,GameMode统一判定胜负、结算数据,并通过GameState广播到所有客户端。
这个例子里,GameInstance管设备配置和持久数据,Pawn管物理模拟和手感反馈,GameMode管理查询判定和比赛规则。每一层只做自己该做的事,互相之间通过清晰的接口衔接。
如果有人把方向盘映射放在GameMode里,那么客户端切个关卡,配置就全丢了。如果有人把车辆物理模拟结果直接当成服务器判定依据,那么不同设备、不同帧率、不同网络延迟下的结果会有很大差异,比赛公平性无法保证。这些坑我在实际项目中都见过,区分开之后项目稳定了很多。
5. 避坑指南:常见问题与排查技巧实录
聊完架构和实战,这一章专门整理一份“翻车现场实录”。每一类问题都是我或身边朋友在真实项目中踩过的,我把现象、原因、解决方案全部分享给你,方便你直接对照排查。
5.1 数据存错地方,关卡切换后丢失
这是最常见的一个问题。现象是:玩家在大厅选了一堆东西,进入战斗关卡后发现所有初始化数据消失。
原因是数据存到了GameMode或其他关卡级对象里,切换地图时对象被销毁了。
排查思路:
- 先确认数据读写对象是不是GameInstance
- 在GameInstance里加日志,查看关卡切换时数据是否被意外重置
- 检查是不是有代码在关卡加载时对GameInstance做了清空操作
解决方案就是规范数据存放位置,跨关卡数据一律走GameInstance。
5.2 客户端访问GameMode返回空
联机项目里,客户端上调用GetGameMode得到空引用,然后就崩溃或逻辑异常。
原因是GameMode只在服务器上存在,这是引擎设计使然,不是Bug。
排查思路:
- 检查调用者是否在客户端
- 确认客户端是否真的通过RPC请求服务器转发
- 用IsLocalController或HasAuthority来判断执行环境
解决方案是:客户端需要规则数据时,通过GameState读取或通过PlayerController向服务器发RPC请求,而不是直接访问GameMode。
5.3 GameInstance不参与复制,联机数据不同步
联机时,服务器改了GameInstance数据,客户端完全无感知,或者各自存各自的值。
原因是GameInstance不复制、不同步,每端独立。
排查思路:
- 检查数据是否真的需要同步
- 如果必须同步,考虑改用PlayerState、GameState或复制Actor
- 如果只是本地UI的全局设置,不需要同步,那继续用GameInstance没问题
解决方案是:分清“全局本地数据”和“全局同步数据”,跨玩家一致的数据用GameState/PlayerState,纯本地配置用GameInstance。
5.4 外接设备在换关卡后映射失效
玩家在菜单界面校准好方向盘,进入比赛后方向盘输出变得异常。
原因是设备映射配置存在了关卡内对象里,关卡切换时被销毁。
排查思路:
- 检查设备配置数据的存放位置
- 确认校准结果的持久化时机
- 看Pawn或PlayerController里是否有独立的映射加载逻辑
解决方案是:把设备配置持久化到GameInstance或SaveGame,把运行时映射加载逻辑放在Pawn初始化或PlayerController初始化时读取,这样每次进入关卡都会重新应用同一套映射配置。
5.5 游戏流程顺序导致的数据覆盖
这里分享一个我在项目中遇到过的真实问题。玩家在大厅选择一个角色后,通过“开始游戏”按钮进入战斗关卡。战斗关卡的GameMode在PostLogin时从GameInstance读取角色数据,但我当时在GameInstance里做了一个初始化操作,会在关卡加载时把角色数据重置为默认值,结果就是GameMode每次读到的都是默认值。
看起来又是一个“数据没传过去”的问题,但仔细排查后发现,数据其实传过去了,只是GameInstance在加载新地图时还做了数据覆盖操作。排查方法很简单,在GameInstance的各个生命周期函数里打印日志,看数据被清空的时间点即可。
这个问题让我养成了一个习惯:在GameInstance里专门写一个DebugPrintAllData函数,方便随时查看全局数据的状态。尤其在联机排查时,这个函数能帮你快速判断是数据没写进去、被覆盖了,还是根本没从客户端发到服务器。
5.6 速查表:GameInstance与GameMode对比
为了方便你日常开发时快速决策,我把两者的核心差异整理成一张表:
| 维度 | GameInstance | GameMode |
|---|---|---|
| 生命周期 | 整个游戏进程 | 单个关卡 |
| 是否跨关卡 | 是 | 否 |
| 服务器/客户端 | 每端独立存在 | 仅服务器存在 |
| 数据复制 | 不复制 | 不复制,逻辑不广播 |
| 适合存储 | 全局配置、跨关卡数据 | 当前比赛规则、流程状态 |
| 典型用法 | 存档管理、设备映射配置、全局管理器 | 出生管理、胜负判定、关卡规则 |
| 客户端访问 | 每端都有,可直接访问 | 客户端不可访问,需走GameState/RPC |
| 典型错误 | 存局内实时数据 | 存跨关卡全局数据 |
这张表你直接抄走用就行。每次不确定数据该放哪时,对着这个表看一眼,基本不会错。
5.7 联机项目架构清单
最后分享一个我在联机项目里使用的架构检查清单,供你搭建项目时参考:
- 跨关卡不做同步的数据放GameInstance
- 跨关卡需要同步的数据放PlayerState或SaveGame
- 单局规则放GameMode
- 单局状态广播放GameState
- 玩家个体局内数据放PlayerState
- 局内实时表现数据放Actor组件并通过复制同步
- 外接设备配置放GameInstance或SaveGame
- 外接设备实时输入在客户端处理,需要同步时走RPC
- 物理模拟效果放Pawn组件,关键事件通过RPC/复制广播
- 物理查询判定放在服务器权威层,保证公平性和一致性
这套清单帮我避掉了大量联机同步问题,尤其是外接设备的跨关卡映射配置、查询和物理模拟的职责分离这两块,按这个方式划分后,项目结构清晰很多,排查问题时也快得多。
我在实际项目里见过太多人把GameInstance当存储箱、把GameMode当万能工具类,最后项目越做越乱。其实引擎把这些类设计出来,边界是很清晰的,你只要尊重它的边界,它就能把你照顾得很好。反过来说,你随意越界使用,就会得到一堆莫名其妙的问题。GameInstance和GameMode的配合没什么高深玄机,想清楚“数据归谁管、规则归谁定、状态归谁传”这三件事,你的游戏架构就已经赢了一大半。