写游戏引擎前需明确目标、控制复杂度、优化内存与API使用:先实现最小可交付场景,避免过早抽象;用对象池和栈分配减少动态内存;首版聚焦单一图形后端;ECS按需引入,优先可读性与调试性。

从零写游戏引擎前,先问自己三个问题
不是不能写,而是绝大多数人卡在「没想清楚要做什么」。一个能跑通 main() 并渲染出三角形的“引擎”,和一个能支撑中型 3D 游戏开发的引擎,中间隔着至少两年真实项目迭代。你真正需要的,大概率不是“从零造轮子”,而是“用最小可行模块验证设计假设”。
常见错误现象:Engine 类刚建好就塞进 Renderer、AudioSystem、PhysicsWorld —— 结果不到两周,Update() 函数变成 800 行意大利面条,任何修改都牵一发而动全身。
实操建议:
- 先定义你的第一个「可交付场景」:比如“加载一个 glTF 模型,用方向光照亮,按 WASD 移动相机”——所有代码只服务于这个目标
- 拒绝抽象先行。不要提前写
IResourceLoader接口,先硬编码加载一个.obj文件;等真遇到第二个格式(比如.fbx)再抽离 - 把
main.cpp当作胶水层,而非总控。引擎内部各系统之间尽量不依赖全局单例,改用显式传参或依赖注入(哪怕只是构造函数传指针)
核心循环里别碰 new / delete
游戏主循环每秒执行 60–120 次,new 和 delete 在这里触发,等于主动邀请堆碎片和缓存失效。很多初学者以为“C++ 就是手动管内存”,结果帧率卡顿根源就在这一行 new Entity()。
立即学习“C++免费学习笔记(深入)”;
使用场景:实体创建、临时计算缓冲区、事件队列节点、粒子系统实例
实操建议:
- 用对象池管理高频生命周期对象,比如
ParticlePool或EventQueueArena,预分配一大块连续内存,用游标(m_freeListHead)代替malloc - 栈上分配优先:函数内小数组用
std::array<float, 16>,不用std::vector;矩阵运算用glm::mat4(它默认栈分配) - 若必须动态分配,统一走自定义
Allocator,且只在帧开始时批量申请、帧结束时批量释放(如FrameAllocator)
不要过早封装 OpenGL/Vulkan 抽象层
写个 IRenderer 接口,底下同时支持 OpenGL 和 Vulkan,听起来很工程,实际只会拖慢开发节奏、掩盖 API 差异带来的设计缺陷。Vulkan 的 vkCmdBindPipeline 和 OpenGL 的 glUseProgram 根本不是同一抽象层级。
性能影响:强行统一接口常导致“最低公分母”设计,比如为兼容 OpenGL 而禁用 Vulkan 的 subpass 优化,或为 Vulkan 强制引入 OpenGL 风格的立即模式调用
实操建议:
- 第一版只支持一个后端:推荐 OpenGL 4.5(调试友好、驱动成熟),等核心管线跑稳再考虑 Vulkan
- 把图形 API 调用尽量收拢到极小范围:比如所有
glDraw*只出现在OpenGLCommandBuffer::Execute()里,上层只处理DrawCall数据结构 - 如果真要多后端,用编译期分发:
#ifdef VK_ENABLE+RendererVK/RendererGL两套实现,而非运行时虚函数跳转
Entity-Component-System 不是银弹
很多人一上来就抄 entt 或手撸 ECS,结果发现连“播放一个音效”都要拆成 AudioSourceComponent + PlaybackSystem + AudioEvent,反而比直接 audioEngine->Play("jump.wav") 多写三倍代码。
容易踩的坑:System 之间隐式依赖难调试(TransformSystem 必须在 RenderSystem 前运行),组件数据布局不连续导致缓存命中率低,以及最致命的——调试时根本不知道哪个 Entity ID 对应编辑器里的“主角玩家”
实操建议:
- 先用裸指针或 ID 映射管理对象:比如
std::unordered_map<uint32_t, Player*>,等真出现 10k+ 实体且性能瓶颈明确在遍历上,再换entt::registry - 组件数据优先用 SOA(结构体数组):把所有
position.x放一起,而不是struct Transform { vec3 pos; quat rot; }的 AOS,这对 SIMD 和缓存更友好 - 给每个
Entity加可读标签(std::string m_debugName),调试器里一眼看出entity.id == 42是不是“Boss_Spawner”
真正难的从来不是“怎么写引擎”,而是“怎么让下个接手的人不骂你写的引擎”。变量命名、日志粒度、资源加载失败时的 fallback 路径、甚至错误信息里是否包含文件行号——这些细节堆起来,才决定一个引擎是玩具还是生产工具。



















