因为游戏引擎核心诉求是启动快、内存省、编译可控,通用反射库如Boost.Hana或RTTR会拖慢编译、增大体积且无法与序列化/编辑器深度对齐;手写宏方案可精确控制暴露类、字段序列化逻辑及编辑器支持,只生成所需代码,无运行时类型查找开销。

为什么不用现成的反射库,而要手写宏方案
游戏引擎里加反射,核心诉求不是功能全,而是启动快、内存省、编译可控。像 Boost.Hana 或 RTTR 这类通用库会拖慢编译、增加二进制体积,且无法和引擎的序列化/编辑器系统深度对齐。手写宏方案能精确控制哪些类暴露、字段怎么序列化、是否支持编辑器修改——关键在于“只生成需要的代码”,不带 runtime 类型查找开销。
REFLECT_CLASS 和 REFLECT_FIELD 宏怎么组织结构
主流做法是用两层宏:外层声明反射元信息,内层展开为静态成员函数或结构体。典型结构如下:
#define REFLECT_FIELD(name) \
{ #name, offsetof(ClassName, name), sizeof(ClassName::name), &ClassName::name }
<h1>define REFLECT_CLASS(cls) \</h1><p>template <> struct ReflectInfo<cls> { \
static constexpr const char* name = #cls; \
static constexpr std::array fields = { REFLECT_FIELD(x), REFLECT_FIELD(y) }; \
};</p>注意三点:
-
offsetof要求类必须是标准布局(std::is_standard_layout_v<T>),不能有虚函数、虚基类、非公有继承 - 字段名字符串用
#name,不是运行时拼接,避免 string 对象构造开销 - 别在宏里写
std::vector或动态分配——所有元数据必须是constexpr可求值的,否则链接期会出错
如何让反射支持编辑器属性(比如 [ReadOnly]、[Range(0,100)])
纯 C++17 不支持属性语法,但可以用宏模拟注解。关键是把修饰符编码进字段元数据结构:
立即学习“C++免费学习笔记(深入)”;
#define REFLECT_FIELD_RANGE(name, min, max) \
{ #name, offsetof(C, name), sizeof(C::name), &C::name, \
ReflectFieldFlags::HasRange, min, max }
<h1>define REFLECT_FIELD_READONLY(name) \</h1><p>{ #name, offsetof(C, name), sizeof(C::name), &C::name, \
ReflectFieldFlags::ReadOnly }</p>这样每个 ReflectField 结构体多两个 float 成员(用于 range)和一个 flag 枚举。编辑器读取时根据 flag 分支处理,不需要额外解析字符串。常见坑:
- 宏参数里传表达式(如
[Range(0, TIME_MAX)])会导致宏展开失败,建议只接受字面量 - 不同修饰符宏不能混用(比如同时用
REFLECT_FIELD_READONLY和REFLECT_FIELD_RANGE),得统一收口到一个宏里做参数重载 - Unity/Unreal 风格的
[Tooltip("xxx")]必须存为const char*,不能用std::string,否则破坏constexpr
宏反射怎么和序列化系统联动(比如保存/加载 JSON)
反射本身不负责序列化逻辑,它只提供“字段名→内存偏移→类型大小→访问地址”的映射。真正干活的是通用序列化函数:
template<typename T>
void SerializeJSON(json& j, const T& obj) {
for (const auto& f : ReflectInfo<T>::fields) {
auto* ptr = reinterpret_cast<const char*>(&obj) + f.offset;
if (f.type_size == sizeof(int)) {
j[f.name] = *reinterpret_cast<const int*>(ptr);
} else if (f.type_size == sizeof(float)) {
j[f.name] = *reinterpret_cast<const float*>(ptr);
}
}
}
这里的关键约束:
- 必须按
type_size匹配原始类型,不能靠typeid——那是 runtime 反射,违背轻量目标 - 指针字段(
int*)、STL 容器(std::vector<int>)无法自动处理,得手动特化SerializeJSON模板或加字段标记跳过 - JSON 键名用
f.name(即宏里的#name),确保和编辑器显示一致;如果字段名含下划线(如m_health),建议在宏里额外加别名字段,而不是运行时 trim
最易被忽略的是字段对齐:结构体 padding 会让 offsetof 正确,但跨平台时需确认编译器 pack 设置(如 #pragma pack(4))是否全局一致,否则序列化出来的二进制在不同平台读取会错位。



















