UE5.8 Game Features 喂饭级专题

UE5.8 Game Features 专题(十):自定义 GameFeatureAction

从 UGameFeatureAction 派生、处理 WorldContext、保存 FPerContextData、注册/解绑 delegate、异步停用、数据校验和 AssetBundleData,写一个可生产使用的自定义 Action。

总览

项目真正落地时,一定会写自定义 Action:加 UI、注册消息监听、授予任务、接相机模式、注册商店入口、注入 GameplayCue 路径。写 Action 的标准不是“能激活”,而是重复激活不重复、停用能清干净、多世界不串、异步不悬空

UE5.8 Game Features 专题(十):自定义 GameFeatureAction 配图
自定义 Action 的核心不是写 Activating,而是能按 Context 精确清理。

使用案例:AddReticleWidget

Rifle 武器包激活后,需要在 HUD 上显示准星。主工程不应该永远内置所有武器准星,武器包也不应该直接改主 HUD 蓝图。更稳的方式是写一个 Add Reticle Widget Action:激活时向项目 UI 扩展点注册准星 Widget,停用时注销。

示例:AddReticleWidget

目标:武器包激活后,给本地玩家 HUD 加一个准星 Widget;停用后移除。

USTRUCT()
struct FReticleWidgetEntry
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    TSoftClassPtr<APlayerController> PlayerControllerClass;

    UPROPERTY(EditAnywhere, meta=(AssetBundles="Client"))
    TSoftClassPtr<UUserWidget> WidgetClass;

    UPROPERTY(EditAnywhere)
    FGameplayTag SlotTag;
};

UCLASS(meta=(DisplayName="Add Reticle Widget"))
class UGameFeatureAction_AddReticleWidget : public UGameFeatureAction
{
    GENERATED_BODY()

public:
    virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
    virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;

#if WITH_EDITOR
    virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override;
#endif

private:
    UPROPERTY(EditAnywhere)
    TArray<FReticleWidgetEntry> Widgets;

    struct FPerContextData
    {
        TArray<TWeakObjectPtr<UUserWidget>> AddedWidgets;
        TArray<FDelegateHandle> DelegateHandles;
    };

    TMap<FGameFeatureStateChangeContext, FPerContextData> ContextData;
};

这只是骨架,关键是 FPerContextData。每次激活按 Context 保存自己加过的东西,停用时只清理自己那份。

Activating 怎么写

激活阶段:

  1. 遍历 GEngine->GetWorldContexts()
  2. Context.ShouldApplyToWorldContext(WorldContext) 过滤。
  3. 确认 World->IsGameWorld()
  4. 找本地 PlayerController/HUD/UI Extension。
  5. 创建或注册 Widget。
  6. 保存 widget 指针或注册 handle。

不要在一个全局数组里保存所有 Widget,也不要假设只有一个 World。

Deactivating 怎么写

停用阶段:

void UGameFeatureAction_AddReticleWidget::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
    if (FPerContextData* ActiveData = ContextData.Find(Context))
    {
        for (TWeakObjectPtr<UUserWidget>& Widget : ActiveData->AddedWidgets)
        {
            if (UUserWidget* StrongWidget = Widget.Get())
            {
                StrongWidget->RemoveFromParent();
            }
        }

        ActiveData->AddedWidgets.Reset();
        ActiveData->DelegateHandles.Reset();
    }

    ContextData.Remove(Context);
}

如果 UI 有退场动画,可以用 PauseDeactivationUntilComplete,动画完成后再调用 delegate。但不要在 Shipping 路径里无限等待。

Data Validation

#if WITH_EDITOR
EDataValidationResult UGameFeatureAction_AddReticleWidget::IsDataValid(FDataValidationContext& Context) const
{
    EDataValidationResult Result = EDataValidationResult::Valid;
    for (int32 Index = 0; Index < Widgets.Num(); ++Index)
    {
        if (Widgets[Index].WidgetClass.IsNull())
        {
            Context.AddError(FText::Format(
                NSLOCTEXT("GameFeatures", "MissingWidget", "WidgetClass is missing at index {0}."),
                FText::AsNumber(Index)));
            Result = EDataValidationResult::Invalid;
        }
    }
    return Result;
}
#endif

自定义 Action 是给团队配置的,不校验就等于把错误推到运行时。

AddAdditionalAssetBundleData

如果 Action 引用资源,要考虑 bundle:

#if WITH_EDITORONLY_DATA
void UGameFeatureAction_AddReticleWidget::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
    for (const FReticleWidgetEntry& Entry : Widgets)
    {
        AssetBundleData.AddBundleAsset(
            UGameFeaturesSubsystemSettings::LoadStateClient,
            Entry.WidgetClass.ToSoftObjectPath().GetAssetPath());
    }
}
#endif

这样 Cook 和预加载才知道 Widget 属于 Client bundle。

架构分析

自定义 Action 的本质是一条“可撤销命令”。它可以注册东西、创建东西、授予东西,但必须记录自己做过什么。FPerContextData 是核心:同一个插件可能在不同 WorldContext 下激活,PIE、多世界、服务器旅行都会暴露全局状态的错误。所有 delegate handle、widget handle、ability handle、request handle 都应该按 Context 保存。

常见坑

  • ContextData 不按 Context 存,多 PIE 或多世界互相清理。
  • Delegate 只绑定不解绑。
  • Deactivate 里 Remove 全部同类 Widget,把别的玩法包 UI 也删了。
  • 异步加载完成时插件已经停用,没有检查 WeakThis 或 ActiveData。
  • Editor Data Validation 缺失,策划填空资源后运行时才报错。

源码依据

UGameFeatureAction 支持 context 版 Activating/Deactivating。Lyra 的多个自定义 Action 使用 TMap<FGameFeatureStateChangeContext, FPerContextData> 保存激活数据,并在 Deactivate 中 Reset。FGameFeatureDeactivatingContext 提供 PauseDeactivationUntilComplete 支持异步停用收尾。

源码路径索引

  • GameFeatures/Public/GameFeatureAction.h
  • GameFeatures/Public/GameFeaturesSubsystem.h
  • Samples/Games/Lyra/Source/LyraGame/GameFeatures/GameFeatureAction_WorldActionBase.h
  • Samples/Games/Lyra/Source/LyraGame/GameFeatures/GameFeatureAction_AddWidget.h
  • GameFeatures/Public/GameFeatureData.h