尧图精选

UE4网络同步五大核心类:边界、生命周期与复制

🕒 发布时间:2026/10/2 15:45:37 📁 来源:尧图网络
刚接触 UE4 网络同步那会儿我在一个 PlayerController 里写了GetWorld()-GetAuthGameMode()单机 PIE 里跑得好好的打包成专用服务器、连上两个客户端之后其中一个客户端的日志里直接蹦出空指针警告紧接着就是一连串读访问冲突。排查了一下午才想明白GameInstance、GameMode、GameState、PlayerState、PlayerController 这五个东西不是五个可以随便互相 Get 的全局容器它们的可见性被引擎的复制规则、生命周期和 Owner 链条严格划定了边界。谁在哪一端存在、谁在什么时刻出生、谁的数据会同步给别人这些问题的答案都是硬性的不因为你的代码写得漂亮就网开一面。这篇内容我打算把这五者的关系从三个维度拆开讲一是职权边界也就是每个类到底该放什么数据二是生命周期从引擎启动到关卡切换它们的出生与死亡顺序三是复制视角也就是服务器和客户端看到的世界有多不一样。中间会穿插我自己踩过的坑、排错的完整链路以及数据归属的判断方法。适合已经能写基本 Actor 逻辑、但一碰网络同步就犯迷糊的开发者也适合想重新梳理一遍架构的老手——很多玄学 Bug其实都是这层关系没理清。1. 五个类的职权边界先搞清谁管规则、谁管状态、谁管个人在 UE4 里这五个类经常被新手当成五个都能存数据的地方然后随便挑一个塞进去结果单机没问题、联机全崩。核心原因是它们的设计出发点完全不同有的是引擎层的单例有的是规则裁判有的是状态公告板有的是玩家档案有的是输入与网络通道。先把定位摆正后面所有问题都会顺很多。1.1 GameInstance唯一活得过关卡切换的引擎级单例UGameInstance是这五个类里唯一一个不是 Actor 的它是 UObject由引擎在启动时创建在引擎退出时才销毁。它最大的特点就是跨关卡存活无论你OpenLevel切多少次图、无论中间经历了多少次玩家加入和离开GameInstance 始终是同一个实例。引擎内部还挂在它下面维护着ULocalPlayer列表这也是它和本地玩家系统绑定的证明。正因为这个特性GameInstance 天然适合放与单局比赛无关、但需要贯穿整个程序生命周期的数据玩家的存档句柄、账号登录态、音效与画质的用户设置、跨关卡要传递的选人结果、网络会话的引用等等。我自己项目里就有一个UMyGameInstance里面挂了一个USaveGame指针和一个FPlayerProfile结构体无论玩家从主菜单进大厅、再进战斗地图、再回来这些数据都不会丢。但它的限制同样明显第一它不是 Actor不能复制、不能用 RPC、不会自动同步到其他端。你在监听服务器上往 GameInstance 里写一个值客户端那边的 GameInstance 完全不知道。第二它没有 Tick想做定时逻辑得自己用 TimerManager 或者放到别的地方。第三它的生命周期太长如果你把一局比赛的临时状态比如当前回合数放进去下一局开始时你很容易忘记清理造成上一局的数据串到这一局这种非常隐蔽的 Bug。1.2 GameMode只在服务器存在的规则裁判AGameModeBase以及它的子类AGameMode是这个体系的规则中枢。它决定玩家进来时用哪个 PlayerController 类、默认 Pawn 用什么、GameState 和 PlayerState 用哪个子类、HUD 是什么、玩家死亡后怎么重生、什么时候算一局结束。注意这些全是规则和决策而不是状态。它最关键的一个特性是GameMode 只存在于服务器上单机模式下本地就是权威端所以 PIE 里你能拿到它。它默认不复制客户端上根本没有 GameMode 实例你写GetWorld()-GetAuthGameMode()在客户端返回的一定是 nullptr。这个设计是合理的——规则只需要一个人说了算没必要让每个客户端都知道裁判的笔记本上写了什么。所以 GameMode 里适合放的是不需要客户端知道的判定逻辑、服务器独占的作弊校验、刷怪与关卡流程控制、伤害计算的最终裁决。反过来任何客户端 UI 需要显示的数据都不应该只放在 GameMode 里否则客户端要么读不到要么得靠 RPC 一个个推过去费力不讨好。1.3 GameState全场共享的公告板AGameStateBase是 GameMode 的对外发言人。它在服务器上被 GameMode 创建大体是在InitGame阶段具体时机随引擎版本略有差异并且会复制到所有客户端。它的职责是承载所有人都应该知道的全局状态比赛阶段MatchState比如 WaitingToStart、InProgress、GameOver、已经过去的时间ElapsedTime、双方队伍比分、以及最常用的PlayerArray——一个包含场上所有 PlayerState 的数组。我一般把 GameState 理解成一块挂在广场中央的公告板谁都能看内容由服务器统一更新客户端只能读不能改。想改数据得走服务器侧的 GameMode 或者对 GameState 发起 Server RPC前提是 Owner 链条允许这个后面细说。AGameMode相比AGameModeBase多出来的那套 MatchState 流程就是 Matinee/回合制玩法最常用的骨架。你要是做的是一个有明确开始—进行—结算三段式的玩法直接用AGameMode会省不少事。1.4 PlayerState每个人的档案卡APlayerState是一个玩家的公开档案服务器上每个玩家一份并且会复制给所有客户端。注意这里是所有客户端——A 玩家能通过 GameState 的 PlayerArray 拿到 B 玩家的 PlayerState看到他的名字、分数、队伍 ID。这正是它存在的意义让每个人都能知道别人是谁、状态如何。典型放进去的字段有PlayerName玩家昵称、Score个人得分、TeamID、UniqueId唯一标识做断线重连认人很有用以及你自己扩展的 K/D、击杀数、职业选择、准备状态等等。Ping这类只跟本人相关的数据通常用COND_OwnerOnly条件复制省带宽也避免误导。一个容易忽略的点PlayerState 的 Owner 是 PlayerController。这个 Owner 关系决定了它上面的 Server RPC 能否被正确路由后面第 3 节会展开。另外PlayerState 是会随关卡切换而重建的除非你走无缝切换SeamlessTravel——很多人把跨局保留的等级/经验放这里然后奇怪为什么切图之后全没了就是踩了这个坑。1.5 PlayerController玩家的意志与外网通道APlayerController是这五个类里唯一直接对应一个真实玩家的一条连接的对象。它负责把输入变成动作InputComponent、按键绑定、外接设备映射的落点管理 Camera 与视角并且承担唯一可靠的客户端到服务器的 RPC 通道。玩家在客户端上按下一个键、点一下 UI 按钮最终要影响服务器几乎都要经过 PlayerController 发起 Server RPC。它还有一个非常硬性的特性PlayerController 默认是bOnlyRelevantToOwner只复制给拥有它的那个客户端。也就是说A 客户端的机器上根本不存在 B 玩家的 PlayerController 对象。你想在 A 那边获取 B 玩家按了什么键是拿不到的——那种信息必须经过 PlayerState 这类公共载体或者由服务器转发 NetMulticast RPC。所以我的经验是任何只有本人需要知道的东西放 PlayerController任何别人也需要知道的东西放 PlayerState任何全场需要知道的东西放 GameState。这条线划清楚了后面复制和 RPC 的问题能少掉一大半。2. 生命周期时间轴从引擎启动到关卡切换谁先出生谁先死理解了职权边界还得知道时间顺序。很多 Bug 不是数据放错了类而是你在它还没出生的时候就去拿它——比如在 PlayerController 的构造函数里访问 GameState或者在 GameInstance 的Init里访问 PlayerController这些时序上根本不可能成立。2.1 引擎启动到地图加载完成GameInstance 先落地整个链条的起点是引擎启动。GameInstance 在这一步被创建Init()被调用此时它手里还没有 World更没有玩家。接着引擎加载默认地图UWorld建立然后根据 World Settings 里指定的 GameMode 类去 Spawn GameMode 实例——这里要注意GameMode 是跟着地图走的换一张地图就会换一个 GameMode 实例除非是无缝切换且 GameMode 类相同引擎倾向复用。GameMode 出生后依次经过InitGame、PreInitializeComponents、StartPlay等阶段其中在InitGame附近会创建 GameState。所以正常流程下GameState 一定比任何玩家都早存在。GameMode 的StartPlay之后比赛进入正式运行状态。这里有个实操习惯我强烈建议在 GameInstance 的Init里别碰任何 World 相关的东西。我见过有人在Init里写GetWorld()-SpawnActor(...)直接崩在启动阶段因为那一刻 GameInstance 还没被赋予 Outer World。要初始化跨关卡的数据用Init里只做纯数据准备需要 World 的逻辑推迟到第一次进入关卡时做。2.2 一个玩家进服的四步Login、InitPlayerState、PostLogin、RestartPlayer玩家连接进来之后服务器侧的流程大致是这样四步每一步该做什么事是有讲究的LoginGameMode 被调用Login在这里决定是否允许加入并 Spawn 出一个 PlayerController。这一步是分配座位。PlayerState 初始化PlayerController 会创建自己的 PlayerState 并建立 Owner 关系。这一步是发档案卡。PostLoginPlayerState 就绪之后GameMode 的PostLogin被调用。这是做初始化最常用、最安全的钩子——比如设置初始分数、把玩家加入队伍、广播有新玩家加入。因为此时 PlayerController 和 PlayerState 都已经有效访问它们不会拿到空指针。RestartPlayer最后 GameMode 通过RestartPlayer给玩家生成 Pawn 并 Possess玩家这才真正出现在场上。顺序上的关键结论是在 PostLogin 里PC 和 PS 都有了但 Pawn 可能还没有。很多新手在PostLogin里写GetPawn()然后直接用结果拿到 nullptr。需要操作 Pawn 的逻辑要么放在RestartPlayer之后要么监听APlayerController::OnPossess或者 Pawn 的BeginPlay。2.3 关卡切换的生死名单谁被销毁、谁被带走关卡切换是这五个类命运分歧最大的时刻用一张表最直观对象普通切换OpenLevel无缝切换SeamlessTravelGameInstance保留保留GameMode销毁重建通常复用同类时GameState销毁重建重建并转移部分数据PlayerState销毁重建默认保留PlayerController销毁重建默认保留Pawn销毁重建销毁重建普通切换时除了 GameInstance其他全部重来一遍。这就是为什么跨关卡的等级、背包、进度一定要放 GameInstance不能放 PlayerState。而如果你做的是房间制玩法一局一局之间不想让玩家重新加入重新握手、重新加载资源那就该走无缝切换让 PlayerController 和 PlayerState 被带到下一张图里。无缝切换有两个前置条件服务器用ServerTravel加?game之类的选项触发并且你要在GetSeamlessTravelActorList里明确指出需要额外保留的 Actor。踩坑点在于Pawn 一定不会保留所以别把重要数据挂在 Pawn 上以及无缝切换时PostLogin不会被再次调用玩家重新回到场景是靠HandleSeamlessTravelPlayer如果你把初始化逻辑全写在PostLogin里切图之后玩家就会处于半初始化状态——这是我见过最多的一类无缝切换 Bug。3. 服务器与客户端视角差异为什么 GameMode 在客户端永远是空如果让我只挑一个知识点推荐给所有刚做联机的人那就是这一节。理解不同端看到的对象集合不一样能省掉你至少一半的排错时间。3.1 一张复制规则表胜过十次试错类服务器拥有它的客户端其他客户端GameInstance有有各自独立有各自独立GameMode有无无GameState有有有PlayerState有有有PlayerController有有无这张表里最容易被忽视的是最后一行。A 客户端上只能拿到自己的 PlayerController拿不到 B 的。做观战系统、做显示队友正在看的方向这类需求时如果你第一反应是遍历所有 PlayerController在客户端一定跑不通——正确做法是把需要公开的信息放到 PlayerState 的自定义字段里复制出去或者由服务器主动发 NetMulticast RPC。GameInstance 那一行也值得说一句三个端都有但它们是三个互不相干的对象。服务器 GameInstance 里的变量改了客户端 GameInstance 里的同名变量不会被改。这一点和 GameState 形成鲜明对比把 GameInstance 当同步容器用是最常见的误解之一。3.2 Owner 链条决定了 RPC 能往哪儿发RPC 不是随便哪个 Actor 都能发的。规则是Server RPC只有由本地玩家拥有的、且本地有控制权的 Actor才能调用成功。PlayerController 天然满足它 Possess 的 Pawn、以及 Owner 链条最终指回该 PC 的 Actor比如 PlayerState、PlayerController 创建的 HUD也满足。Client RPC只能发到该 Actor 的 Owner 客户端。你要让所有客户端同时播一个特效就用 NetMulticast而不是循环发 Client RPC。NetMulticast RPC只要这个 Actor 被复制到了某个客户端那个客户端就能收到。所以 GameState 上挂 NetMulticast 广播是很常见的做法——比如比赛开始这种全场事件。这里有个很多人不知道的细节PlayerState 上也能写 Server RPC。因为 PlayerState 的 Owner 就是对应的 PlayerControllerOwner 链条是通的。有些需求比如玩家点准备按钮写在 PlayerState 上反而更自然因为它同时是复制给所有人的服务端改个 bool 就能让所有人看到准备状态变化。3.3 一次空指针排查的完整链路回到开头那个空指针。当时的场景是一个 UI 面板在NativeConstruct里去GetWorld()-GetAuthGameMode()取当前回合数。单机没问题联机时客户端炸。排查链路是这样的第一步看崩的是哪一端——日志里的NetMode显示是客户端第二步确认GetAuthGameMode()的实现它在非服务器端直接返回 nullptr这是设计如此不是 Bug第三步看这个数据本来该从哪儿来——回合数属于全场共享的全局状态归 GameState 管第四步检查 GameState 里有没有这个字段发现确实没有只在 GameMode 里存了一份第五步把字段挪到 GameState服务器端由 GameMode 更新它因为 GameState 的成员变量在服务器上可以直接写客户端用GetWorld()-GetGameStateT()读取。改完之后还要注意一件事如果这个字段是运行时动态变化的必须在GetLifetimeReplicatedProps里声明复制并且最好用ReplicatedUsing绑一个 OnRep 回调去刷新 UI否则客户端可能拿到旧值。这个坑我在第 6 节还会展开。4. 数据归属判断一个新需求来了字段到底该挂在哪这是日常开发里最高频的决策。我的做法是连续问自己三个问题基本能在半分钟内定位。4.1 三个自问可见范围、存活周期、是否要权威第一个问题这个数据谁需要看到只有本人 → PlayerController所有人都要看到 → PlayerState每人的或 GameState全局的只有服务器需要 → GameMode。第二个问题这个数据要活多久只活一局 → GameState / PlayerState要跨关卡活 → GameInstance只活一次交互比如一次按键、一次点击→ 直接 RPC 传参别存。第三个问题这个数据需要服务器权威吗涉及分数、伤害、胜负、道具数量这类会被玩家想办法修改的必须放服务器权威的地方GameMode 计算、GameState / PlayerState 复制出去。纯表现的、不影响胜负的比如本地摄像机抖动可以放客户端本地不必复制。这三个问题串起来你会发现绝大多数归类困难的需求都能被拆开比如击杀提示其实拆成了击杀计数PlayerState权威和屏幕上的飘字客户端本地表现两部分硬塞进一个类里反而别扭。4.2 常见字段的落位对照表数据推荐位置理由玩家昵称、头像 IDPlayerState所有人都要看到个人得分、K/DPlayerState权威计算全员可见队伍总分GameState全局唯一全员可见比赛阶段、剩余时间GameState全员需要同步的时钟当前回合数GameState全员可见的全局状态玩家等级、背包、存档GameInstance跨关卡存活用户音量、画质设置GameInstance程序级配置按键绑定、输入映射PlayerController GameInstance 存配置输入落点配置要持久化本局是否按下冲刺瞬时PlayerController只有本人和服务器关心观战目标索引PlayerState其他人也可能需要渲染表里最后一条我要多说一句观战目标看起来是本地选择但如果服务器要把你观战的对象同步给导播系统或者其他观战者那就变成公开信息了放 PlayerState 更合适。判断依据永远是可见范围而不是我第一反应觉得它属于谁。4.3 状态查询和物理模拟不是一回事这一点在热词里也有人问UE4 里查询状态和物理模拟的区别在网络同步语境下非常关键。GameState、PlayerState 里放的数据是逻辑状态它们是布尔值、整数、枚举、结构体由服务器权威修改后原样复制。客户端拿到的是服务器告诉我现在是这样而不是自己去推算。这类数据的特征是可枚举、可比较、可以在 UI 上直接显示。而物理模拟载具、布娃娃、可推动的箱子的结果是连续浮点数由各端各自的物理引擎独立演算。你不可能把每一帧的位置和速度全量复制过去——带宽受不了。所以 UE4 的做法是服务器权威模拟、客户端预测、必要时做平滑校正。这意味着不要把物理模拟的中间结果塞进 PlayerState 或 GameState 当状态用因为那些数值在客户端和服务器上本来就不完全一致你拿来做判定逻辑会得到两边算不一样的答案这种诡异现象。我实际项目里的做法是物理相关的判定全部在服务器做比如这个箱子砸到玩家了没把结论性的逻辑状态扣了多少血、是否被击倒写进 PlayerState 复制出去客户端只负责根据这个结论播表现。这条分界线划清楚联机手感会稳定很多。5. 获取与 Cast 的正确姿势C 和蓝图里的常用写法知道了谁在哪还得知道怎么拿到它。这一段我按使用频率列一遍包括那些容易看起来能拿到其实拿不到的写法。5.1 C 侧的取用方式在大多数AActor或UActorComponent里标准写法是// GameInstance任何有 World 的地方都能拿 UMyGameInstance* GI GetWorld()-GetGameInstanceUMyGameInstance(); // GameMode只有服务器有效客户端返回 nullptr AGameModeBase* GM GetWorld()-GetAuthGameMode(); AMyGameMode* MyGM GetWorld()-GetAuthGameModeAMyGameMode(); // GameState所有端都能拿 AMyGameState* GS GetWorld()-GetGameStateAMyGameState(); // PlayerController拿本地第一个玩家 APlayerController* PC GetWorld()-GetFirstPlayerController(); // PlayerState从 PC 或 Pawn 反查 APlayerState* PS PC ? PC-PlayerState : nullptr;几个容易被忽略的细节。第一GetAuthGameMode()和GetGameMode()有区别。前者只在权威端返回实例后者在某些版本/上下文中可能返回一个 CDO 或者空不要混用。养成一律用GetAuthGameMode()的习惯出问题的时候至少能立刻意识到哦我在客户端。第二GetGameInstanceT()里的模板参数必须和 Project Settings 里配置的 GameInstance 类一致否则 Cast 失败返回空。这地方配错了不太会报错只会静默返回 nullptr很难查。第三在 PlayerController 内部可以直接用PlayerState成员Pawn 内部可以用GetPlayerState()、GetController()这些走的是引擎已经建立好的引用比全局查找更快也更安全。5.2 蓝图与 UMG 里最容易拿空的三个节点蓝图里对应的节点是Get Game Instance、Get Game Mode、Get Game State、Get Player Controller、Get Player State。坑主要集中在三个地方。第一个坑Get Game Mode在客户端返回空。这几乎是每个新手都会踩一次的。解决办法有两个要么改用Get Game State要么用IsStandalone/HasAuthority判断后再走不同分支。第二个坑UMG 控件里没有直接的 World 上下文时拿不到东西。控件的Get World在Construct之前可能是空的稳妥做法是在NativeConstruct/Event Construct之后再去获取并且加空判断。第三个坑Get Player Controller的 Index 参数。默认 0 号在单机和对战里通常没问题但如果你做的是本地分屏0 号只代表第一个玩家另一个玩家的 UI 必须取 1 号。另外在纯服务器上Dedicated Server这个节点返回也是不可靠的因为服务器上没有本地玩家——服务器上的玩家对象是 PlayerController但不是本地玩家意义上的 FirstPlayerController。涉及服务器逻辑时遍历PlayerArray比拿 0 号更靠谱。5.3 从一个深层子对象反查 PC 的完整链路实战里经常遇到这种情形某个组件、某个 Actor 被放在很深的层级里它手里只有一个AActor*需要向上反查属于哪个玩家。我的标准链路是这样的// 从任意 Actor 出发先找 Controller AController* Controller nullptr; if (APawn* AsPawn CastAPawn(Actor)) { Controller AsPawn-GetController(); } if (!Controller) { Controller Actor-GetInstigatorController(); } // 拿到 PlayerController再拿 PlayerState APlayerController* PC CastAPlayerController(Controller); APlayerState* PS PC ? PC-PlayerState : nullptr;这个链路里GetInstigatorController()是个宝藏函数尤其在投射物、特效、伤害事件里非常有用——它能帮你一路追回到谁干的。我在做伤害统计系统的时候就是靠它把伤害归属到具体玩家的 PlayerState 上的比在每个地方手动传 PC 引用省事得多。如果连 Instigator 都没有比如环境伤害那就该落到 GameState 的玩家数组里去做额外判定或者干脆记到一个世界规则的数据结构里。这时候不要硬找一个 PC 塞进去会让代码变得很难维护。6. 踩坑实录复制、重生、无缝切换里的典型故障理论讲完来点真实发生过的事。这一节里每个坑我都尽量把排查链路写出来方便你在遇到类似现象时对号入座。6.1 自定义字段没声明复制客户端一直显示默认值现象我在 PlayerState 里加了一个KillCount服务器上打完人立刻能看到数字涨客户端 UI 死活显示 0。第一反应是 UI 绑定错了检查了半天绑定没问题。正确排查顺序是第一步确认字段在服务器上确实变了打日志变了第二步确认这个 PlayerState 复制到了客户端PlayerArray 能看到说明复制了第三步才去看GetLifetimeReplicatedProps——果然新增字段没有加DOREPLIFETIME所以整个 PlayerState 复制过去了但这个字段没有单独声明。这里有个非常重要的细节Actor 被复制和这个 Actor 的某个属性被复制是两回事。只有写了DOREPLIFETIME或者DOREPLIFETIME_CONDITION的属性才会进复制队列。新手常见的误解是我的类继承了 PlayerState里面所有变量应该自动同步吧——不会的必须显式声明。另外如果这个字段是运行时频繁变化的记得用ReplicatedUsing OnRep_Xxx绑定回调去刷新 UI光靠蓝图里的绑定事件在属性没触发再复制的时候可能不刷新。6.2 玩家重生之后数据被清空现象玩家死亡重生之前积累的击杀数没了而队伍总分还在。这说明 GameState 没事问题出在 PlayerState。排查后发现两个可能原因。第一种自己在死亡流程里手动调用了Destroy或者重置 PlayerState。第二种更隐蔽你把数据存在 Pawn 上重生时 Pawn 被销毁重建了。Pawn 是每次重生都会换一个实例的所有挂在 Pawn 上的自定义数据都会归零这完全符合引擎行为。解决办法很直接凡是跟人绑定而不是跟身体绑定的数据一律搬到 PlayerState。这也是为什么装备、技能等级、分数这些字段在成熟的工程里几乎都长在 PlayerState 上。我自己后来养成了一个习惯在 Pawn 上只放身体相关的东西——血条显示、动画状态、当前武器挂点其他全放 PlayerState。6.3 无缝切换下 PostLogin 不会再被调用这个坑最折磨人因为它在编辑器里测试时可能根本复现不了。现象是第一张地图一切正常ServerTravel到第二张地图之后玩家虽然还在场景里但技能栏空了、队伍信息没了而且服务器日志里没有PostLogin的记录。原因是无缝切换走的是HandleSeamlessTravelPlayer不是PostLogin。你的初始化逻辑如果只写在PostLogin里第二张图就不会执行。修法是把公共初始化抽成一个独立函数PostLogin和HandleSeamlessTravelPlayer都调用它。同时别忘了检查GetSeamlessTravelActorList把需要额外保留的自定义 Actor比如队伍管理器、观众席逻辑显式加进去。PlayerController 和 PlayerState 默认会被引擎带过图但自定义的东西不会。这个默认名单和自定义名单的差别我当年是靠翻源码确认的文档里写得比较含糊。6.4 外接设备与输入映射最终落在谁身上有朋友问过外接设备游戏手柄、飞行摇杆、赛车踏板、自定义按键盒在 UE4 里到底映射到哪个类。答案是输入这一层最终都落到 PlayerController 上——输入映射表把物理设备的按键/轴映射成 Action 和 AxisPlayerController 的 InputComponent 接收这些事件再决定是否转发给当前 Possess 的 Pawn。这条链路有几个实践上的注意点。第一输入只在本地玩家所在的端有意义。服务器上没有本地设备输入它收到的永远是客户端发来的 RPC。所以别在服务器上写读按键值的逻辑。第二外接设备的轴值比如方向盘转角通常是带浮点的连续值从客户端往服务器发的频率要自己控制不然带宽很浪费——常见做法是本地按 60Hz 采样但只在值变化超过阈值时才发。第三多设备同时接入时PlayerController上可以通过设置InputComponent的优先级或者用自定义的输入预处理来区分是谁在操作做双人同屏时这两个模式配合很关键。我自己的项目里做飞行模拟的时候摇杆的曲线、死区这些参数是存在 GameInstance 里的因为要持久化到玩家配置运行时的输入状态挂在 PlayerController 上而当前飞机的操纵面偏转这种结果走物理和复制。三个类各管一段职责清楚调试的时候一眼就能定位问题出在哪一层。最后分享一个我个人的小习惯当我不确定某个数据该放哪儿的时候我会先画一张表横轴是服务器 / 拥有者客户端 / 其他客户端纵轴是具体的字段然后开始填空。填不出来的地方往往就是设计没说清楚的地方比写代码的时候才纠结要高效得多。这个五个类的关系说到底就是可见性、生命周期、权威性三个维度交叉出来的结果把这三条线在心里理直了绝大多数网络同步的疑难杂症都会变成可解释、可预期的现象。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →