如何在UE中创建动态枚举

前言

在UE项目开发中,枚举(Enum)是最常用的数据类型之一。但传统的静态枚举有个致命问题:枚举值在编译时固定,无法根据配置动态调整

想象这样的场景:

  • 你的游戏有多个AI类型,需要在编辑器中配置,但不想每增加一个AI就重新编译代码
  • 你的关卡有多个检查点,想在策划配置表中添加,但枚举值是硬编码的
  • 你的道具系统需要从外部配置文件读取道具类型,但C++枚举无法动态扩展

这就是动态枚举要解决的问题——让枚举值可以运行时动态生成,同时保持类型安全和编辑器友好。

本文将介绍动态枚举的实现原理,分享我在实际项目中的解决方案,并介绍一个开箱即用的插件工具。


什么是动态枚举?

传统静态枚举的痛点

// C++静态枚举:编译时固定
UENUM(BlueprintType)
enum class EAIType : uint8
{
    None,
    Soldier,
    Archer,
    Mage,
    Tank,
    MAX
};

问题:

  • ❌ 新增AI类型必须修改代码并重新编译
  • ❌ 策划无法自主配置,依赖程序员
  • ❌ 多个项目无法复用同一套枚举定义
  • ❌ 每次调整都要等待漫长的编译时间

蓝图枚举的局限性

有人可能会想:"直接用蓝图枚举不就行了?策划可以在编辑器里随时添加值。"

确实,蓝图枚举可以在编辑器中配置,但有一个致命问题:

// ❌ 无法在C++中使用蓝图枚举
void SpawnAI(/* 这里无法引用蓝图枚举类型 */)
{
    // C++代码无法直接访问蓝图枚举
    // 只能通过字符串或反射间接操作,失去类型安全
}

蓝图枚举的限制:

  • C++无法引用 - 蓝图资产在编译时不存在,C++代码无法使用蓝图枚举作为类型
  • 失去类型安全 - 只能用字符串/整数间接操作,容易出错
  • 性能损失 - 需要运行时查找和转换,无法编译期优化
  • 代码可读性差 - C++中看不到枚举值,维护困难

适用场景:

  • ✅ 纯蓝图项目,完全不涉及C++逻辑
  • ❌ 需要C++和蓝图协同工作的项目(绝大多数商业项目)

动态枚举方案(本文方案)

// 动态枚举:运行时从配置生成
UENUM(BlueprintType)
enum class EAIType : uint8
{
    None,    // 占位符:表示无效值
    MAX      // 占位符:表示边界
    // 中间的枚举值从配置文件动态生成!
};

三种方案对比:

特性 C++静态枚举 蓝图枚举 动态枚举(本文)
策划可配置
C++中使用
蓝图中使用
类型安全
热更新
性能 最优 较差 最优

动态枚举的优势:

  • ✅ 策划可以在编辑器配置中自由添加枚举值
  • ✅ 无需重新编译,热更新枚举内容
  • C++和蓝图都能使用,保持类型安全
  • ✅ 配置表、存档、网络传输统一使用同一枚举
  • ✅ 编译期优化,性能与静态枚举相同

动态枚举的实现原理

UE的枚举反射系统

UE的枚举通过反射系统暴露,每个枚举都是一个UEnum对象。关键发现:UE允许在运行时修改枚举的内部数据

// 获取枚举的反射对象
UEnum* EnumPtr = StaticEnum<EAIType>();

// 枚举内部用一个数组存储所有值
TArray<TPair<FName, int64>> EnumNameArray;

// 核心API:可以动态替换整个枚举!
EnumPtr->SetEnums(EnumNameArray, ECppForm::Namespaced);

实现思路:

  1. 定义一个只包含NoneMAX的空枚举框架
  2. 在引擎启动时,从配置读取枚举值
  3. 使用UEnum::SetEnums()动态填充NoneMAX之间的内容
  4. 编辑器和蓝图自动识别新的枚举值

实战案例:从零实现动态枚举

以下是我在一个AI项目中的实现方案,代码简洁但功能完整。

第一步:定义枚举框架

// AIBlackboard.h
UENUM(BlueprintType)
enum class EAIFlowType : uint8
{
    None UMETA(DisplayName = "None")
};

UENUM(BlueprintType)
enum class EAIBlackboardBoolType : uint8
{
    None UMETA(DisplayName = "None")
};

只定义一个None,等待运行时填充。

第二步:创建配置类

// AIHumanSettings.h
UCLASS(config = AIHuman, defaultconfig, DisplayName = "AIHuman Settings")
class UAIHumanSettings : public UDeveloperSettings
{
    GENERATED_BODY()
public:
    UPROPERTY(config, EditAnywhere, BlueprintReadOnly, Category = "Flow")
    TArray<FString> AIFlowTypeEnum;

    UPROPERTY(config, EditAnywhere, BlueprintReadOnly, Category = "Blackboard")
    TArray<FString> AIBlackboardBoolTypeEnum;

    // ... 更多枚举配置
};

策划在项目设置中编辑这些数组,就能控制枚举值。

第三步:编写核心初始化函数

// AIHumanEngineSubsystem.h
static void DynamicInitEnum(UEnum* DynamicEnum, TArray<FString> EnumArray,
                           bool AddNone = true, bool AddMax = false)
{
    if(DynamicEnum == nullptr) return;

    TArray<TPair<FName, int64>> EnumNameArray;
    int64 CurEnumIndex = 0;

    // 添加None值
    if(AddNone)
    {
        EnumNameArray.Emplace(TPairInitializer<FName, int64>(
            FName(*DynamicEnum->GenerateFullEnumName(*FString("None"))),
            CurEnumIndex++));
    }

    // 添加配置中的枚举值
    for(auto It : EnumArray)
    {
        if(It.IsEmpty()) continue;
        EnumNameArray.Emplace(TPairInitializer<FName, int64>(
            FName(*DynamicEnum->GenerateFullEnumName(*It)),
            CurEnumIndex++));
    }

    // 添加Max值(可选)
    if(AddMax)
    {
        EnumNameArray.Emplace(TPairInitializer<FName, int64>(
            FName(*DynamicEnum->GenerateFullEnumName(*FString("Max"))),
            CurEnumIndex++));
    }

    // 替换枚举内容
    DynamicEnum->SetEnums(EnumNameArray, DynamicEnum->GetCppForm());
}

第四步:在引擎子系统中初始化

// AIHumanEngineSubsystem.cpp
void UAIHumanEngineSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);

    UAIHumanSettings* Settings = GetMutableDefault<UAIHumanSettings>();

    // 初始化所有动态枚举
    DynamicInitEnum(StaticEnum<EAIFlowType>(), Settings->AIFlowTypeEnum);
    DynamicInitEnum(StaticEnum<EAIBlackboardBoolType>(), Settings->AIBlackboardBoolTypeEnum);

    // 监听配置变化,编辑器中修改后立即生效
    Settings->OnSettingChanged().AddLambda([](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
    {
        UAIHumanSettings const* Settings = GetDefault<UAIHumanSettings>();

        if(PropertyChangedEvent.GetPropertyName() == "AIFlowTypeEnum")
        {
            DynamicInitEnum(StaticEnum<EAIFlowType>(), Settings->AIFlowTypeEnum);
        }
        // ... 其他枚举的热更新
    });
}

关键点:

  • 使用UEngineSubsystem而非EditorSubsystem,这样打包后也能正常工作
  • OnSettingChanged()监听配置变化,实现编辑器热更新

效果演示

策划在项目设置 -> AIHuman Settings中配置:

AIFlowTypeEnum:
  - "巡逻"
  - "追击"
  - "撤退"
  - "警戒"

保存后,枚举EAIFlowType自动变为:

enum class EAIFlowType : uint8
{
    None,
    巡逻,    // 动态生成
    追击,    // 动态生成
    撤退,    // 动态生成
    警戒,    // 动态生成
};

蓝图中的下拉框立即刷新,无需重启编辑器!


这个方案的局限性

虽然上述实现已经可用,但在实际项目中我发现了一些问题:

1. 样板代码太多

每增加一个枚举,都要:

  • 在配置类中加TArray<FString>属性
  • 在子系统初始化中调用DynamicInitEnum()
  • OnSettingChanged()中写重复的if判断

10个枚举就要写30行几乎相同的代码,容易出错且难维护。

2. 缺少健壮性处理

  • 没有去重:配置中写两个"巡逻"会生成重复枚举
  • 没有过滤空格:"Soldier"和"Soldier "被视为不同值
  • 没有边界检查:None/MAX可能被覆盖
  • 没有错误日志:出问题时难以排查

3. 不支持复杂场景

  • 无法自定义None/MAX的名称(比如使用Begin/End)
  • 无法将多个配置数组绑定到一个枚举的不同范围
  • 无法在运行时动态切换配置源

4. 跨项目复用困难

每个项目都要复制粘贴这些代码,而且版本不一致时难以同步更新。


开箱即用的解决方案

为了解决上述问题,我开发了SimpleAutoEnum | Fab插件,在保留原理的基础上,提供了工程级的完整方案。

核心特性

1. 一行宏完成绑定

// 传统方式:3个地方写代码
// 配置类 -> 手动调用初始化 -> 手动监听变化

// SimpleAutoEnum:一行搞定
SIMPLE_BIND_ENUM_TO_CONFIG(EAIType, None, MAX, UMySettings, AITypeList)

原理:

  • 宏展开后创建静态初始化器,自动在引擎启动时注册
  • 无需在子系统中手动编写任何代码
  • 使用匿名命名空间避免符号冲突

2. 完善的数据验证

// 自动去重
["Soldier", "Archer", "Soldier"] → ["Soldier", "Archer"]

// 过滤空值和空格
["", " ", "Mage "] → ["Mage"]

// 保护None/MAX
["None", "Soldier", "MAX"] → ["Soldier"]  // None和MAX不会被覆盖

3. 灵活的边界定义

// 标准模式
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
    None,
    MAX
};
SIMPLE_BIND_ENUM_TO_CONFIG(EWeaponType, None, MAX, UMySettings, WeaponList)

// 自定义边界
UENUM(BlueprintType)
enum class EQuestState : uint8
{
    Begin,
    End
};
SIMPLE_BIND_ENUM_TO_CONFIG(EQuestState, Begin, End, UMySettings, QuestList)

4. 完整的日志系统

LogSimpleAutoEnum: Log: Registered pending enum binding: EAIType -> UMySettings::AITypeList
LogSimpleAutoEnum: Log: Processing 3 pending enum bindings from static initialization
LogSimpleAutoEnum: Log: Processed enum binding: EAIType -> UMySettings::AITypeList
LogSimpleAutoEnum: Warning: Duplicate value found: "Soldier"

出问题时一目了然,不用猜。

使用对比

手动实现 vs SimpleAutoEnum

方面 手动实现 SimpleAutoEnum
代码量 每个枚举30行+ 每个枚举1行
去重/验证 需要自己写 内置完整验证
错误排查 无日志 详细日志
自定义边界 需改代码 宏参数指定
跨项目复用 复制粘贴 安装插件
维护成本 每个项目独立维护 插件统一更新

实际使用示例

插件支持三种常见的使用模式,满足不同场景需求。

示例1: 标准用法 - 一对一绑定

最常见的模式,一个枚举绑定一个配置数组。

// 1. 定义枚举框架
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
    None,
    MAX
};

// 2. 创建配置类
UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="Game Settings"))
class UMyGameSettings : public UDeveloperSettings
{
    GENERATED_BODY()
public:
    UPROPERTY(Config, EditAnywhere, Category="Weapons")
    TArray<FString> WeaponsList;
};

// 3. 绑定枚举
SIMPLE_BIND_ENUM_TO_CONFIG(EWeaponType, None, MAX, UMyGameSettings, WeaponsList)

配置:

WeaponsList: ["剑", "斧", "弓", "法杖"]

结果:

EWeaponType::None, 剑, 斧, 弓, 法杖, EWeaponType::MAX

示例2: 范围绑定 - 多数组组合

将多个配置数组绑定到一个枚举的不同范围,适合分类管理。

// 定义带有多个边界的枚举
UENUM(BlueprintType)
enum class ECombinedWeapons : uint8
{
    None            UMETA(DisplayName = "None"),
    PrimaryEnd      UMETA(DisplayName = "--- Primary End ---"),
    AllWeaponsEnd   UMETA(DisplayName = "--- All End ---")
};

// 绑定主武器到第一个范围
SIMPLE_BIND_ENUM_TO_CONFIG(ECombinedWeapons, None, PrimaryEnd, UMyGameSettings, PrimaryWeaponsList)
// 绑定副武器到第二个范围
SIMPLE_BIND_ENUM_TO_CONFIG(ECombinedWeapons, PrimaryEnd, AllWeaponsEnd, UMyGameSettings, SecondaryWeaponsList)

配置:

PrimaryWeaponsList: ["长剑", "战斧"]
SecondaryWeaponsList: ["匕首", "手枪"]

结果:

None, 长剑, 战斧, PrimaryEnd, 匕首, 手枪, AllWeaponsEnd

适用场景:

  • 武器系统(主武器/副武器/特殊武器)
  • 技能分类(主动技能/被动技能/终极技能)
  • 物品分类(消耗品/装备/材料)

示例3: 共享数组 - 多枚举共用

多个枚举使用同一个配置数组,保证分类一致性。

// 品质等级枚举(用于UI显示)
UENUM(BlueprintType)
enum class EUIQuality : uint8
{
    None,
    Max
};
SIMPLE_BIND_ENUM_TO_CONFIG(EUIQuality, None, Max, UMyGameSettings, QualityLevelsArray)

// 物品品质枚举(用于道具系统)
UENUM(BlueprintType)
enum class EItemQuality : uint8
{
    Begin,
    End
};
// 使用同一个配置数组,保证UI和物品系统的品质分类一致
SIMPLE_BIND_ENUM_TO_CONFIG(EItemQuality, Begin, End, UMyGameSettings, QualityLevelsArray)

配置:

QualityLevelsArray: ["普通", "优秀", "稀有", "史诗", "传说"]

结果:

// EUIQuality枚举:
None, 普通, 优秀, 稀有, 史诗, 传说, Max

// EItemQuality枚举:
Begin, 普通, 优秀, 稀有, 史诗, 传说, End

// 两个枚举的索引值完全对应
// EUIQuality::普通 (索引1) 和 EItemQuality::普通 (索引1)
// EUIQuality::传说 (索引5) 和 EItemQuality::传说 (索引5)

适用场景:

  • 颜色等级(UI/物品/特效统一配色)
  • 稀有度等级(装备/道具/怪物统一分级)
  • 难度等级(关卡/任务/Boss统一难度)

支持版本:

  • UE 5.2 +
  • Windows / Mac / Linux
  • 支持打包运行

总结

动态枚举解决了传统静态枚举的核心痛点:配置驱动 vs 代码硬编码

核心价值:

  • 策划自主配置,无需程序员介入
  • 编辑器实时更新,提升迭代效率
  • 保持类型安全,避免硬编码字符串
  • 跨蓝图/C++/配置统一使用

本文从原理到实战,演示了如何从零实现动态枚举,并分享了工程化的插件方案。对于需要大量配置化枚举的项目,SimpleAutoEnum可以显著减少样板代码,提升开发体验。


技术交流与反馈:

如果在使用中遇到问题,欢迎通过评论区或者邮箱联系我

📧 邮箱: mengzhishanghun@outlook.com


本文技术方案已在多个商业项目中验证,SimpleAutoEnum插件适用于UE 5.2+版本。

posted @ 2025-10-12 10:33  mengzhishanghun  阅读(13)  评论(0)    收藏  举报