SSmallOrange

微风需要竹林,溪流需要蜻蜓

现代反射实现(四):Json反序列化

第四章:基于反射的 Json 反序列化 前一章我们已经用编译期反射实现了 Json 序列化,把一个聚合类型一次性吐成一段 Json 字符串,不需要手写拼接。 这章做反方向的事情: 给定一段 Json 文本,把它自动填充进一个结构体对象里,支持嵌套结构体、顺序容器、关联容器等常见组合。 最终对外只保留一个接口: 1 2 3 4 5 6 7 8 tinyrefl::Complex obj{}; const char* json = R"({ "id": 42, "name": "tinyrefl", "values": [1, 2, 3] })"; auto st = tinyrefl::reflection_from_json(obj, json); 这里我采用不构建 DOM,直接基于RapidJSON的流式 SAX 接口,把事件派发到对应的结构体成员。 主要思路: 由于RapidJson的SAX接口只提供回调,所以对于递归反序列化,还需要我们自己维护一个解析栈来处理嵌套情况 同时,RapidJson对于数组类型和普通类型的回调方式不同,所以这里需要分别进行处理 剩下的就是使用RapidJson的接口了,RapidJson相关文档:https://rapidjson.org/zh-cn/ 结构体成员KV化 到反序列化这里,前面的铺垫已经很充分了: members_count_v<T> 能在编译期算出成员个数 struct_members_to_array<T>() 能拿到成员名数组 struct_members_to_tuple<T>() 能拿到成员引用 tuple(在有对象实例的前提下) 序列化的时候,我们只需要在“有对象”的场景下遍历一遍成员,把 (member_name, member_reference)这一对在运行时交给 to_json_value 就行,所以直接用 tuple + 泛型 lambda 遍历就够了。 反序列化就不一样了,真正要解决的是下面这个问题: 当 RapidJSON 回调告诉我「现在读到了 key = ‘id’」,如何在 O(1) 时间里找到这个成员在结构体里的“位置”和“类型”,然后把后续的值事件填到正确的成员上? 也就是说,我们需要把“结构体的成员元信息”预先组织成某种 Key → Value 的形式: ...

2025-11-15   6分钟   SSmallOrange

Hazel学习笔记:AssetsManager

Hazel学习笔记:AssetsManager 新增功能如下: 新增 Asset 类型,将 Texture、Shader、Scene都继承于 Asset 对象,用于 AssetsManager管理 AssetManager区分:MemoryAsset 和 LoadedAsset 区分临时内存资源和磁盘资源 GetAsset 采用懒加载策略 Hazel学习笔记:Editor2D Phase 0: 新增功能如下: EditorCommandHistory.h Undo、Redo实现 ctrl + z 操作撤销、回撤 IEditorCommand.h 封装任务 EditorContext.h 编辑器所需要的上下文 EditorDirtyTracker.h 编辑器状态追踪 EditorSelectionContext.h 封装当前选择上下文 EditorViewportSettings.h 视口参数设置 拆分 EditorLayer 功能 面板之间解耦 Phase 1: 将 Viewport 能力抽象,区分展示和 渲染,将 Overlay 渲染 和 场景渲染 封装成管线 处理。 新增 EditorViewportPanel 负责视口部分工作 添加选中实体边框(GetWorldTransform)、世界原点绘制 Phase 2: 新增世界网格绘制 网格支持动态裁剪、动态密度判断、主次网格分层 渲染顺序: Background Overlay Pass —— 网格、原点(位于场景内容下方) Scene Pass —— 场景内容 + 写入实体 ID 附件 Foreground Overlay Pass —— 选中框、碰撞体等(覆盖在场景内容上方)

2026-04-15   1分钟   SSmallOrange

Hazel学习笔记:编辑器

Hazel学习笔记:编辑器 经过一段时间的挣扎,终于决定要向完整的3D编辑器出发了,还是Hazel游戏引擎的框架进行完善,对于游戏脚本仍然沿用之前的 Lua 脚本,对于后端仍然保留OpenGL作为后端图形API,以此两点作为与原Hazel引擎的不同之处。 本篇内容不涉及ImGUI使用教程。 新增功能如下: UUID生成器、UUID组件 新增组件之间父子关系,子只保存对父的相对坐标 对每个组件而言,当父子关系发生变化时,坐标也要相应的进行变化 编辑器窗口 视图窗口(Viewport)、属性窗口(Property)、组件树、窗口支持拖拽 独立的Camera(EditorCamera) 鼠标拾取 引入 ImGui 的 ImGuizmo,使得实体支持手柄拖动修改(QWER:关闭平移旋转缩放) 引入 Line、Circle 绘制,增强编辑器 Debug 能力 添加Scene拷贝能力,用来支持播放、暂停功能。 添加 Project,定制项目文件格式,统一管理脚本、资源、场景文件 父子关系 以场景为单位,在场景中新增实体时,为每个实体附加一个UUID组件和关系组件: 1 2 3 4 5 6 7 8 9 Entity Scene::CreateEntityWithUUID(UUID uuid, const std::string& name) { Entity entity = { m_Registry.create(), this }; entity.AddComponent<IDComponent>().ID = uuid; // UUID entity.AddComponent<RelationshipComponent>(); // Relationship // ... m_EntityIDMap[uuid] = entity.m_EntityHandle; // uuid 与组件映射 return entity; } 定义常用组件接口: 查找: 1 2 3 4 5 6 Entity Scene::FindEntityByUUID(UUID uuid) { if (auto it = m_EntityIDMap.find(uuid); it != m_EntityIDMap.end()) return Entity{ it->second, this }; return Entity{}; } 实体销毁: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void Scene::DestroyEntity(Entity entity) { // 递归销毁子实体 if (entity.HasComponent<RelationshipComponent>()) { auto children = entity.Children(); for (auto childId : children) { Entity child = FindEntityByUUID(childId); if (child) DestroyEntity(child); } // 移除自己 Entity parent = entity.GetParent(); if (parent) parent.RemoveChild(entity); } // 从 UUID 映射中移除 if (entity.HasComponent<IDComponent>()) m_EntityIDMap.erase(entity.GetComponent<IDComponent>().ID); m_Registry.destroy(entity.m_EntityHandle); } 取消实体的父: ...

2026-04-05   2分钟   SSmallOrange

Qt-MetaObject&SignalSlot

Qt 元对象系统 & 信号槽机制 头文件扫描 → moc 代码生成 → 生成内容解析 → 运行时信号槽调度 一、整体架构:为什么需要 moc? C++ 本身 不支持运行时反射。Qt 为了实现信号槽、属性系统、动态调用等能力,需要在 C++ 之上添加一层"元信息"。这层信息由 moc(Meta-Object Compiler) 在编译前自动生成。 Tip ...

2026-03-23   9分钟   SSmallOrange

Sol2:在C++中调用Lua脚本

Sol2:在C++中调用Lua脚本 本文档记录了在 C++ 代码中使用 sol2 嵌入 Lua 脚本时的一些个人理解。 向Lua中注册自定义用户类型 先看一段代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sol::state lua; lua.new_usertype<Type>("TypeName", // 类型构造函数 sol::constructors<ConstructFunc1, ConstructFunc2, ...> // 或使用:sol::no_constructor 来禁用 TypeName.new() // 成员变量 "member1", Type.valueptr1, "member2", Type.valueptr2, // 元方法 sol::meta_function::addition, AddFun1, sol::meta_function::substraction, SubFun1, sol::meta_function::multiplication, MultiplicationFun1, // 成员方法 "memberFunc1", MemberFunc1(), "memberFunc2", [](Type& self, /* args... */) { // 对应成员方法的第一个参数 // func... }, ); 这段代码用来向lua脚本中注册一个c++类型,支持一些基本的元方法重载(比如四则运算、比大小、迭代(pair或ipair)等)。 ...

2026-02-02   19分钟   SSmallOrange

Hazel学习笔记:游戏功能完善(持续更新)

Hazel学习笔记:游戏功能完善 由于Hazel剩下的内容非常多,我以前也没有接触过游戏、渲染部分,所以学起来比较吃力。这部分我将作为OpenGL、游戏相关知识的积累,沉淀一些个人理解,并持续更新。 ECS ECS是游戏中常用的架构,他之所以流行,是因为:他把对象的存储和使用分开,在不影响易用性的前提下,保证了性能。 如果没有ECS,我们应该如何处理游戏对象? 假如我们现在有一个路灯和一个玩家,他们作为游戏世界的一员,首先需要有自己的位置信息,现在要让这个路灯和这个玩家都具备发光的能力,那么我们很容易能想到: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Transform { public: void SetPos(vec3 pos) {} vec3 GetPos() {} private: vec3 pos; }; class Light { public: void StartLight(); void StopLight(); }; class StreetLight : Light, Transform { public: void StartLight() { /* ... */ } }; class Player : Light, Transform { public: void StartLight() { /* ... */ } } 后来游戏逐渐更新,玩家的灯光逻辑变得越来越复杂… ...

2026-01-20   13分钟   SSmallOrange

Hazel学习笔记:Renderer2D(持续更新)

Hazel学习笔记:Renderer2D部分 ​ 最近一段时间一直想拓展一下个人的技术栈,想学习一些图形学的知识,毕竟目前在做客户端相关的开发,学一些图形相关的知识应该也没有什么坏处,从我过去一段时间的工作经历来看:只要是学过的芝士,总有一天会用得上的。 ​ 正好翻B站看到了有人搬运了TheCherno 大佬的Hazel游戏引擎教程,就决定从这里入手学习,了解OpenGL和一些图形学的基础知识,并将一些芝士整理在此, 对于引擎基础部分的搭建就先略过(比如:日志部分(log)、项目搭建(Premake)、事件、窗口等,后面可以单开一节整理Premake和性能分析部分),重点整理他对OpenGL的封装部分,以及后面的Renderer2D的抽象。 ​ 整体架构 OpenGL封装 Hazel从OpenGL的“Hello World”入手,逐步减少原生OpenGL代码的编写,所以我们首先需要知道,原生OpenGL绘制一个三角形都需要哪些步骤? 首先,需要准备一些顶点数据: 1 2 3 4 5 6 float vertices[] = { // 位置 // 颜色 -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f }; 准备VAO/VBO: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned int vao, vbo; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 位置属性,location = 0 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); // 颜色属性,location = 1 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glBindVertexArray(0); 准备着色器: ...

2025-11-24   6分钟   SSmallOrange

现代反射实现(三):Json序列化

第三章:基于反射的 Json 序列化 代码实现方案参考:https://github.com/qicosmos/iguana 通过前面的努力,我们目前已经有了以下能力: 通过编译器内置宏拿到成员名 通过结构化绑定和打表拿到成员引用 tuple 通过递归构造拿到成员数量 简单说,现在我们已经能在编译期拿到一个结构体的“成员列表”,并在运行时获得对应的引用。 有了这些能力,我们就能实现:一行代码序列化结构体对象,摒弃繁琐的Json拼装过程: 1 2 3 struct Complex obj; std::string strJson; tinyrefl::reflection_to_json(obj, strJson); // 序列化 最终支持的能力包括: 1 2 3 4 5 6 7 // 普通字段 int / double / bool / std::string / char* // 嵌套 struct std::vector / std::list / std::deque // 容器和多层嵌套容器 std::map<std::string, T> / std::unordered_map<std::string, T> vector<vector<int>> / vector<vector<T>> 实现方案 目前传统的Json支持各种类型,如:对象、数组、字符串、数字、Bool等。 类型不同,Json的输出方式也不同,比如字符串类型可能需要双引号进行包裹,数组类型或对象类型则需要括号来标识,想要将结构体序列化成一个Json格式的字符串,就需要对结构体中的各种类型分别进行处理。 做编译期类型区分,模板显然是再适合不过了。 前两章已经把“怎么拿到成员”这件事解决掉了:我们能拿到 members_count_v<T>,能拿到成员名数组,也能遍历到每个成员的引用。序列化阶段真正要解决的问题,其实只有一个: 对于任意一个 member_reference,它到底应该被当成什么类型写进 Json。 典型几种情况: 内置算术类型,按数字写 布尔类型,写成 true / false 字符串、字符指针,加引号并做转义 容器,写成数组或对象 自定义聚合类型,展开成对象并递归处理成员 在类型分发之前,首先我们需要能够遍历结构体成员。 结构体成员循环遍历 对外只暴露一行接口: 1 2 template <detail::AggregateType T, detail::OutputStream Stream> inline void reflection_to_json(T&& object, Stream &stream); 其中AggregateType是目前序列化所支持的成员类型,OutputStream是我们能够支持的输出类型,可以只限定为std::string。 1 2 3 4 5 6 7 8 9 // 检查是否为聚合类型 template <typename T> concept AggregateType = ::std::is_aggregate_v<remove_cvref_t<T>>; // 检查字符串类型 template<typename Stream> concept OutputStream = requires(Stream& s) {{ s.append("abc") };}; // 或 template<typename Stream> concept OutputStream = ::std::is_same_v<std::remove_cvref_t<Stream>, std::string>; 在第一层类型检查通过后,我们就需要对具体类型进行递归分发了,首先我们要能够循环的遍历结构体成员: ...

2025-11-15   4分钟   SSmallOrange

现代反射实现(二):元数据获取

现代反射实现(二):元数据获取 这一部分,我们就来使用上一章介绍的芝士获取结构体的元信息。 参考链接: 编译期反射成员名称 编译期获取结构体成员数量 侵入式编译期反射 结构体成员名称获取 ​ 上一章我们提到,可以通过编译器内置的宏(MSVC:__FUNCSIG__、clang、gcc:__PRETTY_FUNCTION__)来获取成员变量名称,具体表现为: 1 // void __cdecl get_func_name_template<&p.m_name>(void) 定义一个示例用的结构体:struct Person { int m_age; std::string m_name; }; 那么,通过一定的字符串截取,我们就可以拿到一个结构体成员的名称: 1 2 3 4 5 6 7 8 9 10 11 12 template <auto val> inline constexpr std::string_view get_member_type_name() { #if defined(_MSC_VER) std::string_view funcName = __FUNCSIG__; size_t begin = funcName.rfind(".") + 1; size_t end = funcName.rfind(">("); return funcName.substr(begin, end - begin); #endif } static Person p; get_func_name_template<&p.m_name>(); // out: m_name 但是,对于一个用于存储网络请求的结构体来说,结构体成员的数量可能非常多,如果对于每一个都这样手动的去获取,和REFLECTION(Person, m_name, m_age)就没区别了,这时候上一章讲到的结构化绑定就派上用场了,能够想到: ...

2025-08-12   4分钟   SSmallOrange

现代反射实现(一):语法基础

现代反射实现(一):语法基础 对于模板了解不多的同学可以先学习下模板基础,这里推荐博客:模板元编程教程 ,讲的很详细。 以下是我们在实现反射时会用到的部分语法介绍 为什么选择 C++20 首先,实现一个反射功能就需要知道一个对象的:成员名称、成员的值,并且要有修改成员值的能力,而c++因为“零运行时开销”的设计初衷,导致反射迟迟没有被纳入标准(c++26已支持),因为反射往往意味着运行时元数据,这会带来额外内存和性能开销,与零开销原则冲突。 不过我们可以通过一些其他的方式,实现我们自己的简单的反射功能,不过这肯定会不可避免的带来: 代码体积膨胀(反射类型越多,体积越大) 运行时内存增加(运行时保留类型元数据) 不过在软件工程中,复杂度是守恒的,只会转移不会消失,所以一定程度的编码便利带来一定的开销也是可以接受的。 通过查找资料发现,目前c++20的反射实现主要思路都是: 通过结构化绑定和编译器内置的宏(MSVC:__FUNCSIG__、clang、gcc:__PRETTY_FUNCTION__)来获得对象的成员名称 通过结构化绑定获得对象的成员引用(可以获得和修改对象数据) 这里的__FUNCSIG__顾名思义是获得函数签名的宏,对于如下调用: 1 2 3 4 5 6 7 8 9 10 11 template <auto val> inline void get_func_name_template() { std::cout << __FUNCSIG__ << std::endl; } struct Person { int m_age; std::string m_name; }; static Person p; get_func_name_template<&p.m_name>(); // MSVC C++20编译输出:void __cdecl get_func_name_template<&p.m_name>(void) 可以看到,输出中包含了我们需要的成员名称,对于这种固定模板,我们可以很容易的获得他的成员名称,但是如上代码在C++17是无法编译通过的,C++17需要如下格式: 1 get_func_name_template<&p>(); 原因在于:C++17对于非类型模板参数的值类型限制很严格,不允许将对象的子对象地址作为模板入参,这就直接导致了C++20以下的版本通常只能通过如下方式手动注册对象的元信息: 1 REFLECTION(Person, m_age, m_name); 这就直接导致了想要一行代码实现序列化和反序列化,只能以C++20作为基础来实现。 而其他的限制通常都可以通过SFINAE来规避,只不过实现起来会更加复杂。 同时C++20还有很多方便我们实现的特性,如:支持模板的Lambda表达式,concept和requires等,能够大大提高我们的代码可读性,提高开发效率。 语法介绍 对模板Lambda表达式的支持 在 C++20 中,你可以让 Lambda 接收模板参数,这在编译期展开成员信息时非常方便。 ...

2025-08-11   3分钟   SSmallOrange