
Hazel Engine 没有使用分层式架构设计,多模块间的耦合相当严重,类之间的相互依赖也比较常见,这对分块编译和后续扩展提出了较大的挑战。
持续思考一个尽可能简单又有效的架构设计,尽可能满足多Target分离,动态加载DLL,模块解耦。
Architecture
- 纯抽象类 + Service Locator
- 需要热更新则引入 C 函数字典
模块间接口调用最核心的目标是将 编译器耦合 降低到最小,再用运行时注册或数据驱动将系统拼接起来。
常见问题:
- 不要在 Interface 头里暴露 STL 容器或自定义模板实例,因为一旦编译器版本不同 ABI 就崩溃。
- 跨 DLL 抛异常,传递
std::string
都很危险,统一使用 POD 或string_view
。 - C API 虽丑但稳定,很多商业引擎(Unity NativePlugin、Godot GDNative)都依赖它实现长期兼容。
- 如果模块间必须传复杂对象,可用 Serializer:: Write(Object*, Blob&) 把状态打包,用 blob 交换。
Service Locator
- 接口只在编译器出现,不让任何实现细节泄露;
- 运行时把实时对象注册到一个全局的注册表(或显式传入)中;
- 调用时通过接口指针完成功能调用(多态)。
但在实现时需要注意一些坑:
接口应该统一放在 Core 中还是各个子模块?
最小原则:只有“跨模块要被别人用”的 API 才放到最低层。
常见做法是:
在 Core 目录里只放通用基类和 Service Locator;
每个子系统再有自己的 Public 头,例如 Render/Public/IRender.h、Physics/Public/IPhysics.h。
这样 Render 的实现(Render.dll / libRender.a)位于更高层,但它的 接口头文件 可以像第三方 SDK 一样被任何人 include,而不会造成链接依赖。
- 需要调用渲染的模块只依赖 IRender.h(接口),不会链接 Render 的实现;
- Render 若想回调 Physics,就只能依赖 IPhysics.h 或通过事件/ECS 数据,避免形成编译环。
POD
POD 是 “Plain Old Data” 的缩写,意指“像 C 语言那样朴素的数据类型”。在 C++ 语境中它有一套非常具体的标准定义,用来保证以下两件事 ︰
- 内存布局可预测——字段按声明顺序顺排,不会插入 v-ptr、padding 不会被继承级别打乱;
- 拷贝 / 反序列化 /
memcpy
/ 跨 DLL 或跨语言传递 时不会触发构造、析构、虚函数等隐式行为,因而 ABI(应用二进制接口)稳定、安全。
一、正式定义(C++11 之后)
C++11 起标准把 “POD” 拆成两类 type traits,再用“二者皆满足”来等价于旧称 POD:
• trivial type
- 默认构造、拷贝构造、移动构造、拷贝赋值、移动赋值、析构函数全部是 trivial(编译器隐式生成,且只是 bit-wise 拷贝 / 释放)。
• standard-layout type - 没有虚函数或虚继承;
- 同一访问级别(public/private/protected)的非静态数据成员彼此在同一个类定义层次;
- 第一个非静态数据成员与结构体本身地址相同;
- 不是多继承等会导致额外 v-ptr 或调整指针的复杂继承结构。
若一个类型同时满足 “trivial” 与 “standard-layout”,标准就把它视作 POD。std::is_pod<T>::value
(已在 C++20 标准中弃用,推荐用 is_trivial && is_standard_layout
自组合)。
二、常见满足/不满足例子
满足(POD):
struct Vec3 { float x, y, z; }; // 仅含内建类型 |
不满足(非 POD):
struct Foo { std::string s; }; // 成员有非 POD |
三、为什么跨模块 / 语言首选 POD
- 可直接
memcpy
、读文件、网络反序列化——不触发构造/析构栈; - ABI 在不同编译器版本、不同 DLL 间一致(只要基本对齐规则一致);
- 与 C API、GPU 着色器常量缓冲 (Uniform Buffer) 或 SIMD 批处理结构天然对齐;
- 避免把
std::string
、std::vector
、虚函数表指针等 实现相关 信息暴露到接口层,降低升级和热更新风险。
四、在游戏引擎中的用法示例
// Public Interface 头文件(跨 DLL ) |
- 跨 DLL 不要抛/捕异常,也不要返回/接收 STL 容器;
- 若需要复杂对象,可在模块内部保留指针,向外部只暴露
uint32_t handle
; - 若必须传非 POD(例如
std::string
),可封装成struct { const char* data; uint32_t size; }
这样的轻量 POD。
简而言之,POD 是“没有隐藏行为、内存布局稳定”的数据结构,在模块边界、网络、磁盘、GPU 乃至脚本绑定等场景都极其安全、易维护,是现代游戏引擎接口层的首选交换格式。
- 本文标题:游戏引擎架构设计学习
- 创建时间:2025-05-30 11:23:00
- 本文链接:2025/05/30/note/Programming/CG/Architecture/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!