Gameplay Framework 与端存在性矩阵
多人游戏里最容易踩的坑:在客户端访问
GameMode拿到nullptr、把分数存PlayerController结果别的玩家看不到。根源是——UE 的每个框架类只存在于特定的端。搞清「谁在服务器、谁在所有客户端、谁只在本地」,一半的网络 bug 都能提前避开。
一句话结论
GameMode 只在服务器,PlayerController 只复制给 owner,PlayerState/GameState 复制给所有人——存数据前先问「这个类在哪端存在」。
场景问题
打个比方:一场线上考试。监考老师(GameMode)只在考场服务器那头,规则由他定,考生(客户端)根本见不到他本人;每个考生的答题器(PlayerController)只发到自己手上,你看不到别人的答题器;成绩公示栏(PlayerState)贴在墙上所有人都能看;考场大屏(GameState)显示全场进度也是人人可见;而你戴的耳机(HUD/UI)只有你自己有。存东西前先想清楚「这玩意儿贴哪」——把成绩写进自己的答题器(PlayerController),别人当然看不见;得贴到公示栏(PlayerState)上才行。
面试高频题:「玩家名字、分数应该存在哪个类?」——答 PlayerState。为什么不是 PlayerController?因为 PC 只复制给它自己的客户端,别的玩家拿不到。这类问题的通用解法就是背下端存在性矩阵。
实现方案
1. 核心框架类
| 类 | 职责 |
|---|---|
AGameModeBase / AGameMode | 规则、胜负、玩家进出、生成 Pawn。权威逻辑的家 |
AGameStateBase / AGameState | 全局比赛状态(比分、阶段、倒计时),给所有人看 |
APlayerController | 一个玩家的「大脑」,处理输入、拥有 Pawn,客户端与服务器的连接代表 |
APlayerState | 一个玩家的可公开数据(名字、分数、Ping、队伍) |
APawn / ACharacter | 玩家控制的「身体」,Character 带 CMC 移动组件 |
AHUD / UUserWidget | 屏幕 UI,纯表现 |
UGameInstance | 跨关卡存活的进程级容器 |
2. 端存在性矩阵(核心)
| 类 | 服务器 | 所有客户端 | 仅本地客户端 | 存什么 |
|---|---|---|---|---|
GameMode | ✅ | ❌ | ❌ | 规则、权威判定(客户端拿到 nullptr) |
GameState | ✅ | ✅ 复制 | — | 全场共享状态:比分、阶段 |
PlayerController | ✅(每玩家一个) | 只有自己那个 | ✅ | 输入、拥有关系;每客户端只看到自己的 PC |
PlayerState | ✅(每玩家一个) | ✅ 全部复制 | — | 玩家公开数据:名字、分数、队伍 |
Pawn/Character | ✅ | ✅ 相关的复制 | ✅ 自己的 | 角色实体、位置、动作 |
HUD/Widget | ❌(DS 无渲染) | — | ✅ | 纯本地 UI |
GameInstance | ✅ | ✅ 各端一个 | ✅ | 各端独立、不复制、跨关卡 |
三个最常考的结论
GameMode只在服务器。客户端GetGameMode()返回nullptr。想让客户端知道的规则/状态,放GameState。PlayerController只复制给拥有它的客户端。每个客户端只能看到「自己的」PC,看不到别人的。所以别人也要看到的数据不能放这。PlayerState复制给所有客户端。名字、分数、队伍这类「所有玩家都要看到的玩家数据」放这里。
3. Role:一个 Actor 在不同端的身份
同一个 Actor(比如你的角色)在不同端有不同「身份」,由两个字段描述:LocalRole(在本机的身份)与 RemoteRole(在对端的身份)。
enum ENetRole {
ROLE_None, // 无网络
ROLE_SimulatedProxy, // 模拟:这是「别人的」角色,靠复制+插值表现
ROLE_AutonomousProxy, // 自主:这是「我控制的」角色,可本地预测
ROLE_Authority, // 权威:这一份是说了算的(通常在服务器)
};
三种视角看同一个玩家角色:
| 在谁的机器上 | LocalRole | 表现方式 |
|---|---|---|
| 服务器 | ROLE_Authority | 权威计算,最终裁定 |
| 操控它的客户端 | ROLE_AutonomousProxy | 本地预测(立即响应输入),见 移动预测 |
| 其他客户端 | ROLE_SimulatedProxy | 只收复制来的状态,做插值平滑 |
判断用法:
if (HasAuthority()) // == GetLocalRole() == ROLE_Authority,服务器权威分支
Health -= Damage; // 只有服务器扣血
if (IsLocallyControlled()) // 本地玩家操控的
ShowAimUI(); // 只给本人显示准星
HasAuthority() 的正确姿势:任何「改变游戏状态」的代码都要包在 HasAuthority() 里,只让服务器执行,靠复制同步到客户端。客户端只做预测和表现,最终以服务器为准。这是 UE 网络的服务器权威原则,详见 服务器权威与反作弊。
一句话记住三种 Role 的差别:同一个角色,三台机器上是三种身份。
| Role | 一句话 | 类比 |
|---|---|---|
ROLE_Authority | 这一份说了算,别人都得听它 | 裁判手里的正式记分册 |
ROLE_AutonomousProxy | 我操控的那个,可以先动再对账 | 你自己的记事本:先记下来,回头跟裁判核对 |
ROLE_SimulatedProxy | 别人操控的,我只照收来的状态画 | 场边看板:只显示裁判播报的结果,还得补插值让它别一格一格跳 |
易错点:AutonomousProxy 和 SimulatedProxy 都在客户端、都不是权威,区别只在「是不是我操控的」——是我的才允许本地预测,别人的只能插值。而 Authority 通常在服务器,但 Listen Server 上房主自己的角色是 Authority(不需要预测,因为它就是权威),这也是「Listen Server 上房主手感天然更好」的原因。
4. 所有权(Ownership)链
RPC 和 COND_OwnerOnly 复制都依赖「所有权」:PlayerController 拥有它的 Pawn(Pawn->GetOwner() 链最终到 PC,PC 又对应一个 NetConnection)。判断「这个客户端能不能对这个 Actor 发 Server RPC」,就是看这条 owner 链是否连到它的连接。详见 RPC。
为什么这么做
按「可见范围」分类存储,是为了让复制最省、最安全:
- 全场共享的(比分)放
GameState,一份复制给所有人。 - 单个玩家公开的(名字)放
PlayerState,也复制给所有人(别人要看到你名字)。 - 单个玩家私有的(背包明细、输入)放
PlayerController,只复制给本人,省带宽也防作弊(别人拿不到你的私有数据)。 - 权威规则放
GameMode且不下发,客户端根本碰不到,天然防篡改。
为什么别的选择不行
- 全塞进一个 Actor 复制给所有人:带宽浪费(私有数据也全网广播)+ 安全隐患(作弊者能读到本不该看到的数据)。
- 把分数存
PlayerController:别的客户端看不到(PC 只复制给 owner),做不了记分板。经典错误。 - 在客户端访问
GameMode写规则:拿到nullptr崩溃;即使能拿到,客户端权威 = 作弊天堂。规则必须服务器权威。
沉淀结论
存数据前先查端存在性矩阵:GameMode 只在服务器、PlayerController 只给 owner、PlayerState/GameState 给所有人。改状态用 HasAuthority() 锁到服务器,客户端只预测和表现。
记忆口诀
端存在:GameMode 仅服务器 / GameState 全体复制 / PlayerController 只给自己 / PlayerState 全体可见 / HUD 仅本地
存数据:全场状态→GameState / 玩家公开→PlayerState / 玩家私有→PlayerController
Role:Authority 权威(服务器)/ AutonomousProxy 我操控(预测)/ SimulatedProxy 别人(插值)
铁律:改状态包 HasAuthority / 客户端只预测表现 / 服务器最终裁定
内容来源
综合整理。主要参考:Epic 官方文档 Gameplay Framework、Actor Role and RemoteRole、Networking Overview、UE 源码 GameMode.cpp/PlayerController.cpp、社区分析 Cedric Neukirchen《Multiplayer Network Compendium》。
相关专题:Role 三态如何驱动预测与插值见 移动预测;所有权如何约束 RPC 见 RPC;服务器权威原则见 反作弊;局外后台与局内的整体差异见 游戏 vs 互联网后台。
自测:合上资料能说清楚吗?
玩家名字和分数应该存在哪个类?为什么不是 PlayerController?
参考答案
存 PlayerState。因为 PlayerState 复制给所有客户端,别的玩家能看到你的名字分数,才能做记分板。而 PlayerController 只复制给拥有它的那个客户端,别人拿不到——存 PC 里做不了记分板。
客户端调用 GetGameMode() 会返回什么?想让客户端知道规则/状态怎么办?
参考答案
返回 nullptr——GameMode 只存在于服务器。要让客户端知道的全局状态(比分、阶段、倒计时),放 GameState(复制给所有客户端);权威规则本身留在服务器不下发。
ROLE_Authority / AutonomousProxy / SimulatedProxy 三者分别对应什么?
参考答案
同一个角色 Actor 在不同端的身份:服务器上是 ROLE_Authority(权威,说了算);操控它的客户端上是 ROLE_AutonomousProxy(自主,本地预测立即响应输入);其他客户端上是 ROLE_SimulatedProxy(模拟,只收复制状态做插值平滑)。
为什么改游戏状态的代码要包在 HasAuthority() 里?
参考答案
服务器权威原则:只有服务器有权改变游戏状态(扣血、扣钱、判胜负),改完通过复制同步到客户端。客户端只做预测和表现。若客户端也能直接改状态,就是作弊天堂。HasAuthority()(等价 GetLocalRole()==ROLE_Authority)确保这些代码只在服务器执行。