1888 字
9 分钟
GameFeatures 与 Experience——模块化 Gameplay 的编排层

一、Gameplay 框架的最后一块拼图#

前面两篇分别讲了:

  • Gameplay 框架(第 8 篇):GameMode / GameState / PlayerController / Pawn / PlayerState 各自的职责
  • 模块化架构(第 9 篇):Module、Plugin、Component、Interface、GameFeature 怎么拆

现在还有一个问题没回答:谁来把这些东西串起来?

传统做法是在 GameMode::InitGame() 里手动初始化一切。但如果你的游戏有多个模式、每个模式需要加载不同的模块组合,硬编码的方式很快就失控了:

// 这个 InitGame 可能长这样——一个巨型 switch
void 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 加一个 Component
class 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 配置 + 注册反射:

Experience_Solo.json
{
"gameMode": "SoloAuction",
"features": ["AuctionCore", "AuctionCollection"],
"inputConfig": "SoloInput",
"pawnClass": "AuctionCameraPawn",
"actions": [
{ "type": "AddUI", "uiClass": "HUD_Solo" },
{ "type": "SpawnManager", "managerClass": "SoloAIManager" }
]
}
// C++ 侧解析这个 JSON,加载对应的 DLL/Module
void 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 系统搬进来——太重了。但你可以做一个最小版本:

  1. 把 GameMode 里的 InitGame 里硬编码的功能初始化,改成读一个 DataTable
  2. 把你竞拍游戏的不同角色技能,定义为 GameFeatureAction(即使你不用 GameFeature Plugin,用 DataAsset 也行)
  3. 试着把你的藏馆系统拆成一个独立的 Plugin,体会一下”这个功能能不能独立于主项目编译和测试”

这三步做完,你对”模块化 Gameplay”的理解就从”看过”变成”做过”了。


六、你导师让你看的三个东西串起来#

Gameplay 框架(8)── 搞清楚"每个类干嘛"
模块化架构(9) ── 搞清楚"怎么把功能拆开"
Experience(10) ── 搞清楚"怎么把拆开的东西灵活组合"

这三个放在一起,就是现代 UE Gameplay 架构的完整图景:

用 Gameplay 框架的类各司其职 → 每个功能做成独立的模块/Plugin → 用 Experience 层按模式配置组合

你的导师想让你理解的,大概率就是这条路。


七、总结#

概念一句话
Experience一个玩法模式的完整定义——GameMode + Features + Actions 的打包
GameFeatureActionExperience 的执行单元——“激活时做什么”
与传统 InitGame 的区别从硬编码初始化 → 配置资产声明,代码零改动
对自研引擎的启发JSON 配置 + Module 动态加载 + Action 模式 = 自己的 Experience
你现在可以做的拆一个 Plugin、用一个 DataAsset 驱动 GameMode 初始化

Experience 不是 Lyra 的专利——它是一种设计思想。“让玩法配置来编排功能模块,而不是让代码硬编码初始化顺序。” 理解了这一点,你以后用任何引擎都能做出类似的架构。

GameFeatures 与 Experience——模块化 Gameplay 的编排层
https://www.m4doka.xyz/posts/engine/engine-10-experience-system/
作者
m4doka
发布于
2026-07-30
许可协议
CC BY-NC-SA 4.0