2418 字
12 分钟
UE Gameplay 框架——谁在管理这个游戏

一、一张图看清六个人的关系#

在你 mini-game 的服务端代码里,这几个类就像不同部门的负责人:

服务端(权威) 客户端
┌─────────────────┐ ┌─────────────────┐
│ GameMode │ │ │
│ "规则制定者" │ │ PlayerController│
│ 只存在于服务端 │ │ "玩家的遥控器" │
├─────────────────┤ ├─────────────────┤
│ GameState │──复制到全部→│ GameState │
│ "比赛记分板" │ 客户端 │ (只读副本) │
├─────────────────┤ ├─────────────────┤
│ PlayerState │──复制到所属→│ PlayerState │
│ "个人记分卡" │ 客户端 │ (只读副本) │
├─────────────────┤ ├─────────────────┤
│ Pawn │──复制到全部→│ Pawn │
│ "玩家的肉身" │ 客户端 │ (受控副本) │
└─────────────────┘ └─────────────────┘

核心原则:每个类有自己的职责边界。搞混了边界,代码就会越来越难维护。


二、六个核心类的职责#

1 GameMode ——“比赛规则”#

只存在于服务端。定义了这局游戏的玩法规则。

UCLASS()
class AGameMode : public AInfo {
// 核心职责
void InitGame(...); // 一局开始时调用
void HandleMatchHasStarted();// 比赛正式开始
void HandleMatchHasEnded(); // 比赛结束
void RestartPlayer(...); // 玩家重生/重新加入
AActor* ChoosePlayerStart(...); // 决定在哪 Spawn
// 用哪套类(通过配置指定,不改代码)
TSubclassOf<AGameState> GameStateClass;
TSubclassOf<APlayerController> PlayerControllerClass;
TSubclassOf<APawn> DefaultPawnClass;
TSubclassOf<APlayerState> PlayerStateClass;
};

GameMode 是”今天玩什么游戏”——换一个 GameMode,换一套规则。CTF(夺旗)和 DeathMatch(死斗)的 GameMode 完全不同。

在一个合作副本里,GameMode 应该管的事情:

  • 这局有几个阶段(准备、探索、战斗、结算)
  • 每个阶段的时间限制
  • 判定任务是否完成的条件
  • 什么时候开始战斗、什么时候发放奖励

你容易犯的错:把太多东西塞进 GameMode。如果 GameMode 里出现了”处理开箱动画”的代码,就是放错了——那是 Pawn 或 PlayerController 该管的。

2 GameState ——“记分板”#

服务端 + 复制到所有客户端。存储所有人都需要知道的公共状态。

UCLASS()
class AGameState : public AInfo {
UPROPERTY(Replicated)
TArray<APlayerState*> PlayerArray; // 所有玩家
UPROPERTY(Replicated)
float MatchStartTime; // 比赛开始时间
UPROPERTY(Replicated)
float RemainingTime; // 剩余时间
UPROPERTY(Replicated)
bool bIsCombatPhase; // 当前是否是战斗阶段
};

判断一个数据该不该进 GameState 的标准所有人都需要知道它吗?

在一个合作副本里:

  • ✅ 当前任务阶段、剩余时间 → GameState
  • ✅ 当前副本目标和完成进度 → GameState
  • ✅ 任务结算结果(公开信息) → GameState
  • ❌ 某个玩家的背包内容(私有信息) → 不出现在 GameState,服务端只发给对应玩家

3 PlayerState ——“个人记分卡”#

服务端 + 复制到该玩家客户端。存储单个玩家的”账号级”状态。

UCLASS()
class APlayerState : public AInfo {
UPROPERTY(Replicated)
FString PlayerName; // 玩家名
UPROPERTY(Replicated)
int32 Score; // 得分/总资产
UPROPERTY(Replicated)
int32 Ping; // 延迟
UPROPERTY(Replicated)
bool bIsSpectator; // 是否观战
};

在一个合作副本里:

  • ✅ 玩家等级、经验 → PlayerState
  • ✅ 玩家是否在线(掉线状态) → PlayerState
  • ✅ 玩家角色(使用哪个职业,技能是什么) → PlayerState
  • ❌ 背包里的具体物品列表(自己的背包只有自己需要) → 服务端存,按需发给客户端,不一定放 PlayerState

4 PlayerController ——“玩家的遥控器”#

每个玩家一个。客户端持有一个(可以发送 RPC)、服务端持有一个(权威)。

它是玩家和游戏世界之间的翻译官:

  • 接收输入(按键、鼠标)
  • 翻译为游戏操作(RPC → 服务端执行)
  • 管理相机(ViewTarget)
  • 管理 UI(HUD)
  • 处理网络同步(它是控制通道)
UCLASS()
class APlayerController : public AController {
// 控制的对象
APawn* AcknowledgedPawn;
// 输入处理
virtual void SetupInputComponent();
// 相机
void SetViewTarget(AActor* NewTarget);
// RPC 通道
UFUNCTION(Server, Reliable)
void Server_UseAbility(FGameplayTag AbilityTag);
};

在一个合作副本里,PlayerController 应该管的事:

  • 玩家的 UI 输入(按下技能按钮 → 触发 Server_UseAbility RPC)
  • 相机控制(探索视角、战斗锁定和过场特写)
  • 玩家掉线/重连的通知

5 Pawn / Character ——“玩家的肉身”#

被 PlayerController 控制、在世界上有物理存在的对象

UCLASS()
class APawn : public AActor {
APlayerController* Controller; // 谁控制我
virtual void PossessedBy(AController* NewController); // 被控制时
virtual void UnPossessed(); // 被放弃时
// 移动(如果是 Character——加了 UCharacterMovementComponent 的子类)
virtual void AddMovementInput(FVector Direction, float Scale);
};

在一个纯菜单或策略游戏里,Pawn 可能只是一个空壳(放相机用),或者根本用不到;而在 RPG 或动作游戏里,Pawn 通常承载移动、碰撞和动画。Gameplay 框架是弹性的,不需要 Pawn 的场合就别硬用。

6 PlayerController 和 Pawn 的区别#

经常有新人困惑。一句话:

PlayerController 是”玩家”,Pawn 是”玩家在游戏世界里的身体”。 玩家断线了——PlayerController 还在(等待重连),Pawn 被 Destroy。 玩家换角色——PlayerController 不变,Possess 一个新的 Pawn。


三、初始化顺序:一个客户端加入时发生了什么#

理解这个能帮你 debug 很多”为什么这个时候我拿不到 GameState”的问题。

1. 客户端连接 → 服务端创建 PlayerController
2. PlayerController::BeginPlay (服务端)
3. GameMode::Login → 创建 PlayerState
4. PlayerController::Possess → 创建/控制 Pawn
5. 复制 GameState 到客户端
6. 复制 PlayerState 到客户端
7. 复制 Pawn 到客户端
8. PlayerController::BeginPlay (客户端) ← 此时 GameState 已可用
9. Pawn::BeginPlay (客户端)

常见坑:在 BeginPlay 里用 GetGameState(),在客户端可能拿不到。因为 GameState 的复制可能晚于你的 BeginPlay。安全做法:

void AMyPlayerController::BeginPlay() {
Super::BeginPlay();
if (GetGameState()) {
// 可能为空!GameState 的复制可能还没到达
}
}
// 正确做法:覆盖 ReceivedGameState
void AMyPlayerController::ReceivedGameState() {
Super::ReceivedGameState();
// 这里保证 GameState 已经到了
}

四、用在合作副本里的设计练习#

假设给一个合作副本做一次”类职责分配”练习:

需求该放在哪为什么
阶段计时器、阶段切换GameMode(服务端)规则制定者
当前目标、倒计时显示GameState(Replicated)所有人都要看到
玩家背包内容PlayerController 对应的 OwnerOnly 状态不能让其他客户端知道
玩家等级和经验PlayerState(Replicated)队友也需要看到基础信息
技能使用(法师:释放范围火球)GameMode 或一个独立的 CombatAbilityComponent这是技能系统的范畴,见下一节 GAS
战斗演出触发PlayerController(Multicast RPC 或服务端触发客户端)演出是纯客户端表现
任务日志和地图标记客户端 Local 逻辑表现不需要全部网络同步
掉线玩家托管GameMode(服务端)规则制定者

这种”给需求找主人”的思维练习,就是在训练你作为 Gameplay 程序员的架构能力。


五、GAS——用技能系统做复杂的 Gameplay 逻辑#

1 GameplayAbilitySystem 是什么#

GAS 是 UE 官方提供的一套用来做技能、属性、Buff 的框架。它不是引擎内置类型——而是在 UE 源码的 Plugins/Runtime/GameplayAbilities 下,需要手动启用。

很多游戏项目的技能系统是在 GameMode/Actor 里硬写 if-else,最后变成几千行的上帝类。GAS 解决的问题就是把”谁会什么技能、技能有什么效果、效果怎么叠加”从具体 Actor 里解耦出来

2 核心概念#

AbilitySystemComponent (ASC)
├── GameplayAbilities (技能——具体"做什么")
│ ├── 火球技能 → 对范围内敌人造成伤害
│ ├── 治疗技能 → 恢复队友生命值
│ └── 冲刺技能 → 短时间提高移动速度
├── GameplayEffects (效果——"状态变化")
│ ├── 恢复生命值
│ ├── 被定身(不能移动)
│ └── 下一阶段获得额外护盾
├── GameplayTags (标签——"你是谁、你身上有什么状态")
│ ├── Character.Mage (法师)
│ ├── Status.Phase.Combat (战斗阶段)
│ └── Buff.NextWave.Shield (下一波护盾)
└── GameplayAttributes (属性——"你的数值")
├── Health (生命值)
├── MoveSpeed (移动速度)
└── AbilityPower (技能强度)

3 为什么用 GAS 而不是自己写#

自己写GAS
技能效果散落在各处GameplayEffect 统一描述”属性和标签的状态变化”
Buff/Debuff 需要手动计时和清理GE Duration 策略(Instant / HasDuration / Infinite)自动管理
网络同步自己写 RPCGAS 内置 Prediction(客户端预测技能效果,服务端验证)
技能之间的交互(免疫、覆盖)写 if-elseGameplayTag 查询——“有没有这个 Tag?有就不能释放”

4 合作副本里用 GAS 合适吗#

当角色有技能、Buff、免疫和多人预测时,GAS 就很合适。即使是一个小型合作副本,它也值得学习,原因是:

  • 你的导师让你看 Gameplay,GAS 是 UE Gameplay 方向的必修课
  • 很多商业项目用 GAS(包括网易的一些 UE 项目),面试可能会问
  • 即使不用全套 GAS,它的设计思想(把 GameplayEffect 看成”状态变化的事务”、把 GameplayTag 看成”状态查询的索引”)值得在你的任何技能系统里采用

六、总结#

一句话在哪存在
GameMode比赛规则,换玩法就换它仅服务端
GameState公开记分板,所有人都能看服务端 + 复制到所有客户端
PlayerState个人记分卡,跨 Pawn 保持服务端 + 复制到所属客户端
PlayerController玩家的遥控器,输入→操作服务端 + 客户端各一个
Pawn玩家的身体,在世界上有位置服务端 + 复制
ASC (GAS)技能/属性/效果的管理中心通常在 PlayerState 或 Pawn 上

搞清这六个类的职责边界,是写好 UE Gameplay 代码的第一步。大多数”为什么这个值读不到”、“为什么客户端看不到这个变化”的 bug,根因都在这六个类谁该拥有什么数据上。

UE Gameplay 框架——谁在管理这个游戏
https://www.m4doka.xyz/posts/engine/engine-8-gameplay-framework/
作者
m4doka
发布于
2026-07-24
许可协议
CC BY-NC-SA 4.0