虚幻引擎中的GameFeatures插件

文章字数:5279

核心概念与发展由来

GameFeature是UE5推出的支持动态装载游戏玩法的框架,采用插件化架构实现"即插即用"的模块化游戏功能。Epic开发这套框架的初衷是为了解决现代游戏(特别是服务型游戏)面临的以下挑战:

  • 内容频繁更新:如《堡垒之夜》赛季制内容轮换,需要动态添加/移除游戏功能
  • 并行开发需求:新功能可与当前版本并行开发,不破坏核心游戏
  • 热插拔能力:运行时动态开关功能,无需重新打包整个游戏

可结合虚幻引擎中的模块化Gameplay插件来理解本插件。

系统架构与核心组件

  graph TD
    A[UGameFeaturesSubsystem<br/>全局管理器] --> B[UGameFeaturePluginStateMachine<br/>状态机]
    B --> C[EGameFeaturePluginState<br/>状态枚举]
    A --> D[UGameFeatureData<br/>数据配置]
    D --> E[UGameFeatureAction<br/>动作列表]
    E --> F[AddComponent/AddWidget<br/>具体Action实现]
    
    A --> G[UGameFeaturesProjectPolicies<br/>策略控制]
    G --> H[Load/Client/Server模式]
    
    F --> I[UGameFrameworkComponentManager<br/>组件管理]
    I --> J[Receiver注册机制]

核心类职责

类名继承关系职责
UGameFeaturesSubsystemUEngineSubsystem全局单例,管理所有GameFeature生命周期,提供外部API
UGameFeaturePluginStateMachineUObject每个GameFeature对应一个状态机,处理状态流转
UGameFeatureDataUPrimaryDataAsset配置资产,定义该GameFeature要执行的Actions列表
UGameFeatureActionUObject动作基类,定义在特定生命周期阶段执行的操作
UGameFeaturesProjectPoliciesUObject策略类,控制加载规则(Client/Server/编辑器环境)
UGameFrameworkComponentManagerUGameInstanceSubsystem组件管理系统,处理动态AddComponent

生命周期状态机

GameFeature的生命周期由状态机严格管理。每个GameFeaturePlugin对应一个UGameFeaturePluginStateMachine实例,存储在UGameFeaturesSubsystem::GameFeaturePluginStateMachines(TMap<FString, StateMachine>)中。

核心状态定义

  stateDiagram-v2
    [*] --> Installed: 发现插件
    Installed --> Registered: Load/Register
    Registered --> Loaded: 预加载资源
    Loaded --> Active: Activate
    Active --> Loaded: Deactivate
    Loaded --> Registered: Unload
    Registered --> Installed: Unregister
    
    Active --> [*]: Terminal(释放内存)
    Installed --> [*]: Terminal
    
    note right of Active
        完全激活状态
        Actions正在执行
        对游戏产生影响
    end note
    
    note left of Installed
        仅在硬盘上存在
        Content浏览器不可见
    end note

状态详细说明

状态英文标识说明资产可见性
InstalledInstalled插件在本地存储中,尚未注册到AssetManager❌ Content中不显示
RegisteredRegistered插件资产为已知,扫描到AssetManager但尚未加载✅ 可浏览但未加载
LoadedLoaded插件加载到内存,在游戏中注册但未激活✅ 已加载
ActiveActive完全激活,Actions正在执行,影响Gameplay✅ 运行中

状态转换API

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// UGameFeaturesSubsystem提供的外部控制接口
void LoadGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void LoadAndActivateGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void DeactivateGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void UnloadGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void ReleaseGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void UninstallGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);
void TerminateGameFeaturePlugin(const FString& PluginURL, CompleteDelegate);

// 目标状态枚举(简化操作)
enum class EGameFeatureTargetState : uint8
{
    Installed,    // 仅安装
    Registered,   // 注册到AssetManager
    Loaded,       // 加载到内存
    Active        // 完全激活
};

状态机内部实现

状态转换通过ChangeGameFeatureDestination实现:

cpp
1
2
3
4
5
void UGameFeaturesSubsystem::ChangeGameFeatureDestination(
    UGameFeaturePluginStateMachine* Machine,
    const FGameFeaturePluginStateRange& StateRange,
    FGameFeaturePluginChangeStateComplete CompleteDelegate
);

每个状态继承自FGameFeaturePluginState,包含:

  • BeginState():进入状态时调用
  • UpdateState():每帧更新(检查条件推进到下一状态)
  • TryCancelState():尝试取消当前状态
  • EndState():离开状态时调用

过渡状态:状态之间存在中间过渡状态(如RegisteringLoadingActivating等),用于处理异步资源加载。


实战:创建第一个GameFeature

前置条件:开启必要插件

必须同时开启两个插件:

  1. GameFeatures:实现Actions执行和GameFeature装载
  2. ModularGameplay:为AddComponent提供实现支撑

路径:编辑(Edit)插件(Plugins) → 搜索启用

步骤1:创建GameFeature插件

  1. 打开插件窗口,点击+ 新建插件
  2. 选择 Game Feature (with C++) 模板
  3. 命名插件(如MyBattleFeature
  4. ⚠️ 关键约束:不要更改默认目录,必须放在Plugins/GameFeatures目录下! 源码硬编码只检测该目录

创建后自动生成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Plugins/GameFeatures/MyBattleFeature/
├── Content/
│   └── MyBattleFeature.uasset    ← GameFeatureData(必须保持同名)
├── Resources/
│   └── Icon128.png
├── Source/
│   └── MyBattleFeatureRuntime/
│       ├── MyBattleFeatureRuntime.Build.cs
│       ├── Public/MyBattleFeatureRuntimeModule.h
│       └── Private/MyBattleFeatureRuntimeModule.cpp
└── MyBattleFeature.uplugin

⚠️ 关键约束GameFeatureData资产名称必须与插件名完全一致!

步骤2:配置AssetManager

自动配置:通过编辑器创建GameFeature会自动配置AssetManager。

手动配置(如果从别处拷贝GameFeature):

  1. 打开 项目设置(Project Settings)Asset Manager
  2. 添加Primary Asset Type:
    • Primary Asset Type: GameFeatureData
    • Asset Base Class: GameFeatureData
    • Directories: 添加Plugins/GameFeatures/路径

⚠️ 必须配置:GameFeature强烈依赖AssetManager的资源探测发现机制。未配置会在编辑器启动时报警告,且无法正常工作。

步骤3:配置Actions

双击MyBattleFeature.uasset打开配置面板:

配置项说明
Initial State编辑器启动时的默认目标状态
Current State当前实际状态(运行时切换)
Actions该GameFeature激活时要执行的动作数组

常用内置Actions

Action类型功能适用场景
AddComponent向指定Actor类动态添加组件添加技能组件、动画组件
AddWidget向HUD添加UI控件添加血条、小地图
AddDataRegistry添加数据注册表配置表扩展
AddGameplayCue添加GameplayCue特效扩展

配置示例(AddComponent):

  • Actor Class: ACharacter(目标Actor基类)
  • Component Class: UMyBattleSkillComponent(要添加的组件)
  • Server/Client: 可选择仅服务器、仅客户端或两者都添加

⚠️ 注意:在Active状态下无法点击Edit Plugin按钮。编辑完GameFeatureData后,需将Current State改为Installed再切回Active以重新加载,或直接重启编辑器。

步骤4:注册Actor为Receiver

要让AddComponent生效,目标Actor必须注册为Receiver

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
    
    // 注册为GameFeature Receiver
    UGameFrameworkComponentManager::AddReceiver(this);
}

void AMyCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // 必须配对移除,避免内存泄漏
    UGameFrameworkComponentManager::RemoveReceiver(this);
    
    Super::EndPlay(EndPlayReason);
}

⚠️ 关键约束:忘记调用AddReceiver是调试时最常见的问题——配置了Action但Actor无反应!

简化方案:从ModularGameplayActors提供的基类继承,已内置Receiver注册:

  • AModularCharacter
  • AModularPlayerController
  • AModularGameState
  • AModularGameMode
  • 等等…

步骤5:运行时激活

编辑器方式

直接在GameFeatureData资产面板切换Current State滑块:InstalledRegisteredLoadedActive

C++运行时API

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取PluginURL(通过插件名)
FString PluginURL;
UGameFeaturesSubsystem::Get().GetPluginURLByName(FString("MyBattleFeature"), PluginURL);

// 方式1:加载并激活(异步)
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(
    PluginURL,
    FGameFeaturePluginLoadComplete::CreateLambda([](const UE::GameFeatures::FResult& Result)
    {
        if (Result.HasError())
        {
            UE_LOG(LogTemp, Error, TEXT("激活失败: %s"), *Result.GetError());
        }
        else
        {
            UE_LOG(LogTemp, Log, TEXT("激活成功!"));
        }
    })
);

// 方式2:指定目标状态(更灵活)
UGameFeaturesSubsystem::Get().ChangeGameFeatureTargetState(
    PluginURL,
    EGameFeatureTargetState::Active,  // 或 Installed/Registered/Loaded
    CompleteDelegate
);

⚠️ 注意Result.GetError()在成功时为空字符串,直接打印可能导致崩溃。应先判断Result.HasError()

控制台命令(Debug)

1
2
3
4
GameFeaturePlugin.Activate MyBattleFeature
GameFeaturePlugin.Deactivate MyBattleFeature
GameFeaturePlugin.Load MyBattleFeature
GameFeaturePlugin.Unload MyBattleFeature

核心机制深度解析

AddComponent实现原理

  sequenceDiagram
    participant GFCM as GameFrameworkComponentManager
    participant GF as GameFeature
    
    目标Actor->>GFCM: AddReceiver(this)
    GFCM->>GFCM: 添加到Receiver列表
    
    GF->>GFCM: AddComponentRequest(ActorClass, ComponentClass)
    GFCM->>GFCM: RequestTrackingMap[Key]++ (引用计数)
    
    alt Actor已初始化
        GFCM->>目标Actor: CreateComponentOnInstance()
        GFCM->>GFCM: ComponentClassToComponentInstanceMap[ComponentClass].Add(Instance)
    else Actor未生成
        GFCM->>GFCM: 等待Actor生成时再创建
    end
    
    GF->>GFCM: RemoveComponentRequest() (Deactivate时)
    GFCM->>GFCM: RequestTrackingMap[Key]--
    
    alt 引用计数==0
        GFCM->>GFCM: 查找ComponentClassToComponentInstanceMap
        GFCM->>目标Actor: 销毁该Component的所有Instance
    end

引用计数机制:多个GameFeature可向同一ActorClass请求添加相同ComponentClass,通过引用计数管理生命周期。

延迟注册支持:即使在GameFeature已激活后,新Actor才调用AddReceiver,也能立即收到ExtensionAdded事件并创建组件。

ExtensionHandler机制(非Component扩展)

用于监听Actor生命周期事件而非直接添加组件:

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 在GameFeatureAction中注册ExtensionHandler
TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle = 
    ComponentManager->AddExtensionHandler(
        HUDActorClass,  // 监听的Actor类
        UGameFrameworkComponentManager::FExtensionHandlerDelegate::CreateUObject(
            this, 
            &ThisClass::HandleActorExtension,
            ChangeContext
        )
    );

void HandleActorExtension(AActor* Actor, FName EventName, FGameFeatureStateChangeContext ChangeContext)
{
    if (EventName == UGameFrameworkComponentManager::NAME_ExtensionAdded ||
        EventName == UGameFrameworkComponentManager::NAME_GameActorReady)
    {
        // Actor准备就绪,执行自定义逻辑(如添加UI)
        AddWidgets(Actor);
    }
    else if (EventName == UGameFrameworkComponentManager::NAME_ExtensionRemoved ||
             EventName == UGameFrameworkComponentManager::NAME_ReceiverRemoved)
    {
        // Actor被移除,清理资源
        RemoveWidgets(Actor);
    }
}

资源加载流程

Registered状态,根据GameFeatureData中配置的PrimaryAssetTypesToScan

cpp
1
2
3
4
5
6
7
void UGameFeaturesSubsystem::AddGameFeatureToAssetManager(const UGameFeatureData* GameFeatureToAdd)
{
    // 1. 检查资源是否存在
    // 2. 扫描指定PrimaryAssetType
    // 3. 注册到AssetManager的扫描路径
    // 4. 异步加载所需Bundle
}

Unregistering状态时反向执行RemoveGameFeatureFromAssetManager卸载资源。


高级用法与扩展

自定义GameFeatureAction

继承UGameFeatureAction实现自定义逻辑:

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
UCLASS(MinimalAPI, meta = (DisplayName = "Add My Custom System"))
class UGameFeatureAction_AddMySystem : public UGameFeatureAction
{
    GENERATED_BODY()
    
public:
    // 配置属性
    UPROPERTY(EditAnywhere, Category=Settings)
    TSoftClassPtr<UMySystem> SystemClass;
    
    // 生命周期回调
    virtual void OnGameFeatureRegistering(...) override;
    virtual void OnGameFeatureActivating(...) override;
    virtual void OnGameFeatureDeactivating(...) override;
    virtual void OnGameFeatureUnregistering(...) override;
};

自定义ProjectPolicies

创建策略类控制加载规则(如区分Client/Server加载不同GameFeature):

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
UCLASS(Config=Game)
class MYGAME_API UMyGameFeaturePolicy : public UGameFeaturesProjectPolicies
{
    GENERATED_BODY()
    
public:
    virtual void InitGameFeatureManager() override;
    virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;
    virtual bool IsPluginAllowed(const FString& PluginURL) const override;
};

配置路径:项目设置GameFeaturesGame Features Manager Class

Lyra项目最佳实践

Lyra示例项目展示了GameFeature的工业化用法:

UI扩展模式

cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GameFeatureAction_AddWidgets 示例
void AddToWorld(const FWorldContext& WorldContext, const FGameFeatureStateChangeContext& ChangeContext)
{
    if (UGameFrameworkComponentManager* ComponentManager = 
        UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance))
    {
        // 注册ExtensionHandler而非直接AddComponent
        TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle = 
            ComponentManager->AddExtensionHandler(
                ALyraHUD::StaticClass(),
                FExtensionHandlerDelegate::CreateUObject(this, &ThisClass::HandleActorExtension, ChangeContext)
            );
    }
}

void HandleActorExtension(AActor* Actor, FName EventName, ...)
{
    if (EventName == UGameFrameworkComponentManager::NAME_GameActorReady)
    {
        // 向CommonUI的LayerStack推入Widget
        UCommonUIExtensions::PushContentToLayer_ForPlayer(LocalPlayer, LayerID, WidgetClass);
    }
}

常见问题与调试技巧

必查清单

问题现象可能原因解决方案
GameFeature不显示在Content中不在Plugins/GameFeatures目录移动插件到正确目录
AssetManager报错未配置GameFeatureData扫描路径检查AssetManager设置
AddComponent无反应Actor未调用AddReceiver在BeginPlay中添加Receiver注册
重命名Blueprint后验证失败GameFeature引用旧名称(UE5.1 Bug)重新编译+保存,或回退到5.0.3
运行时加载失败PluginURL错误使用GetPluginURLByName获取正确URL
Callback崩溃成功时调用Result.GetError()先判断Result.HasError()

调试命令

cpp
1
2
3
4
5
6
7
// 控制台
GameFeatures.List          // 列出所有GameFeature状态
GameFeatures.Activate [Name]
GameFeatures.Deactivate [Name]

// C++调试
UGameFeaturesSubsystem::Get().DumpGameFeatureState();  // 打印状态机详情

最佳实践总结

  1. 目录结构:严格保持Plugins/GameFeatures/路径,不修改GameFeatureData命名
  2. 状态管理:理解状态机流转,避免在Active状态编辑配置
  3. Receiver注册:所有需要动态添加Component的Actor必须正确注册/移除Receiver
  4. 资源管理:合理配置PrimaryAssetType扫描,避免加载不必要的资源
  5. 网络兼容:利用GetGameFeatureLoadingMode区分Client/Server加载策略
  6. 版本控制:GameFeature插件应独立版本控制,便于并行开发和热更新
  7. 错误处理:异步API回调中始终检查Result.HasError()再访问错误信息

通过GameFeature框架,可以实现真正的模块化、服务化游戏开发,让"游戏即服务"(GaaS)的技术落地变得切实可行。

参考信息

本文包含一些 AIGC 辅助生成内容,由作者人工校对整理后发布
本文采用 CC BY-NC-SA 4.0协议,如果对您有帮助或存在意见建议,欢迎在下方评论交流。
本页面浏览次数 加载中...
本页面访客数 加载中...

加载中...