一、Gameplay 框架的最后一块拼图
前面两篇分别讲了:
- Gameplay 框架(第 8 篇):GameMode / GameState / PlayerController / Pawn / PlayerState 各自的职责
- 模块化架构(第 9 篇):Module、Plugin、Component、Interface、GameFeature 怎么拆
现在还有一个问题没回答:谁来把这些东西串起来?
传统做法是在 GameMode::InitGame() 里手动初始化一切。但如果你的游戏有多个模式、每个模式需要加载不同的模块组合,硬编码的方式很快就失控了:
// 这个 InitGame 可能长这样——一个巨型 switchvoid InitGame() { if (Mode == EGameMode::Solo) { LoadSoloSystems(); } else if (Mode == EGameMode::Multiplayer) { LoadMultiplayerSystems(); } else if (Mode == EGameMode::HalloweenEvent) { LoadHalloweenSystems(); // 加载的东西和上面完全不同 } // ... 越来越长}Experience 系统就是来解决”不同玩法模式需要不同模块组合”的最外层编排问题。
二、Experience 是什么
1 一句话定义
Experience = 一个玩法模式的完整定义。它声明了:谁来当规则(GameMode)、加载哪些功能模块(GameFeatures)、加载哪些输入/技能/ UI 的默认设置。
它不是 UE 内置类型——而是 Lyra 示例项目(Epic 官方出品的 UE5 多人游戏示例)里提出的一种架构模式。你的导师让你看 Experience,大概率是在指 Lyra 的这种设计思路。
2 Experience 里包含了什么
UCLASS()class ULyraExperienceDefinition : public UPrimaryDataAsset { // 这一局用什么 GameMode UPROPERTY() TSubclassOf<AGameModeBase> GameModeToUse;
// 需要加载哪些 GameFeature Plugin UPROPERTY() TArray<FString> GameFeaturesToEnable;
// 需要执行哪些 Action(给 Pawn 加 Component、加载 UI 等) UPROPERTY() TArray<UGameFeatureAction*> Actions;
// 默认的输入配置(哪个按键做哪个操作) UPROPERTY() ULyraInputConfig* DefaultInputConfig;
// 默认加载的 Pawn UPROPERTY() TSubclassOf<APawn> DefaultPawnClass;};你可以把 Experience 理解为一个配置表——把 GameMode + GameFeature + InputConfig + DefaultPawn 的选型打包成一个资产。换一个玩法模式,就换一个 Experience 资产。
3 一个竞拍游戏的可能 Experience 设计
DA_Experience_SoloAuction (单人竞拍)├── GameMode: AGameMode_SoloAuction├── GameFeatures:│ ├── AuctionCore│ └── AuctionCollection├── DefaultPawn: APawn_AuctionCamera└── Actions: ├── Add HUD_SoloAuction (单人版 UI) └── Add BP_SoloAuctionManager (离线 AI 对手管理)
DA_Experience_MultiAuction (四人联机)├── GameMode: AGameMode_MultiAuction├── GameFeatures:│ ├── AuctionCore│ ├── AuctionMatch│ ├── AuctionBidding│ ├── AuctionCollection│ └── AuctionOnline├── DefaultPawn: APawn_AuctionCamera└── Actions: ├── Add HUD_MultiAuction (联机版 UI——多了聊天、玩家列表) ├── Add BP_NetworkManager (网络同步管理) └── Register OnlineSubsystem
DA_Experience_Halloween (万圣节限时活动)├── GameMode: AGameMode_MultiAuction (复用联机规则)├── GameFeatures:│ ├── (继承 MultiAuction 的全部 Feature)│ └── AuctionHalloween ← 只追加万圣节特有内容├── DefaultPawn: APawn_HalloweenCamera└── Actions: ├── 替换藏品模型为万圣节版本 ├── 添加 "Trick or Treat" 技能 └── 加载万圣节日历 UI注意 Halloween 复用了 MultiAuction 的 GameMode ——规则不变,只是换了皮肤和加了一个技能。这就是模块化的威力。
三、Experience 的生命周期
1 从加载到激活
1. 前端选择 Experience → 决定是 Solo / Multi / Halloween
2. GameInstance::LoadExperience(ExperienceDef) → 加载 Experience 配置资产
3. 加载所有列出的 GameFeature Plugin → AssetManager 异步加载 .uplugin 包 → 每个 Plugin 的 StartupModule 被执行
4. 执行所有 GameFeatureAction → AddComponents → Pawn 获得了竞拍相关的能力 Component → AddAbilities → ASC 获得了角色技能 → AddInputMapping → 建立了按键绑定
5. 切换 GameMode → 开始游戏关键:Experience 的加载发生在 GameMode 之前。GameMode 开始工作时,所有需要的功能模块都就绪了。
2 和传统 InitGame 的对比
// 传统方式:InitGame 里硬编码void AMyGameMode::InitGame(...) { Super::InitGame(...);
// 初始化所有子系统——每次加一个都得改这里 AuctionSystem = NewObject<UAuctionSystem>(this); SkillSystem = NewObject<USkillSystem>(this); MatchSystem = NewObject<UMatchSystem>(this); // ... 这些东西到底哪些是 Solo 需要的?哪些是 Multi 需要的? // 一个 InitGame 全加载了,游戏模式之间没法区分}
// Experience 方式:由配置资产声明// 代码零改动。策划/TA 可以在编辑器里拖配置来改变玩法加载的内容3 Action:Experience 的执行单元
UGameFeatureAction 是 Experience 里最灵活的部分:
// 给所有 PlayerState 加一个 Componentclass UGameFeatureAction_AddComponents : public UGameFeatureAction { UPROPERTY() TArray<FComponentRequest> ComponentList; // 要加的组件类 + 目标类
virtual void OnGameFeatureActivating() override { // 监听 World 里新 Actor 的 Spawn // 当匹配的 Actor 出现时,自动附加 Component }};
// 给所有 ASC 添加技能class UGameFeatureAction_AddAbilities : public UGameFeatureAction { UPROPERTY() TArray<FAbilityGrant> AbilitiesToGrant;
virtual void OnGameFeatureActivating() override { // 找到所有 ASC,注册技能 }};Lyra 里提供了几十种 Action。你自己的项目可以根据需要扩展——比如写一个 UGameFeatureAction_EnableAuctionSkills 来批量注册竞拍角色技能。
四、这套东西对自研引擎的启示
你的导师让你看这些,不一定是让你背 Lyra 的 API。更重要的是理解这背后的设计模式。
1 自研引擎大概率没有 GameFeatures
但你可以自己实现类似的概念——不一定叫 Experience,但解决的是同一个问题:“不同游戏模式/活动下,功能模块的组合不同。”
最简单的实现就是 JSON 配置 + 注册反射:
{ "gameMode": "SoloAuction", "features": ["AuctionCore", "AuctionCollection"], "inputConfig": "SoloInput", "pawnClass": "AuctionCameraPawn", "actions": [ { "type": "AddUI", "uiClass": "HUD_Solo" }, { "type": "SpawnManager", "managerClass": "SoloAIManager" } ]}// C++ 侧解析这个 JSON,加载对应的 DLL/Modulevoid LoadExperience(const FExperienceDef& Def) { for (auto& Feature : Def.Features) { FModuleManager::Get().LoadModule(Feature); // 动态加载 Module } for (auto& Action : Def.Actions) { ExecuteAction(Action); // 反射调用对应的 Action 处理函数 }}2 核心原则
| 原则 | 为什么 |
|---|---|
| 代码不感知模式差异 | GameMode 不写 if (IsSolo) ... |
| 配置资产定义模式 | 换个配置换一套玩法,不改代码 |
| 功能按需加载 | 单人不加载 OnlineSubsystem、万圣节不加载基础竞拍 UI |
| Action 是扩展点 | 未来加了”公会竞拍”模式,只需要一个新的 Action 类型 |
五、你现在的 mini-game 里可以试试的
不用把 Lyra 全套 Experience 系统搬进来——太重了。但你可以做一个最小版本:
- 把 GameMode 里的 InitGame 里硬编码的功能初始化,改成读一个 DataTable
- 把你竞拍游戏的不同角色技能,定义为 GameFeatureAction(即使你不用 GameFeature Plugin,用 DataAsset 也行)
- 试着把你的藏馆系统拆成一个独立的 Plugin,体会一下”这个功能能不能独立于主项目编译和测试”
这三步做完,你对”模块化 Gameplay”的理解就从”看过”变成”做过”了。
六、你导师让你看的三个东西串起来
Gameplay 框架(8)── 搞清楚"每个类干嘛" │ ▼模块化架构(9) ── 搞清楚"怎么把功能拆开" │ ▼Experience(10) ── 搞清楚"怎么把拆开的东西灵活组合"这三个放在一起,就是现代 UE Gameplay 架构的完整图景:
用 Gameplay 框架的类各司其职 → 每个功能做成独立的模块/Plugin → 用 Experience 层按模式配置组合
你的导师想让你理解的,大概率就是这条路。
七、总结
| 概念 | 一句话 |
|---|---|
| Experience | 一个玩法模式的完整定义——GameMode + Features + Actions 的打包 |
| GameFeatureAction | Experience 的执行单元——“激活时做什么” |
| 与传统 InitGame 的区别 | 从硬编码初始化 → 配置资产声明,代码零改动 |
| 对自研引擎的启发 | JSON 配置 + Module 动态加载 + Action 模式 = 自己的 Experience |
| 你现在可以做的 | 拆一个 Plugin、用一个 DataAsset 驱动 GameMode 初始化 |
Experience 不是 Lyra 的专利——它是一种设计思想。“让玩法配置来编排功能模块,而不是让代码硬编码初始化顺序。” 理解了这一点,你以后用任何引擎都能做出类似的架构。