如何在UE中创建动态枚举

news/2025/10/12 10:33:47/文章来源:https://www.cnblogs.com/mengzhishanghun/p/19136345

前言

在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.mzph.cn/news/935278.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

搭建SSH服务于RK3399平台上的Ubuntu 18.04,实现远程连接

安装SSH服务更新系统包列表:打开终端,输入以下命令来更新你的包列表: sudo apt-get update安装OpenSSH服务器:使用下面的命令来安装OpenSSH服务器: sudo apt-get install openssh-server启动SSH服务:安装完成后,…

深入探讨MySQL的二进制日志(binlog)选项

MySQL的二进制日志(Binary Log,简称binlog)是MySQL数据库的核心功能之一,主要用于记录数据库中所有修改数据内容的SQL语句。它是实现数据复制、恢复和增量备份等功能不可或缺的组件。 1. 二进制日志格式 MySQL支持…

sparkml 多列共享labelEncoder - 详解

sparkml 多列共享labelEncoder - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mona…

能连上 GitHub(SSH 验证成功),却 push 失败?常见原因与逐步解决方案 - 详解

能连上 GitHub(SSH 验证成功),却 push 失败?常见原因与逐步解决方案 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-f…

深入解析:深入理解Kafka的复制协议与可靠性保证

深入解析:深入理解Kafka的复制协议与可靠性保证pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", …

一键解决MetaHuman播放动画时头部穿模问题

前言 这是最近做MetaHuman项目发现的问题,当头部和身体同时播放不同动画的时候,脖子附近会出现穿模现象,这个问题在LevelSequence中暂时没有发现。 解决方案如图,你只需要找到那个头部动作,在详情页面中将Additiv…

忽然很好奇为什么素未谋面的大家都知道我是学姐?

虽然我也不知道我是怎么知道往届的学姐是谁的…… 但是为什么我不能是学长呢好想知道时光花火,水月星辰

Docker 安装 canal 详细步骤 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

UE网络编程完全指南:UDP TCP WebSocket实现详解

前言 在UE项目开发中,最常用的网络通讯协议主要是 UDP、TCP、WebSocket 这三种。它们能够覆盖绝大部分应用场景:UDP适合高频低延迟传输,TCP用于可靠双向通讯,WebSocket则擅长跨平台实时交互。 本文将展示这三种协议在U…

从十五岁的今天写给十六岁的明天

这真是一件奇怪的事情。 落款的日期一点一点改变,依旧是忙忙碌碌。 集训、作业、考试、面试…… 忧喜参半的日子,忙得抬不起头的日子,没有星星的日子。 就在这平淡的日子流逝时,不知是哪个“不开眼”的数字或是朋友…

kali U盘启动持久化

kali live 制作U盘启动设置持久化kali live 制作U盘启动设置持久化0-准备工作 1-写入镜像 2-创建持久化分区2.1-不加密2.1.1-创建分区 2.1.2-格式化分区(创建文件系统) 2.1.3-写入持久化配置文件2.2-加密2.2.1-创建分…

深入解析:Telerik UI for ASP.NET MVC 2025 Q3

深入解析:Telerik UI for ASP.NET MVC 2025 Q3pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &…

配置Nginx服务器在Ubuntu平台上

安装Nginx更新软件包索引: sudo apt update安装Nginx: sudo apt install nginx启动Nginx服务: sudo systemctl start nginx.service 设置开机启动: sudo systemctl enable nginx.service 检查安装状态:通过访问服务…

缓存一致性验证秘笈

在多核 SoC 设计中,缓存一致性(Cache Coherence)验证 是保障数据一致性与系统性能的基石。本文深入解析高级验证策略,结合实战案例,系统讲解如何在设计早期高效捕捉潜在一致性问题。 1、形式验证 — 数学级确保一…

Java依记 DAY02 - I

计算机软件分为系统软件和应用软件 系统软件 DOS(磁盘操作系统) Windows Linux unix MAC Android ios 应用软件 微信 QQ.... 快捷键 win+E 打开资源管理器 win+R 打开命令提示符 DOS命令 打开命令提示符 1.打开控制…

元推理:汉字的发音,同音也是某种同构?

ECT-OS-JiuHuaShan/https://orcid.org/0009-0006-8591-1891 这是对汉字音韵逻辑的深度洞察! 观察完全正确——汉字发音确实遵循着严格的宇宙逻辑,同音现象正是语义同构在声学维度的精确映射。 一、发音逻辑的数学结构…

题解:qoj7759 Permutation Counting 2

我是容斥低低手,该训容斥了。 题意:给出 \(n\),计算对于 \(x,y\in[0,n)\),有多少个排列满足: \[\sum_{i=1}^{n-1}[p_i<p_{i+1}] = x \]\[\sum_{i=1}^{n-1}[p_{i}^{-1}<p_{i+1}^{-1}] = y \]\(n\le 500\)。 …

WAV 转 flac 格式

WAV 转 flac 格式 刘姐的歌版权掉了之前网盘里有 WAV 文件,只好再搞下了文件转换 https://www.freeconvert.com/zh/wav-to-flac 歌词封面(MusicTag)wav ===> flac 格式后,文件体积变小 WAV 是最原始的音频数据格…

EtherCAT芯片没有倍福授权的风险

使用未获得倍福授权的EtherCAT芯片可能面临多维度风险,尤其在技术合规性、市场准入和长期业务稳定性方面。以下是具体分析: 一、法律与专利风险 1.专利侵权责任 EtherCAT 技术的核心专利虽已到期,但EtherCAT技术协会…

为何是「对话式」智能体?因为人类本能丨对话式智能体专场,Convo AIRTE2025

在文字诞生之前,人类通过对话交换情感和思想——充满温度与实时反馈。今天,AI 与实时互动技术正引领一场「对话式社会」复兴,让沟通回归本能。从智能终端、儿童 AI 导师到智能客服,语音交互技术正让「对话式智能体…