总览
Game Features 最容易把新人看晕的地方是状态机。源码里的状态图很大:检查状态、下载安装、挂载、等待依赖、资源流送、注册、加载、激活、停用、卸载、卸载挂载、错误状态都在里面。日常使用时先抓四个目标状态就够了:Installed、Registered、Loaded、Active。
四个目标状态
| 目标状态 | 已经发生了什么 | 还没发生什么 |
|---|---|---|
Installed | 知道这个包存在或可安装 | 未注册 GameFeatureData |
Registered | 读到 GameFeatureData,执行 Registering | 未加载激活资源 |
Loaded | 执行 Loading,预加载需要的资产 | Actions 还没应用到世界 |
Active | 执行 Activating/Activated,组件、输入、UI、生效 | 已经进入可用状态 |
停用方向大致是 Active -> Deactivating -> Loaded -> Unloading -> Registered -> Unregistering -> Installed。
控制台命令
UE5.8 UGameFeaturesSubsystem 注册了这些命令:
ListGameFeaturePlugins
LoadGameFeaturePlugin WeaponPack_Rifle
DeactivateGameFeaturePlugin WeaponPack_Rifle
UnloadGameFeaturePlugin WeaponPack_Rifle
UnloadAndKeepRegisteredGameFeaturePlugin WeaponPack_Rifle
ReleaseGameFeaturePlugin WeaponPack_Rifle
CancelGameFeaturePlugin WeaponPack_Rifle
TerminateGameFeaturePlugin WeaponPack_Rifle
EnableDebugGameFeatureState WeaponPack_Rifle
DisableDebugGameFeatureState WeaponPack_Rifle
GameFeaturePlugin.PrintLoadStats
日常调试先用 ListGameFeaturePlugins 看状态,再用 LoadGameFeaturePlugin 激活。命令名叫 Load,但源码里它调用的是 LoadAndActivateGameFeaturePlugin。
C++ API 最小用法
void UMyFeatureRuntimeSubsystem::EnableRifleFeature()
{
FString PluginURL;
if (!UGameFeaturesSubsystem::Get().GetPluginURLByName(TEXT("WeaponPack_Rifle"), PluginURL))
{
return;
}
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(
PluginURL,
FGameFeaturePluginLoadComplete::CreateWeakLambda(this,
[](const UE::GameFeatures::FResult& Result)
{
if (Result.HasError())
{
UE_LOG(LogTemp, Error, TEXT("Rifle feature failed: %s"), *Result.GetError());
}
}));
}
停用:
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(
PluginURL,
FGameFeaturePluginDeactivateComplete::CreateWeakLambda(this,
[](const UE::GameFeatures::FResult& Result)
{
UE_LOG(LogTemp, Log, TEXT("Rifle feature deactivated"));
}));
URL 是什么
Subsystem 支持不同协议。常见本地内置插件用 file protocol,安装包场景会用 install bundle protocol。多数项目初期不需要手写 URL,优先用 GetPluginURLByName。等你做 DLC、IAD、云端安装包,再研究 InstallBundle。
使用案例:大厅按模式启用玩法
大厅里选择“团队死斗”时,服务器先激活 ShooterCore 和 ShooterMaps,客户端收到体验配置后再激活对应 UI/输入包。退出模式时先禁止新输入,再 Deactivate 玩法包。这样主工程不用永远带着所有模式的规则、HUD 和输入映射。
激活上下文
FGameFeatureStateChangeContext 可以限制状态变化应用到特定 WorldContext。FGameFeatureActivatingContext 和 FGameFeatureDeactivatingContext 继承它。大多数单世界游戏不需要手动改;编辑器、多 PIE、服务器旅行、工具世界里会很有用。
停用时有一个特殊能力:FGameFeatureDeactivatingContext::PauseDeactivationUntilComplete。如果你的 Action 停用需要异步收尾,例如等待 UI 退场、保存数据、通知远端,可以暂停停用,完成后调用 delegate。
架构分析
UGameFeaturesSubsystem 为每个插件 URL 找到或创建 UGameFeaturePluginStateMachine。状态机负责推进具体状态,每个状态做完工作后进入下一个状态。源码里还有 GameFeaturePlugin.UseNewExecutor、GameFeaturePlugin.EnableBatchProcessing、GameFeaturePlugin.AsyncLoad、GameFeaturePlugin.EnableAssetStreaming 等变量,用来控制执行器、批处理、异步加载和资源流送。
这就是为什么你不应该自己直接 Mount 插件、手动 Load Asset、再手动调用 Action。让状态机推进,错误处理和依赖顺序才一致。
常见坑
- 在 BeginPlay 里立刻依赖某个 Active 包,但它只是 Registered。
- 控制台命令用 PluginName 能成功,代码里硬写 file URL 反而跨平台失败。
- 忘记处理 Result,激活失败后界面还显示“已启用”。
- 在 Deactivate 回调前就销毁依赖对象,Action 清理时访问失效世界。
- 多 PIE 场景没考虑 WorldContext,调试结果和单机 PIE 不一样。
源码依据
UGameFeaturesSubsystem 暴露 Load、Register、LoadAndActivate、ChangeGameFeatureTargetState、Deactivate、Unload、Release、Terminate 等 API。GameFeaturePluginStateMachine.h 中状态图标注了 Installed、Registered、Loaded、Active 是外部可请求的目标状态,并定义了大量错误状态。GameFeaturesSubsystem.cpp 注册了 List/Load/Deactivate/Unload 等控制台命令。
源码路径索引
GameFeatures/Public/GameFeaturesSubsystem.hGameFeatures/Private/GameFeaturesSubsystem.cppGameFeatures/Private/GameFeaturePluginStateMachine.hGameFeatures/Public/GameFeaturePluginOperationResult.h