Unreal Engine FSelfRegisteringExec 完全指南
概述
FSelfRegisteringExec 是 Unreal Engine 提供的一个自注册控制台命令处理器基类。它允许开发者通过继承该类来创建自定义的控制台命令,无需手动注册到任何管理器中——只需创建一个静态实例,命令就会自动生效。
核心特性
| 特性 |
说明 |
| 自动注册 |
构造函数中自动将实例添加到全局注册表 |
| 自动注销 |
析构函数中自动从全局注册表移除 |
| 线程安全 |
使用 FCriticalSection 保护注册表操作 |
| 多级执行 |
支持 Runtime、Dev、Editor 三种执行级别 |
| 零配置 |
无需修改其他代码,只需定义类和静态实例 |
类层次结构
FExec (基类)
│
└── FSelfRegisteringExec (自注册执行器)
│
├── FStaticSelfRegisteringExec (静态函数包装器)
│
├── FStaticSelfRegisteringExec_Dev (Dev 模式静态函数包装器)
│
└── FStaticSelfRegisteringExec_Editor (Editor 模式静态函数包装器)
源码位置
| 文件 |
说明 |
Engine/Source/Runtime/Core/Public/Misc/Exec.h |
FExec 基类定义 |
Engine/Source/Runtime/Core/Public/Misc/CoreMisc.h |
FSelfRegisteringExec 及其派生类声明 |
Engine/Source/Runtime/Core/Private/Misc/CoreMisc.cpp |
实现代码 |
FExec 基类
FSelfRegisteringExec 继承自 FExec,了解基类的设计对理解整个机制至关重要。
FExec 定义 (Exec.h)
cpp
class FExec
{
public:
CORE_API virtual ~FExec();
#if UE_ALLOW_EXEC_COMMANDS
CORE_API virtual bool Exec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar );
#else
CORE_API virtual bool Exec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) final;
CORE_API virtual bool Exec( UWorld* InWorld, const TCHAR* Cmd ) final;
#endif
protected:
virtual bool Exec_Runtime( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) { return false; }
virtual bool Exec_Dev( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) { return false; }
virtual bool Exec_Editor( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar ) { return false; }
};
三种执行级别
| 方法 |
调用条件 |
典型用途 |
Exec_Runtime() |
UE_ALLOW_EXEC_COMMANDS 为 true 时 |
所有构建版本都需要的命令 |
Exec_Dev() |
非 Shipping 构建 |
开发调试命令 |
Exec_Editor() |
仅编辑器模式 |
编辑器专用工具命令 |
Exec() vs Exec_Runtime() 的区别
Exec(): 基类的公共入口方法,内部会根据构建类型依次调用 Exec_Runtime()、Exec_Dev()、Exec_Editor()
Exec_Runtime(): 受保护的虚函数,在所有允许执行命令的构建版本中被调用
如果直接重写 Exec(),则绕过了执行级别的自动分发机制。大多数情况下应该重写 Exec_Runtime()、Exec_Dev() 或 Exec_Editor()。
编译条件宏
cpp
#ifndef UE_ALLOW_EXEC_COMMANDS
#if UE_BUILD_SHIPPING && !WITH_EDITOR
#define UE_ALLOW_EXEC_COMMANDS UE_ALLOW_EXEC_COMMANDS_IN_SHIPPING
#else
#define UE_ALLOW_EXEC_COMMANDS 1
#endif
#endif
#ifndef UE_ALLOW_EXEC_DEV
#define UE_ALLOW_EXEC_DEV !UE_BUILD_SHIPPING && UE_ALLOW_EXEC_COMMANDS
#endif
#ifndef UE_ALLOW_EXEC_EDITOR
#define UE_ALLOW_EXEC_EDITOR WITH_EDITOR && UE_ALLOW_EXEC_COMMANDS
#endif
FSelfRegisteringExec 详解
类定义 (CoreMisc.h)
cpp
class FSelfRegisteringExec : public FExec
{
public:
CORE_API FSelfRegisteringExec();
CORE_API virtual ~FSelfRegisteringExec();
static CORE_API bool StaticExec( UWorld* Inworld, const TCHAR* Cmd, FOutputDevice& Ar );
};
实现原理 (CoreMisc.cpp)
cpp
using FSelfRegisteredExecArray = TArray<FSelfRegisteringExec*, TInlineAllocator<8>>;
FCriticalSection* GetExecRegistryLock()
{
static FCriticalSection ExecRegistryLock;
return &ExecRegistryLock;
}
FSelfRegisteredExecArray& GetExecRegistry()
{
static FSelfRegisteredExecArray Execs;
return Execs;
}
FSelfRegisteringExec::FSelfRegisteringExec()
{
FScopeLock ScopeLock(GetExecRegistryLock());
GetExecRegistry().Add( this );
}
FSelfRegisteringExec::~FSelfRegisteringExec()
{
FScopeLock ScopeLock(GetExecRegistryLock());
verify(GetExecRegistry().Remove( this ) == 1 );
}
bool FSelfRegisteringExec::StaticExec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar )
{
FScopeLock ScopeLock(GetExecRegistryLock());
for (FSelfRegisteringExec* Exe : GetExecRegistry())
{
if (Exe->Exec( InWorld, Cmd, Ar ))
{
return true;
}
}
return false;
}
工作流程
1. 静态实例创建
│
▼
2. 构造函数调用
│
▼
3. 加锁 (FScopeLock)
│
▼
4. 添加到 GetExecRegistry()
│
▼
5. 解锁
│
▼
6. 命令执行时:
StaticExec() 遍历所有注册的执行器
│
▼
7. 调用每个执行器的 Exec() 方法
│
▼
8. 如果返回 true,停止遍历
使用方法
方法 1: 继承类 + 静态实例(推荐)
这是最常用的方式,适合需要复杂逻辑的命令。
cpp
class FMyCustomExecHelper : public FSelfRegisteringExec
{
virtual bool Exec_Editor(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override
{
if (FParse::Command(&Cmd, TEXT("MYCOMMAND")))
{
Ar.Log(TEXT("MyCommand executed!"));
return true;
}
return false;
}
};
static FMyCustomExecHelper GMyCustomExecHelper;
方法 2: 使用静态函数包装器
适合简单命令,避免创建新类。
cpp
static bool MyExecFunction(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
{
if (FParse::Command(&Cmd, TEXT("MYCOMMAND")))
{
Ar.Log(TEXT("MyCommand executed!"));
return true;
}
return false;
}
static FStaticSelfRegisteringExec GMyExec(MyExecFunction);
static FStaticSelfRegisteringExec_Dev GMyExec(MyExecFunction);
static FStaticSelfRegisteringExec_Editor GMyExec(MyExecFunction);
选择正确的执行级别
| 场景 |
推荐方法 |
| 编辑器专用调试工具 |
Exec_Editor() 或 FStaticSelfRegisteringExec_Editor |
| 开发期间调试命令 |
Exec_Dev() 或 FStaticSelfRegisteringExec_Dev |
| 所有构建版本都需要 |
Exec_Runtime() 或 FStaticSelfRegisteringExec |
实际案例分析
案例 1: Find In Blueprint 索引缓存导出
cpp
class FFiBDumpIndexCacheToFileExecHelper : public FSelfRegisteringExec
{
virtual bool Exec_Editor(class UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
{
if (FParse::Command(&Cmd, TEXT("DUMPFIBINDEXCACHE")))
{
FString FileLocation = FPaths::ConvertRelativePathToFull(
FPaths::ProjectIntermediateDir() + TEXT("BlueprintSearchTools"));
FString FullPath = FString::Printf(TEXT("%s/FullBlueprintIndexCacheDump.csv"), *FileLocation);
FArchive* DumpFile = IFileManager::Get().CreateFileWriter(*FullPath);
if (DumpFile)
{
FFindInBlueprintSearchManager::Get().DumpCache(*DumpFile);
DumpFile->Close();
delete DumpFile;
UE_LOG(LogFindInBlueprint, Log, TEXT("Wrote full index cache to %s"), *FullPath);
}
return true;
}
return false;
}
};
static FFiBDumpIndexCacheToFileExecHelper GFiBDumpIndexCacheToFileExec;
使用方式: 在编辑器控制台输入 DUMPFIBINDEXCACHE
案例 2: D3D11 状态缓存切换
cpp
#if D3D11_ALLOW_STATE_CACHE && D3D11_STATE_CACHE_RUNTIME_TOGGLE
bool GD3D11SkipStateCaching = false;
class FD3D11ToggleStateCacheExecHelper : public FSelfRegisteringExec
{
virtual bool Exec( class UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar )
{
if (FParse::Command(&Cmd, TEXT("TOGGLESTATECACHE")))
{
GD3D11SkipStateCaching = !GD3D11SkipStateCaching;
Ar.Log(FString::Printf(TEXT("D3D11 State Caching: %s"),
GD3D11SkipStateCaching ? TEXT("OFF") : TEXT("ON")));
return true;
}
return false;
}
};
static FD3D11ToggleStateCacheExecHelper GD3D11ToggleStateCacheExecHelper;
#endif
使用方式: 在控制台输入 TOGGLESTATECACHE
注意: 此案例直接重写 Exec() 而非 Exec_Runtime(),因为它需要在所有允许执行命令的构建版本中生效。
案例 3: Vulkan 表面模拟
cpp
#if !UE_BUILD_SHIPPING
bool GSimulateLostSurfaceInNextTick = false;
bool GSimulateSuboptimalSurfaceInNextTick = false;
class FVulkanCommandsHelper : public FSelfRegisteringExec
{
virtual bool Exec_Dev(class UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
{
if (FParse::Command(&Cmd, TEXT("VULKAN_SIMULATE_LOST_SURFACE")))
{
GSimulateLostSurfaceInNextTick = true;
Ar.Log(FString::Printf(TEXT("Vulkan: simulating lost surface next frame")));
return true;
}
else if (FParse::Command(&Cmd, TEXT("VULKAN_SIMULATE_SUBOPTIMAL_SURFACE")))
{
GSimulateSuboptimalSurfaceInNextTick = true;
Ar.Log(FString::Printf(TEXT("Vulkan: simulating suboptimal surface next frame")));
return true;
}
else
{
return false;
}
}
};
static FVulkanCommandsHelper GVulkanCommandsHelper;
#endif
使用方式: 在控制台输入 VULKAN_SIMULATE_LOST_SURFACE 或 VULKAN_SIMULATE_SUBOPTIMAL_SURFACE
命令解析工具
FParse 常用方法
cpp
if (FParse::Command(&Cmd, TEXT("MYCOMMAND")))
{
}
FString StringParam;
if (FParse::Value(Cmd, TEXT("NAME="), StringParam))
{
}
int32 IntParam;
if (FParse::Value(Cmd, TEXT("COUNT="), IntParam))
{
}
if (FParse::Param(Cmd, TEXT("VERBOSE")))
{
}
完整参数解析示例
cpp
class FMyAdvancedExecHelper : public FSelfRegisteringExec
{
virtual bool Exec_Editor(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override
{
if (FParse::Command(&Cmd, TEXT("EXPORTDATA")))
{
FString OutputPath;
FParse::Value(Cmd, TEXT("PATH="), OutputPath);
int32 MaxItems = 100;
FParse::Value(Cmd, TEXT("MAX="), MaxItems);
bool bVerbose = FParse::Param(Cmd, TEXT("VERBOSE"));
Ar.Logf(TEXT("Exporting to: %s, Max: %d, Verbose: %s"),
*OutputPath, MaxItems, bVerbose ? TEXT("Yes") : TEXT("No"));
return true;
}
return false;
}
};
static FMyAdvancedExecHelper GMyAdvancedExecHelper;
使用方式: EXPORTDATA PATH="C:/Output" MAX=500 VERBOSE
与其他命令系统的比较
| 特性 |
FSelfRegisteringExec |
IConsoleCommand |
FAutoConsoleCommand |
| 注册方式 |
自动(静态实例) |
手动注册 |
自动(静态实例) |
| 命令格式 |
自定义解析 |
标准控制台变量格式 |
标准控制台变量格式 |
| 参数处理 |
完全自定义 |
框架处理 |
框架处理 |
| 适用场景 |
复杂自定义命令 |
简单变量/命令 |
简单变量/命令 |
| 执行级别控制 |
支持 Runtime/Dev/Editor |
通过 ECVF 标志 |
通过 ECVF 标志 |
最佳实践
1. 命名规范
cpp
class FMyFeatureExecHelper : public FSelfRegisteringExec { ... };
static FMyFeatureExecHelper GMyFeatureExecHelper;
if (FParse::Command(&Cmd, TEXT("MY_FEATURE_COMMAND")))
2. 日志输出
cpp
Ar.Log(TEXT("Command executed successfully"));
Ar.Logf(TEXT("Processed %d items"), Count);
UE_LOG(LogMyCategory, Log, TEXT("Command executed: %s"), *CommandName);
3. 错误处理
cpp
virtual bool Exec_Editor(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override
{
if (FParse::Command(&Cmd, TEXT("MYCOMMAND")))
{
FString RequiredParam;
if (!FParse::Value(Cmd, TEXT("REQUIRED="), RequiredParam))
{
Ar.Log(TEXT("Error: REQUIRED parameter is missing"));
Ar.Log(TEXT("Usage: MYCOMMAND REQUIRED=<value> [OPTIONAL=<value>]"));
return true;
}
return true;
}
return false;
}
4. 条件编译
cpp
#if WITH_EDITOR
class FEditorOnlyExecHelper : public FSelfRegisteringExec
{
virtual bool Exec_Editor(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override
{
return false;
}
};
static FEditorOnlyExecHelper GEditorOnlyExecHelper;
#endif
注意事项
-
不要用于 UObject: 官方注释明确指出 "Not intended for use with UObjects!"
-
静态实例生命周期: 静态实例在模块加载时创建,模块卸载时销毁
-
线程安全: 虽然注册表操作是线程安全的,但命令执行逻辑需要自行保证线程安全
-
返回值语义:
- 返回
true: 命令已处理,停止继续查找
- 返回
false: 命令未处理,继续查找其他处理器
-
命令冲突: 如果多个处理器响应同一命令,只有第一个返回 true 的会生效
总结
FSelfRegisteringExec 是 Unreal Engine 中一个优雅的自注册命令系统,通过 C++ 的静态初始化机制实现零配置的命令注册。它特别适合:
- 模块级别的调试命令
- 不依赖 UObject 系统的工具命令
- 需要精细控制执行级别的命令
- 需要完全自定义参数解析的复杂命令
通过继承 FSelfRegisteringExec 并创建静态实例,开发者可以快速添加自定义控制台命令,无需修改任何其他代码。