当前位置: 首页 > news >正文

如何在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+版本。

http://www.hskmm.com/?act=detail&tid=29153

相关文章:

  • 能连上 GitHub(SSH 验证成功),却 push 失败?常见原因与逐步解决方案 - 详解
  • 换根dp的一个trick
  • 搭建SSH服务于RK3399平台上的Ubuntu 18.04,实现远程连接
  • 深入探讨MySQL的二进制日志(binlog)选项
  • sparkml 多列共享labelEncoder - 详解
  • 一键解决MetaHuman播放动画时头部穿模问题
  • 忽然很好奇为什么素未谋面的大家都知道我是学姐?
  • UE网络编程完全指南:UDP TCP WebSocket实现详解
  • 配置Nginx服务器在Ubuntu平台上
  • 缓存一致性验证秘笈
  • 从十五岁的今天写给十六岁的明天
  • kali U盘启动持久化
  • 深入解析:Telerik UI for ASP.NET MVC 2025 Q3
  • Java依记 DAY02 - I
  • 元推理:汉字的发音,同音也是某种同构?
  • 题解:qoj7759 Permutation Counting 2
  • WAV 转 flac 格式
  • EtherCAT芯片没有倍福授权的风险
  • 为何是「对话式」智能体?因为人类本能丨对话式智能体专场,Convo AIRTE2025
  • 2014-2024高考真题考点分布详细分析(另附完整高考真题下载) - 详解
  • P4147 玉蟾宫(最大子矩形)
  • 2025 年 10 月西安房屋鉴定公司最新推荐排行榜:覆盖房屋安全评估、结构检测、承载力鉴定、危房鉴定领域,助您选专业机构
  • 完整教程:HAProxy 完整指南:简介、负载均衡原理与安装配置
  • K
  • 阿里发布「夸克 AI 眼镜」:融合阿里购物、地图、支付生态;苹果拟收购计算机视觉初创 Prompt AI丨日报
  • 在AI技术唾手可得的时代,挖掘新需求成为制胜关键——某知名AI聊天框架需求探索
  • 数论学习之路
  • 生成式AI实现多模态信息检索技术突破
  • 在运维工作中,如何过滤某个目录在那边什么路径下面?
  • 完整教程:安卓中,kotlin如何写app界面?