2322 字
12 分钟
模块化游戏架构——从巨型 God Class 到可插拔模块

一、问题:那个四万行的 GameMode#

不用绕弯,直接说痛点。

你接手过的一个 UE 项目或者之后进组会遇到的情况:

class AMyGameMode : public AGameMode {
void InitGame() {
// 初始化拍卖规则
// 初始化角色技能
// 初始化藏馆系统
// 初始化匹配系统
// 初始化商城
// 初始化排行榜
// ...
// 这个文件已经 4000 行了
}
};

大型 God Class 是游戏项目的头号技术债。它的形成路径通常是:

第一周:GameMode 管拍卖规则(合理)
第一个月:GameMode 管拍卖 + 匹配(勉强)
第三个月:GameMode 管拍卖 + 匹配 + 技能 + 商城 + 排行榜 + ...
↑ 已经没人敢动这个类了

模块化架构就是来解决这件事的。


二、UE Module 系统——编译期的模块化#

1 Module 是 UE 最基础的代码组织单元#

每个 Module 是一个独立的编译单元,由 Build.cs 文件定义:

MyAuctionGame.Build.cs
public class MyAuctionGame : ModuleRules {
public MyAuctionGame(ReadOnlyTargetRules Target) : base(Target) {
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"GameplayAbilities", // GAS
"GameplayTags", // Gameplay Tag
"OnlineSubsystem", // 在线服务
});
PrivateDependencyModuleNames.AddRange(new string[] {
"Slate",
"SlateCore",
"UMG", // UI
});
}
}

Public vs Private 依赖的区别

  • PublicDependency:你的头文件里 #include 了这个 Module 的东西 → 依赖你的人也需要链接它
  • PrivateDependency:只有你的 .cpp 内部用了,对外不可见 → 链接更快,更解耦

2 Module 的生命周期#

每个 Module 有一个入口类(IModuleInterface 的子类):

class FMyAuctionModule : public IModuleInterface {
virtual void StartupModule() override {
// Module 加载时调用
// 注册 Asset Types、注册 Console Commands、初始化子系统
UE_LOG(LogTemp, Log, TEXT("拍卖模块已加载"));
}
virtual void ShutdownModule() override {
// Module 卸载时调用
// 清理、反注册
UE_LOG(LogTemp, Log, TEXT("拍卖模块已卸载"));
}
};

Module 之间的依赖是显式的。一个 Module 不声明依赖另一个,就看不到另一个的任何符号。这迫使你做解耦——如果你在拍卖 Module 里想调商城 Module 的代码,你必须显式声明依赖。

3 你怎么给自己的项目拆 Module#

Module 名职责依赖
MyAuctionCore数据类型、枚举、结构体——纯数据Core, CoreUObject
MyAuctionGameplay拍卖规则、状态机、技能Core, GameplayAbilities, MyAuctionCore
MyAuctionUIHUD、开箱界面、出价面板UMG, MyAuctionCore
MyAuctionCollection藏馆 3D 展示Engine, MyAuctionCore
MyAuctionOnline匹配、房间管理、网络消息OnlineSubsystem, MyAuctionCore

经验法则:如果一个功能可以独立禁掉(“这个版本不上藏馆”),它就应该是一个 Module。


三、Plugin——运行时的模块化#

1 Plugin 和 Module 是什么关系#

Plugin (插件) ← 一个包含了一组 Module + 资源的独立包
├── Module A ← 一个编译单元
├── Module B ← 可以包含多个 Module
├── Content/ ← 插件自带的资产(材质、蓝图、UI)
├── Resources/ ← 图标、配置文件
└── MyPlugin.uplugin ← 插件描述文件

MyPlugin.uplugin

{
"FriendlyName": "拍卖系统",
"VersionName": "1.0",
"Modules": [
{
"Name": "MyAuctionCore",
"Type": "Runtime", // Runtime / Editor / Developer
"LoadingPhase": "Default" // 什么时候加载
}
]
}

区别

ModulePlugin
粒度一个 .dll/.so一组 Module + 资源
分发通常跟着项目可以独立分发、在不同项目间复用
版本无独立版本有独立版本号
资产不包含可以包含 Content 资产

2 用 Plugin 做功能拆分(你的竞拍游戏示例)#

Plugins/
├── AuctionCore/ # 拍卖核心逻辑(数据类型、状态机、技能定义)
├── AuctionMatchmaking/ # 匹配系统
├── AuctionCollection/ # 藏馆展示
├── AuctionStore/ # 商城(皮肤、藏馆装饰)
└── AuctionAnalytics/ # 数据埋点

好处:

  • 多人并行开发:你改藏馆 Plugin,我改匹配 Plugin,互不干扰(只要接口不变)
  • 测试隔离:每个 Plugin 可以独立做单元测试甚至独立运行
  • 可复用:下次做另一个竞拍游戏,AuctionCore Plugin 直接复用
  • 强制解耦:Plugin 之间的通信必须通过定义好的接口(不然互相看不到符号),保证了架构的干净性

3 网易内部大概率也是这套#

大厂的 UE 项目通常用 Plugin 做功能拆分。你的导师让你看 Modular,很可能你们组的项目就是 Plugin 架构——每个系统一个 Plugin,核心框架负责把 Plugin 串起来。


四、组合优于继承——设计模式在 Gameplay 的应用#

1 “剑圣继承了勇士,勇士继承了近战单位,近战单位继承了角色…”#

这是刚学 UE 时最容易犯的错误——试图用继承层次来表达所有变化维度。结果:

ACharacter
└── AMonster
├── AMeleeMonster
│ ├── ASwordMonster
│ │ ├── AFireSwordMonster ← 来了:冰焰双剑怎么办?
│ │ └── AStealthSwordMonster
│ └── AAxeMonster
└── ARangeMonster

每增加一个维度(武器 × 元素 × 行为 AI),类的数量呈指数爆炸。

2 用 Component 替代#

UE 的 Component 系统天然支持组合:

// 不用继承表达变化维度
class AMonster : public ACharacter {
UWeaponComponent* Weapon; // 用 Component 表示"拿什么武器"
UElementComponent* Element; // 用 Component 表示"附魔属性"
UBehaviorComponent* AIBehavior; // 用 Component 表示"行为模式"
};
// 剑圣 = AMonster + SwordWeapon + NoElement + AggressiveAI
// 冰焰双剑 = AMonster + DualSwordWeapon + IceFireElement + TacticalAI

Component 带来了两个关键能力

  1. 运行时组合:怪物可以在运行时换武器(SetComponent(NewWeapon)),不是编译时写死的
  2. 独立开发:武器系统、元素系统、AI 系统各自独立迭代,互不影响

3 Interface——“我想和任何这样做的对象交互”#

UE 的 UInterface 可以让你不关心具体类型,只关心”能不能做某事”:

UINTERFACE()
class UBiddable : public UInterface { GENERATED_BODY() };
class IBiddable {
GENERATED_BODY()
public:
virtual void PlaceBid(int32 Amount) = 0;
virtual int32 GetMaxBid() const = 0;
};
// 使用方不需要知道具体类
void ProcessBid(TScriptInterface<IBiddable> Bidder) {
Bidder->PlaceBid(SomeAmount); // 不关心这到底是个 Player 还是 AI Bot
}

判断是否该用接口的标准:如果你发现自己写 if (Cast<AHumanPlayer>(Actor)) { ... } else if (Cast<AAIBot>(Actor)) { ... },就该用接口了。


五、GameFeatures——UE5 的模块化 Gameplay 终极方案#

1 问题的终极形态#

大型游戏的特征:不同模式之间共享大量代码,但又有各自的特殊需求。竞拍游戏里:

基础框架(所有模式共有)
├── 单人竞拍模式
├── 四人联机竞拍模式
├── 团队竞拍模式(2v2)
└── 限时活动:万圣节特别竞拍(特殊藏品、特殊技能)

GameFeatures Plugin 就是为了解决这个问题的——把”模式特有”的东西打包成独立包,运行时按需加载

2 怎么工作#

GameFeatureAction (定义"激活这个 Feature 时做什么")
├── AddComponents → 给特定 Actor 添加 Component
├── AddDataRegistry → 注册新的数据表
├── AddAbilities → 给玩家添加特定的技能
├── AddInputMapping → 添加特殊的按键映射
└── SpawnActors → 生成特定的 Actor
// 万圣节限时活动的 GameFeatureAction
class UGFE_HalloweenEvent : public UGameFeatureAction {
virtual void OnGameFeatureActivating() override {
// 1. 替换所有藏品的模型为万圣节版本
// 2. 给所有角色添加 "不给糖就捣蛋" 技能
// 3. 替换开箱音效为恐怖音效
// 4. 加载万圣节限定 UI 主题
}
virtual void OnGameFeatureDeactivating() override {
// 活动结束后全部还原
}
};

关键:万圣节活动的所有代码和资源(模型、贴图、技能定义、UI)都在一个独立的 Plugin 里。活动上线时热加载,活动结束后禁用。不影响基础竞拍代码的一行代码。

3 网易类似的实践#

网易的很多 UE 项目自己也有类似的模块化方案——不一定叫 GameFeatures,但思路一致:

  • 核心框架 → 定义”功能模块”的加载/卸载接口
  • 各个 Gameplay 系统 → 各自实现为功能模块
  • 运行时配置 → 决定加载哪些模块(不同模式、不同平台、不同地区)

你的导师让你看 Modular 和 Experience,就是在让你理解这套架构思维——未来项目大概率也是这样组织的。


六、你的竞拍游戏怎么拆:一次设计练习#

模块名类型职责依赖
AuctionCorePlugin数据类型(拍卖轮次、藏品、角色)、状态机定义Core, GameplayAbilities
AuctionMatchPlugin房间匹配、大厅、准备OnlineSubsystem, AuctionCore
AuctionBiddingPlugin出价逻辑、技能效果、信息分发AuctionCore, GameplayAbilities
AuctionCollectionPlugin藏馆 3D 展示、藏品旋转查看Engine, AuctionCore
AuctionSeasonGameFeature赛季特殊规则(“本周只能用稀有角色”)AuctionCore
AuctionUIModule所有 UI WidgetUMG, AuctionCore

拆出来的直接好处:当策划说”这周改成团队竞拍模式(2v2)“,你需要改的只是换一个 GameMode + 加载一个 AuctionTeamMode Feature,而不是在四万行的 GameMode 里再加 2000 行的 Team 逻辑。


七、总结#

概念做什么的什么时候用
Module编译期代码组织代码量超过 5 万行就该考虑拆
Plugin可复用的功能包多个项目共享、独立团队维护
Component运行时的能力组合避免”剑圣→冰焰双剑”的继承地狱
Interface消除类型依赖发现自己在写 Cast 链时
GameFeature按需加载的模式/活动限时活动、A/B 测试

模块化不是结果——它是一个持续的过程。你现在 mini-game 阶段就该开始练习”这个东西属于哪里”的肌肉记忆。 进组后面对一个几百万行的项目,你不会被类之间的依赖关系吓到——因为你已经习惯了从”这该放进哪个模块”开始思考。

模块化游戏架构——从巨型 God Class 到可插拔模块
https://www.m4doka.xyz/posts/engine/engine-9-modular-architecture/
作者
m4doka
发布于
2026-07-27
许可协议
CC BY-NC-SA 4.0