模块插件与 Subsystem
自动关联目录:模块插件与 Subsystem
模块(Module)是 UE 的编译单元,插件(Plugin)是模块的分发单元,Subsystem 是"生命周期被引擎托管的全局管理器"。三件事解决的是同一个问题:怎么把代码切成可以被别人依赖、可以独立加载、可以自动管理生命周期的块。
一句话定位:C++ 项目里那些"手写的单例",在 UE 里都应该写成 Subsystem。
模块
Build.cs
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate", "SlateCore", "UMG"
});
}
}| 类别 | 含义 | 选择依据 |
|---|---|---|
PublicDependencyModuleNames | 依赖会传递 | 我的头文件里暴露了来自该模块的类型 |
PrivateDependencyModuleNames | 依赖不传递 | 只在 .cpp 里用 |
判断标准很简单:如果 #include 出现在 .h 里,就该放 Public;只在 .cpp 里,放 Private。全塞 Public 会让依赖图变稠,改动一个模块触发大面积重编。
导出宏
class MYMODULE_API FMyClass // 跨模块访问必须加
{
...
};跨模块使用的类型/函数不带 XXX_API 宏,链接期报"无法解析的外部符号"——这是建新模块时最容易卡住的一步。宏名来自模块名大写 + _API,由 IMPLEMENT_MODULE 自动生成。
入口
// 普通模块
IMPLEMENT_MODULE(FDefaultModuleImpl, MyModule);
// 游戏主模块(一个项目只能有一个)
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, MyGame, "MyGame");
// 自定义模块的启动/关闭
class FMyModule : public IModuleInterface
{
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
IMPLEMENT_MODULE(FMyModule, MyModule);StartupModule 里只能做与引擎无关的事(注册、分配)。此时很多子系统还没起来,访问 GEngine 可能为 null。
插件
| 项 | 说明 |
|---|---|
| 结构 | .uplugin 描述文件 + 一个或多个模块 |
| 加载时机 | LoadingPhase(Default / PreDefault / PostDefault / PostEngineInit…) |
| 类型 | Runtime / Editor / Developer / ThirdParty |
| 启用 | 编辑器插件面板,或 .uproject 里 Plugins 数组 |
插件与模块的边界:需要跨项目复用、需要独立开关、需要带资源或第三方库 → 插件;只是项目内部分层 → 模块。
加载时机选错的表现
| 现象 | 原因 |
|---|---|
编辑器启动时崩在插件的 StartupModule | LoadingPhase 太早,依赖的模块未加载 |
| 编辑器 UI 里看不到自己的扩展 | 加载太晚,扩展点已经收集完了 |
| 打包后插件不生效 | 插件类型为 Editor,未被打进运行时 |
Subsystem
Subsystem 是 UE 官方给出的"全局管理器"方案,替代手写单例。好处是生命周期由引擎托管、自动创建销毁、可配置、可继承。
| Subsystem 类型 | 生命周期 | 典型用途 |
|---|---|---|
UEngineSubsystem | 引擎进程(编辑器也活) | 全局配置、跨关卡服务 |
UEditorSubsystem | 编辑器 | 编辑器工具与状态 |
UGameInstanceSubsystem | 游戏实例(跨关卡存活) | 存档管理、账号、匹配 |
UWorldSubsystem | 单个 World | 关卡内的系统(刷怪、天气) |
ULocalPlayerSubsystem | 本地玩家 | 输入映射、UI 状态 |
UCLASS()
class UMyGameSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
void DoSomething();
};
// 使用
UMyGameSubsystem* Sys = GetGameInstance()->GetSubsystem<UMyGameSubsystem>();Initialize 的参数 Collection 用来声明依赖其它 Subsystem:
void UMyGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Collection.InitializeDependency<UOtherSubsystem>(); // 保证对方先初始化
}不声明依赖而直接 GetSubsystem,可能拿到还没初始化的实例(不会崩,但状态是空的)。
为什么不要手写单例
| 手写单例 | Subsystem |
|---|---|
| 生命周期靠自己管,容易泄漏 | 引擎托管,自动创建销毁 |
| 编辑器与运行时状态会串 | 实例跟随对应的外层对象,切换即销毁 |
| PIE 多实例会互相污染 | 每个 World/GameInstance 各一份 |
| 无法被蓝图/配置系统感知 | 可以 |
目录约定
Source/
MyGame/ 主模块
MyGameEditor/ 编辑器模块(不打进包)
MyFeature/ 功能模块
Plugins/
MyPlugin/
MyPlugin.uplugin
Source/MyPlugin/规则:运行时代码不要放进 Editor 模块,否则打包时报一堆找不到符号;反之编辑器工具放运行时模块会白白增大包体。
依赖关系与编译
| 现象 | 原因 |
|---|---|
| 改一个头文件全项目重编 | 依赖放在 Public 且被广泛依赖 |
| Unity Build 报符号冲突 | 两个模块的匿名空间/静态同名 |
| 链接慢 | 模块太多或依赖过密 |
| 循环依赖编译失败 | A 的 Public 依赖 B,B 的 Public 依赖 A |
解决循环依赖:把公共类型抽到第三个模块;或用接口(IModuleInterface / 纯虚类)反向调用。
常见坑
| 坑 | 说明 |
|---|---|
跨模块类型没加 _API | 链接期找不到符号 |
| Public/Private 全填 Public | 重编范围失控 |
StartupModule 里访问 GEngine | 时机太早,空指针 |
| 手写静态单例 | 编辑器与 PIE 状态串味 |
| Subsystem 之间没声明依赖 | 拿到未初始化的实例 |
| 运行时代码放在 Editor 模块 | 打包失败 |
插件 LoadingPhase 选错 | 扩展点收集不到或启动崩溃 |
| 模块名与宏名不一致 | XXX_API 未定义,编译报错 |