尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

游戏引擎五层架构实战:从零搭建不秃头的引擎骨架

游戏引擎五层架构实战:从零搭建不秃头的引擎骨架 这类讲游戏引擎架构的文章最容易写成纯理论看完一堆分层图还是不知道代码该怎么写、项目该怎么搭。GAMES 104 的课程内容很硬核但“小明秃头记”这个比喻其实点出了一个核心问题一个游戏引擎从一行代码到能跑起来到底是怎么一层层组织起来的这篇文章不打算复述课程的全部细节而是围绕“五层分层设计”这个骨架把它还原成一个真实的、从零开始的引擎搭建过程。我会用更工程化的视角拆解每一层到底在解决什么具体问题层与层之间如何交互以及在实际编码时哪些地方最容易“秃头”。无论你是想理解引擎原理的学生还是准备动手写个玩具引擎的开发者都可以把这篇文章当作一份“避坑指南”和“实现路线图”。1. 先别管五层图理解“小明”到底在为什么秃头在直接看分层图之前我们得先回到问题原点。假设你就是“小明”要一个人或一个小团队从零开始做一个能运行的游戏引擎。你会遇到哪些让你头秃的具体问题硬件五花八门玩家的电脑有 Windows, macOS, Linux显卡有 NVIDIA, AMD, IntelCPU 架构、内存大小、磁盘速度都不同。你的引擎代码总不能为每一台机器写一个版本。资源管理混乱一个游戏有成千上万的纹理、模型、音频、配置文件。它们怎么从硬盘加载到内存怎么知道哪些在用、哪些可以卸载怎么处理加载失败逻辑更新复杂游戏里可能有成千上万个物体游戏对象每个物体都有自己的行为移动、攻击、播放动画。这些行为谁先执行、谁后执行怎么保证物理计算在渲染之前完成渲染管线黑盒你想画一个三角形到屏幕上需要经过顶点着色器、光栅化、片段着色器等一系列步骤。这些步骤如何组织成一个可配置、可扩展的管线而不是一堆写死的 OpenGL 或 DirectX 调用工具链缺失关卡设计师需要编辑器来摆放物体美术需要预览模型和材质策划需要调整数值。这些编辑器工具和引擎运行时是什么关系“小明秃头记”的本质就是试图用一套代码同时解决以上所有问题结果就是代码耦合严重改一动百调试困难最终“秃头”。而分层设计就是为了把这一团乱麻的问题分解到不同的“楼层”去解决每一层只关心自己楼下那层提供的服务并为楼上那层提供清晰的接口。2. 五层架构一个自底向上的施工蓝图现在我们来看这经典的五层。它不是凭空想象的理论而是应对上述问题的自然推导结果。我会从最底层开始用“造房子”的比喻讲清楚每一层的职责和关键实现。2.1 第一层平台抽象层Platform Abstraction Layer这是引擎的“地基”。它的核心目标就一个屏蔽不同操作系统和硬件的差异让上层代码写一次就能在多个平台上运行。这一层具体做什么文件系统提供统一的文件读写接口。在 Windows 上可能是CreateFile/ReadFile在 Linux 上是open/read但到了上层你只调用Engine::FileSystem::ReadTextFile(“config.json”)。时间与计时器提供高精度、稳定的时间获取接口如GetCurrentTimeSeconds(),GetDeltaTime()用于驱动游戏循环。窗口与输入创建和管理应用程序窗口处理鼠标、键盘、手柄等输入事件的收集与派发并统一成引擎内部的事件格式。线程与同步封装线程创建、互斥锁、信号量等并发原语为上层提供任务调度和多线程渲染的基础。图形API抽象这是最复杂的一块。它不直接实现渲染算法而是定义一套统一的图形接口如RenderDevice,Buffer,Texture,Shader然后在底层用 OpenGL, DirectX 12, Vulkan 或 Metal 去实现这些接口。上层渲染代码只跟这套抽象接口打交道。实现要点与避坑不要过度设计初期用#ifdef _WIN32这类宏来做条件编译是完全可以接受的。关键是保持接口统一。输入处理是状态机不要只处理“按键按下”事件更要提供“当前按键状态”如IsKeyDown(KeyCode::Space)这对游戏逻辑至关重要。时间要稳定且防溢出使用高精度计时器如QueryPerformanceCounter并处理好deltaTime为0或过大的异常情况比如游戏窗口失去焦点时。2.2 第二层核心层Core Layer地基打好了现在开始建“承重墙和主框架”。这一层提供引擎最基础、最通用的服务不包含任何游戏逻辑或渲染算法。这一层具体做什么内存管理实现自定义的内存分配器如堆分配器、池分配器、栈分配器。为什么不用new/delete为了减少内存碎片、提高缓存命中率、方便内存泄漏检测。例如为频繁创建销毁的游戏对象使用对象池。数学库提供Vector2/3/4,Matrix3x3/4x4,Quaternion四元数,AABB包围盒,Ray射线等类及其运算。这是所有图形和物理计算的基石必须保证高效和正确。容器与字符串提供引擎专用的Array,HashMap,String等数据结构。标准库如 C STL可能在某些平台或场景下如主机开发有局限性自定义容器可以更好地控制内存和行为。资源管理器定义资源的基类Resource并管理资源的生命周期加载、引用计数、卸载。它通常与一个“资源文件路径”到“资源对象”的映射表HashMap一起工作。配置与日志统一的配置读取接口和分级日志输出系统如 Debug, Info, Warning, Error。这是调试复杂引擎的“眼睛”。实现要点与避坑数学库先求正确再优化先确保矩阵乘法、四元数旋转等运算结果正确。优化如 SIMD 指令可以后期再做。内存管理是性能关键至少实现一个简单的线性分配器用于单帧临时数据和一个池分配器用于粒子、子弹等小对象性能提升立竿见影。资源引用使用句柄或智能指针避免使用原始指针管理资源生命周期容易导致野指针或内存泄漏。可以使用ResourceHandle一个包含资源ID的结构或引擎定制的智能指针。2.3 第三层资源层Resource Layer框架搭好了现在要运“建材”进来。这一层负责将硬盘上的原始数据图片、模型、声音转换并加载成引擎运行时可以高效使用的格式。这一层具体做什么资源定义继承自核心层的Resource基类定义具体的资源类型如TextureResource,MeshResource,MaterialResource,AudioClipResource。资源加载器每个资源类型有对应的加载器ResourceLoader。它负责从文件或网络读取原始字节。解析文件格式如.png,.fbx,.wav。将数据转换成引擎优化格式如将纹理转换成 GPU 支持的压缩格式将模型顶点数据打包成特定布局。创建并初始化对应的资源对象。异步加载为了不阻塞主线程导致游戏卡顿加载过程通常是异步的。资源管理器会管理加载队列和完成回调。依赖管理一个资源可能依赖其他资源如一个材质依赖多个纹理。加载器需要解析并处理这些依赖关系。实现要点与避坑分离运行时格式与磁盘格式磁盘上存储的应该是压缩的、便于传输的格式如.fbx。加载时将其转换成内存/GPU 友好的格式如交错存储的顶点缓冲区。这是提升加载速度和运行效率的关键。一定要有 Fallback 资源当某个纹理加载失败时不要让模型变成黑色或崩溃而是加载一个内置的“错误纹理”比如显眼的紫黑格子。这对快速定位问题和提升健壮性非常重要。实现资源热重载在开发期这是一个神级功能。修改一个纹理文件并保存游戏内实时更新。这需要文件系统监控和资源版本管理。2.4 第四层功能层Function Layer建材齐备现在开始组装“功能模块”。这一层包含了游戏引擎的各种子系统它们利用下层提供的服务实现具体的游戏功能。这是最丰富的一层通常包括渲染引擎基于平台抽象层提供的图形接口组织渲染管线。管理着色器、渲染状态、渲染目标实现视锥剔除、光照计算、后处理等。物理引擎集成或实现物理模拟如 Bullet, PhysX提供碰撞检测、刚体动力学、射线检测等功能。音频引擎管理音效的播放、混音、3D 空间音效。动画系统支持骨骼动画、状态机、混合树等。场景图/游戏对象模型这是功能层的“粘合剂”。它定义游戏世界中的实体通常叫GameObject或Entity以及挂载在这些实体上的组件Component如TransformComponent变换MeshRendererComponent网格渲染RigidbodyComponent刚体。ECS实体-组件-系统架构就是这一层的一种更现代、更数据导向的实现范式。脚本系统提供让游戏逻辑非引擎代码能够运行的环境如 Lua, C# 脚本的绑定。实现要点与避坑子系统间解耦渲染系统不应该直接调用物理系统的内部函数。它们通过共享组件如TransformComponent或事件系统进行通信。例如物理系统更新位置后触发一个“位置更新”事件渲染系统监听该事件并更新渲染数据。游戏循环是总指挥功能层所有子系统的更新都由一个主游戏循环Game Loop驱动。典型的顺序是处理输入 - 更新脚本逻辑 - 更新物理 - 更新动画 - 渲染。这个顺序至关重要。从简单的组件模型开始不必一开始就追求完美的 ECS。一个简单的GameObject包含一个std::vectorstd::shared_ptrComponent在游戏循环中遍历更新所有组件对于中小型项目完全够用且更容易理解。2.5 第五层工具层Tool Layer房子盖好了最后是“室内装修和物业管理”。这一层为内容创作者策划、美术、关卡设计师提供编辑和制作游戏内容的工具。这一层具体做什么关卡编辑器可视化地摆放游戏对象、设置属性、创建地形、规划路径。材质编辑器可视化地连接着色器节点创建和调整材质。动画编辑器编辑骨骼动画的关键帧和曲线。资源浏览器预览和管理项目中的所有资源。属性检查器查看和修改选中游戏对象的组件属性。关键洞察工具层与运行时层的共生关系这是最容易误解的一点。工具层编辑器和功能层以下运行时并不是完全分离的两个程序它们共享了大量的底层代码。编辑器里看到的一个 3D 模型用的是和游戏运行时同一套渲染引擎来绘制的。编辑器里调整物理参数背后调用的是同一个物理引擎的接口进行实时预览。编辑器的“播放”按钮本质上就是启动了同一个游戏循环。区别在于编辑器拥有更多的“控制权”和“元数据管理”功能比如保存关卡文件这只是一堆对象ID和属性数据而运行时则专注于高效地模拟和渲染。实现要点与避坑初期可以用 ImGui 快速搭建原型不要一开始就想着做一个 Unity 那样的复杂编辑器。使用 Dear ImGui 这类即时模式 GUI 库可以快速做出可用的属性面板、资源浏览器验证工作流。数据与表现分离编辑器保存的关卡文件应该只包含数据对象ID、组件类型、属性值不包含任何引擎运行时的对象指针或状态。运行时加载这个文件再根据数据实例化出真正的游戏对象。热重载是生产力核心除了资源热重载最好还能实现代码逻辑脚本的热重载和场景状态的热重载这能极大缩短迭代时间。3. 分层之后数据如何流动对象如何生存理解了静态的分层还要理解动态的协作。两个最核心的流程是一帧的生命周期和一个游戏对象的生命周期。3.1 一帧之内游戏循环的流水线假设一帧是 16.6 毫秒60 FPS引擎在这段时间里做了什么这是一个简化的流水线平台层收集本帧所有的窗口事件如缩放、输入事件按键、鼠标移动。核心层计时器计算上一帧到这一帧的时间间隔deltaTime。功能层 - 逻辑更新事件系统将输入事件分发给感兴趣的脚本组件。所有游戏对象的脚本组件Update(deltaTime)根据输入更新逻辑状态如“按下W键向前移动”。功能层 - 物理更新物理系统FixedUpdate根据力和速度计算新的位置和碰撞。功能层 - 动画更新动画系统根据时间更新骨骼姿态。功能层 - 渲染准备渲染系统遍历所有带渲染组件的对象进行视锥剔除将需要渲染的对象和数据提交到渲染命令队列。平台层/功能层 - 渲染执行图形API抽象层执行渲染命令将图像提交到显卡最终显示到屏幕。核心层本帧结束内存分配器中的“帧分配器”被重置所有本帧申请的临时内存被一次性释放非常高效。3.2 一个游戏对象的诞生与消亡创建在编辑器中设计师在编辑器里拖出一个“箱子”预制体。工具层请求功能层的GameObjectManager创建一个新的GameObject并为其添加TransformComponent和MeshRendererComponent。MeshRendererComponent引用了一个MeshResource和一个MaterialResource。序列化保存关卡工具层将GameObject的 ID、组件列表及每个组件的属性值如位置、引用的资源ID序列化为 JSON 或二进制文件存入磁盘。加载游戏运行时资源层加载关卡文件解析数据。功能层根据数据再次创建GameObject和组件并通过资源ID向资源管理器请求加载对应的MeshResource和MaterialResource。资源管理器异步加载这些资源加载完成后回调函数将资源句柄设置给MeshRendererComponent。存活游戏运行中每一帧该对象都参与上述游戏循环脚本可能让它移动物理可能让它掉落渲染系统将它画出来。销毁被消灭或关卡切换功能层标记该对象为待销毁。在帧末或合适的时机清理其所有组件并通知资源管理器减少相关资源的引用计数。当资源引用计数为0时资源层将其从内存中卸载。4. 从理论到实践你的第一个“不秃头”引擎搭建路线如果你看完想动手下面是一个最小可行性的实践路线能帮你把五层理论落地而不是被复杂性吓倒。4.1 第零步心态和目标调整不要想着复刻 Unity 或 Unreal。目标是做一个能渲染一个立方体在场景中旋转的“引擎”并理解其中每一行代码属于哪一层为什么在那里。4.2 第一步打好地基平台层 核心层基础创建一个跨平台窗口使用 GLFW 或 SDL 库。它们帮你封装了不同操作系统的窗口和输入创建这本身就是平台抽象的一部分。你的代码里不再出现WinMain或X11的具体调用。接入图形API选择 OpenGL 或 Vulkan 的现代版本OpenGL 3.3 或 Vulkan。用 GLEW 或 Glad 加载 OpenGL 函数。在这一步就考虑抽象创建一个GraphicsContext类它负责初始化 API、交换缓冲。未来如果要换 DirectX就重写这个类。实现基础数学库自己实现Vec3,Mat4类。实现矩阵的乘法、求逆初期可以用库如 glm但理解其实现很重要。实现日志系统一个简单的Log::Info(“Hello Engine”)输出到控制台和文件。4.3 第二步运入建材资源层基础定义顶点数据和着色器在代码里硬编码一个立方体的顶点位置、颜色数据。将 GLSL 着色器代码写成字符串字面量嵌入代码。创建最简资源表示创建VertexBuffer,IndexBuffer,ShaderProgram类。它们负责调用 OpenGL 的glGenBuffers,glCompileShader等函数。这些类就是你的第一批“资源”虽然现在还是从代码创建不是从文件加载。4.4 第三步组装模块功能层雏形实现最简游戏循环在窗口的主循环里顺序执行glfwPollEvents()输入- 计算deltaTime- 更新旋转角度逻辑-glClear- 设置着色器Uniform传递旋转矩阵-glDrawArrays渲染-glfwSwapBuffers。引入“组件”思想创建一个Transform结构体包含位置、旋转、缩放和一个MeshRenderer结构体包含VertexBuffer,ShaderProgram的引用。把它们放在一个GameObject结构体里。现在你的“功能层”有了两个最基础的组件系统变换和渲染的雏形。4.5 第四步连接工具链工具层思想用 ImGui 创建调试面板在渲染循环里集成 ImGui创建一个窗口里面用滑块动态调整立方体的旋转速度。这就是最原始的工具层——一个可以实时修改运行时参数的工具。将硬编码数据外置把立方体的顶点数据和着色器代码从代码里挪到外部的文本文件如cube.mesh,shader.vert。写一个简单的LoadMeshFromFile和LoadShaderFromFile函数。恭喜你你有了一个最简单的资源加载器。走到这一步你已经亲手实现了一个微型的、但五脏俱全的五层引擎架构平台层GLFW/SDL 窗口和事件。核心层数学库、日志系统。资源层从文件加载网格和着色器的函数。功能层游戏循环、GameObject带Transform和MeshRenderer组件的更新与渲染逻辑。工具层ImGui 调试面板。虽然每一层都极其简陋但分层的思想和数据的流向已经清晰可见。从这个微型引擎出发你可以沿着任何一层深入在平台层封装更统一的输入接口。在核心层实现一个内存池。在资源层支持加载.obj模型和.png纹理。在功能层加入相机组件、光照组件。在工具层做一个可以拖拽物体的小编辑器。这个过程中你会不断遇到“小明”的秃头问题但因为你已经建立了分层的思维你会清楚地知道问题该由哪一层来解决以及修改它会不会引起“地震”。这就是学习分层设计最大的价值。
返回列表