前言
在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);
实现思路:
- 定义一个只包含
None
和MAX
的空枚举框架 - 在引擎启动时,从配置读取枚举值
- 使用
UEnum::SetEnums()
动态填充None
和MAX
之间的内容 - 编辑器和蓝图自动识别新的枚举值
实战案例:从零实现动态枚举
以下是我在一个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+版本。