如何用 C++ 创建游戏引擎?

2025年3月17日 | 阅读16分钟

本文将讨论 C++ 中的游戏引擎,包括其历史、制作方法和不同方面。

什么是游戏引擎?

一组软件工具的组合称为“游戏引擎”。它主要用于简化视频游戏的创建。这些引擎可能小巧基础,仅提供游戏循环和一些渲染选项,或者它们可以庞大且包罗万象,类似于 IDE 程序,开发者可以在其中编写脚本、调试、自定义关卡逻辑和 AI、设计、发布、协作,并最终从头开始创建游戏,而无需离开引擎。

游戏框架和引擎通常会向用户公开 API。利用这个 API,程序员可以与引擎功能进行交互,并像处理黑盒一样处理具有挑战性的任务。

  • 让我们来理解一下这个API,以便您能够真正理解它的运作。例如,游戏引擎 API 通常包含一个名为“IsColliding()”的函数,开发者可以使用它来确定两个游戏对象是否正在碰撞。
    How to create a game engine in C++

该算法用于准确评估两个形状是否重叠,而无需程序员了解此函数的实现方式。我们将IsColliding函数视为一个神秘的黑盒,它会执行一些魔术,并根据对象是否碰撞准确地返回 true 或 false。它说明了大多数游戏引擎都提供给玩家的一项功能。

  • 除了作为编程 API 之外,游戏引擎的主要职责是抽象硬件。例如,3D 引擎通常使用特定的图形 API 构建,例如Direct3D、VulkanOpenGL。这些 API 可以对图形处理单元 (GPU) 进行软件抽象。
  • 低级库(如DirectX、OpenALSDL)使对各种其他硬件组件的抽象和跨平台访问成为可能,这是硬件抽象的另一个例子。这些库使我们能够访问和管理鼠标移动、网络连接、键盘事件和音频。

游戏引擎的兴起

  • 在游戏行业的早期,代码的创建是为了充分发挥慢速设备的性能。游戏是使用定制的渲染引擎创建的。更多的开发者无法承受代码重用或在各种情况下使用的通用函数
  • 随着游戏和开发团队的规模复杂性的增加,大多数工作室在他们的游戏中使用了相同的函数和子程序。工作室创建了内部引擎,本质上是处理低级功能的内部文件和库的混合体。由于这些功能,其他开发团队成员可以专注于游戏玩法、地图设计关卡自定义的更复杂方面。
  • id Tech、Build 和 AGI 等经典引擎是游戏引擎的几个例子。这些引擎旨在帮助创建特定的游戏,使其他团队成员能够快速设计新关卡、添加独特对象并即时修改地图。这些定制引擎还用于修改或生成游戏的原始版本的扩展包。
  • Id Software创建了id Tech。构成id Tech的引擎的每个版本都与特定游戏相关联。开发者经常将id Tech 0、id Tech 1id Tech 2称为“《德军司令部 3D》引擎”、“《毁灭战士》引擎”“《雷神之锤》引擎”
    How to create a game engine in C++
  • 另一款引擎是Build,它对90 年代的游戏开发做出了贡献。它由Ken Silverman开发,用于帮助第一人称射击游戏的定制。与 ID Tech 一样,Build 随着时间的推移而变化,其多个迭代版本帮助程序员创建了《毁灭公爵 3D》、《影子战士》《血》等游戏。这些游戏通常被称为“三巨头”,无疑是使用Build 引擎制作的最著名的游戏。
  • “SCUMM”(《曼尼卡斯的恶梦》脚本创建实用程序)90 年代另一款游戏引擎的例子。SCUMMLucasArts开发的引擎,是《全金属外壳》和《猴岛小英雄》等许多著名点击式冒险游戏的基础。
  • 《全金属外壳》的对话和动作都是使用 SCUMM 编程语言管理的。
  • 随着机器的发展,游戏引擎也随之发展和增强。现代引擎中功能丰富的工具需要闪电般的处理器、庞大的内存和专用图形卡。

现代引擎用额外的功率来交换机器周期以获得更多抽象。这种权衡使我们可以将现代游戏引擎视为用于快速、经济高效地开发复杂游戏的通用工具。

如何制作游戏引擎?

有几个步骤可以帮助制作游戏引擎。这些步骤如下:

1. 选择编程语言

  • 选择编程语言是我们必须做出的第一个选择之一,它将用于创建核心引擎代码。有几种高级语言,如 C#、Java、Lua、JavaScript,以及原始汇编、C 和 C++,它们都用于创建游戏引擎。
    How to create a game engine in C++
  • C++是创建游戏引擎最广泛使用的语言之一。C++ 编程语言结合了速度和特性,支持面向对象编程(OOP)和其他有助于规划和设计大型软件项目的编程范式。
  • C++ 的优点是它是一种编译型语言,因为性能在创建游戏时至关重要。如果一种语言是编译型的,那么它的最终可执行文件可以直接在目标机器的处理器上运行。
  • 例如,开发者可以使用 Microsoft 自己的 C++ API 访问Xbox 控制器

2. 硬件访问

  • 在早期的操作系统(如MS-DOS)中,我们可以访问内存地址和映射到各种硬件组件的特定位置。例如,要用特定颜色“绘制”一个像素,将对应于正确颜色的数字加载到VGA 调色板的特定内存位置;然后,显示驱动程序会将此修改转换为 CRT 显示器中的实际像素。
  • 由于操作系统的演变,现在由操作系统负责保护硬件免受程序员的干扰。现代操作系统禁止代码修改超出操作系统为进程提供的允许地址范围的内存位置。
  • 例如,如果我们想在运行Windows、macOS、Linux*BSD绘制填充像素到屏幕上,或者与任何其他硬件组件通信,我们必须向操作系统请求适当的权限。即使是最基本的操作,例如在操作系统桌面打开一个窗口,也必须使用操作系统 API 来完成。
  • 因此,操作系统特定的操作包括启动进程打开窗口、在屏幕上生成图形、在该窗口内绘制像素,甚至读取键盘的输入事件。
  • SDL是一个非常知名的,它有助于实现跨平台硬件抽象。SDL作为许多 CPU 架构(Intel、ARM、Apple M1等)和各种操作系统之间的链接。SDL 库通过抽象低级硬件访问来“翻译”代码,使其与这些不同平台兼容。
  • 下面是一个使用SDL打开操作系统窗口的简短代码。
  • 为了简单起见,下面的代码将在Windows、macOS、Linux、BSD甚至Raspberry Pi上运行。
  • SDL只是可用于跨平台访问硬件的一种库。SDL 是2D 游戏以及将现有代码移植到其他平台和游戏机的流行选择。GLFW是另一个流行的跨平台库选项,通常与3D 游戏引擎一起使用。GLFW 库通过OpenGLVulkan等加速3D API与之有效通信。

3. 游戏循环

  • 打开操作系统窗口后,我们必须建立一个受控的游戏循环
  • 我们希望我们的游戏以60帧率运行。为了更好地理解,电影以24 FPS的速率播放(每秒有24 帧画面),而帧率可能会因游戏而异。

在游戏过程中,游戏循环会持续运行,游戏引擎需要在每次循环中执行一些关键操作。标准的游戏循环需要:

  • 不阻塞地处理输入事件。
  • 更新当前帧所有游戏对象的属性。
  • 在屏幕上显示游戏对象和其他关键数据。

游戏循环与实际时间之间必须存在某种联系。毕竟,无论系统的 CPU 时钟速度如何,游戏中的敌人应该以相同的速度移动。

控制这个帧率并将其设置为每秒特定帧数是一个有趣的话题。我们通常会跟踪帧之间的时间并进行一些基本计算,以确保我们的游戏以至少30 FPS的帧率平稳运行。

4. 输入

  • 一个不读取用户输入事件的游戏是没有意义的。这些事件可以来自游戏手柄、键盘、鼠标VR 头显。因此,我们必须在游戏循环中处理各种输入事件。
  • 为了处理用户输入,我们必须使用操作系统 API来请求访问硬件事件。我们可以使用跨平台硬件抽象库(SDL、GLFW、SFML等)来管理用户输入。
  • 如果我们使用SDL,我们可以轮询事件,然后使用几行代码来适当处理它们。

同样,使用像SDL这样的跨平台库来处理输入时,我们无需过多担心特定于操作系统的实现。无论我们针对哪个平台,我们的 C++ 代码都应该是一样的。

一旦我们拥有了正常工作的游戏循环和处理用户输入的方法,我们就开始在内存中组织我们的游戏对象

5. 在内存中表示游戏对象

  • 在创建游戏引擎时,必须建立数据结构来存储和访问游戏对象。
  • 在构建游戏引擎时,程序员会采用多种策略。有些引擎可能使用基于继承的简单面向对象策略来组织对象,而另一些引擎可能将对象分组到实体和组件中。
  • 如果我们使用 C++,使用 STL(标准模板库)是一种选择,它包含各种数据结构,如vector、list、queue、stack、mapset。由于 C++ STL 主要依赖于模板,因此这是一个练习使用模板并观察它们在实际项目中的使用的好机会。
  • 当我们更多地了解游戏引擎架构时,我们会发现实体和组件是游戏中最常用的设计模式之一。我们使用实体-组件设计将游戏场景中的对象组织为实体(在 Unity 中也称为“游戏对象”,在 Unreal 中称为“Actor”)和组件(我们可以添加到实体或附加到实体的数据)。
  • 考虑一个简单的游戏场景,以理解实体和组件如何交互。实体将包括我们的主要角色、敌人、地面和弹药。同时,组件将是我们“附加”到实体上的重要信息,例如位置、速度、刚体碰撞器等。
How to create a game engine in C++

游戏元素组织为实体和组件是游戏引擎的一种常见设计方法。我们可以选择向实体附加多个组件,如下所示:

  1. 位置组件:维护我们的实体在外部世界中的x-y 位置坐标(或三维中的x-y-z)记录。
  2. 速度组件:用于测量对象沿着x-y 轴(或 3D 中的 x-y-z 轴)移动的速度。
  3. 精灵组件:我们应该为特定对象渲染的PNG 图像通常存储在精灵组件中。
  4. 动画组件:动画组件用于跟踪实体的动画速度和动画帧的演变。
  5. 碰撞器组件:它决定了将要碰撞的实体的几何形状(例如边界框、边界圆、网格碰撞器等),并且通常与刚体机制相关联。
  6. 生命值组件:它保存了实体的当前生命值。通常,它只是一个数字百分比值(如生命条)。
  7. 脚本组件:有时,我们可能会附加一个脚本组件到对象上。此脚本组件可能是一个外部脚本文件(例如用Lua、Python等编写的脚本),我们的引擎必须在后台解析和执行。
  • 这是提供关键游戏信息和对象的非常常见的方法。我们通过将各种组件“插入”到我们的实体中来创建实体。
  • 许多书籍和文章讨论了如何实现实体-组件设计以及应该使用的数据结构。开发者经常讨论数据驱动设计、实体-组件-系统 (ECS)、数据局部性等概念,这些概念都与游戏数据在内存中的存储方式以及如何高效访问这些数据有关。使用的数据结构以及如何访问它们直接影响游戏的性能。
  • 在内存中表示和访问游戏对象可能很困难。您可以自己编写实体-组件实现,或者使用已有的第三方ECS 模块
  • 通过将几个流行的、现成的ECS 库包含到我们的 C++ 项目中,我们可以开始构建实体并将组件附加到它们上面,而不必担心它们在内部是如何实现的。EnTTFlecs是两个 C++ ECS 库的例子。
  • 即使实现不完美,从头开始构建 ECS 系统也能让您评估底层数据结构的性能。
  • 自定义即席 ECS 解决方案完成后,我们建议使用任何流行的第三方 ECS 库(EnTT、Flecs等)。这些是专业的库,已经过市场多年创建和测试。它们无疑比我们自己能创建的任何东西都好得多。
  • 总而言之,从头开始构建一个专业的ECS是具有挑战性的。作为学习练习是可以接受的,但在完成简短的学习项目后,请选择一个信誉良好的第三方 ECS 库并将其集成到游戏引擎的代码中。

6. 渲染

  • 我们游戏引擎的复杂性正在逐渐增加。现在我们已经讨论了在内存中存储访问游戏对象的方法,我们需要讨论如何将内容渲染到屏幕上。
  • 第一步是考虑引擎将用于制作哪种类型的游戏。是为了制作 2D 游戏而构建引擎吗?如果是这样,引擎应该考虑绘制精灵、管理层、渲染纹理并使用图形卡加速。好消息是,2D 游戏通常比 3D 游戏更容易理解,并且 2D 数学比 3D 数学简单得多。
    How to create a game engine in C++
  • SDL可以帮助实现跨平台渲染以创建2D 引擎。SDL 可以解码和显示PNG 图像、绘制精灵,并在我们的游戏窗口中渲染纹理。它还可以封装加速的 GPU 硬件。
  • 如果我们的目标是创建3D 引擎,我们将需要指定如何将更多3D 数据(如顶点、纹理着色器)传递给 GPU。图形硬件软件抽象最流行的选择是OpenGL、Direct3D、VulkanMetal。选择使用哪个API可能会受到我们目标平台的影响。例如,Metal仅与Apple 设备兼容,而Direct3D将支持Microsoft 软件
  • 图形管线用于处理 3D 应用程序中的 3D 数据。您的引擎必须如何将图形数据(例如顶点、纹理坐标、法线等)提供给 GPU 由此管线决定。
    How to create a game engine in C++
  • 我们必须通过图形 API管线开发可编程着色器,以适应和修改我们3D 场景的顶点和像素。
  • 可编程着色器决定了 GPU 对3D 对象处理和显示。不同的脚本可用于为每个顶点和每个像素(片段)调整反射、平滑度、颜色、透明度等。
  • 说到顶点和 3D 对象,最好信任一个库来处理不同网格格式的转换。大多数第三方 3D 引擎必须熟悉几种常见的 3D 模型格式。
  • 一些库已经经过充分测试得到支持,可以处理 C++ 的OBJ 加载。许多游戏引擎都使用优秀的选项TinyOBJLoaderAssImp

7. 物理

  • 我们希望添加到我们引擎中的实体能够在场景中移动、旋转反弹。物理模拟是游戏引擎的一部分。它可以手动构建,也可以从物理引擎导入。
  • 这里还需要考虑我们想要建模的物理。虽然2D 物理通常比3D更容易理解,但 2D 和 3D 引擎之间共享许多基本方面。
  • 如果您想为您的项目添加物理库,有几种出色的解决方案。
    How to create a game engine in C++
  • 我们建议查看Box2DChipmunk2D以实现2D 物理。像PhysXBullet这样的库是可靠且专业的 3D 物理模拟的强大选择。如果物理稳定性和开发速度对您的项目很重要,使用第三方物理引擎始终是一个不错的选择。
  • 每个程序员都应该在职业生涯的某个阶段学习如何创建一个基本的物理引擎。同样,我们不必创建一个完美的物理模拟;相反,我们关注事物如何加速以及如何将不同的力施加到游戏中的对象上。
  • 一旦移动完成,考虑添加一些基本的碰撞检测解析
  • 我们可以利用一些优秀的书籍和在线资源来了解更多关于物理引擎的知识。我们可以查看Box2D 源代码Erin Catto关于2D 刚体物理的幻灯片。但是,如果您正在寻找游戏机制的全面介绍,那么从零开始的 2D 游戏机制是一个不错的起点。
    How to create a game engine in C++
  • 如果您想了解3D 物理以及如何创建可靠的模拟David Eberly的著作《游戏物理学》是一本极好的资源。

8. UI(用户界面)

  • 现代游戏引擎,如UnityUnreal,会让人联想到复杂的用户界面,其中包含许多面板、滑块、拖放选项和其他吸引人的 UI 组件,让玩家可以自定义游戏世界。UI 使开发者能够快速调整游戏变量、添加和删除实体,以及即时更改组件设置。
    How to create a game engine in C++
  • 从头开始创建 UI 框架是新手程序员尝试添加到游戏引擎中最令人沮丧的任务之一。我们需要设计按钮、面板、对话框、滑块单选按钮,管理颜色,正确处理 UI 的事件,并始终维护其状态。如果我们向引擎中添加 UI 工具,应用程序的复杂性会增加,源代码也会变得非常混乱。
  • 如果目标是为引擎开发 UI 工具,我们建议使用现有的第三方 UI 库。正如我们通过简短的Google 搜索所见,最受欢迎的选择是Dear ImGui、QtNuklear
  • 我们可以使用Dear ImGui快速为引擎工具设置用户界面ImGui 项目采用的设计模式称为“即时模式 UI”。它因能够通过利用 GPU 加速渲染有效地与3D 应用程序进行交互而广受欢迎。
  • 总而言之,如果您想为游戏引擎添加 UI 功能,我们建议使用Dear ImGui

9. 脚本

  • 随着我们游戏引擎的发展,一个常见的选择是使用简单的脚本语言来实现关卡自定义。
  • 概念很简单。我们将脚本语言集成到我们的原生 C++ 应用程序中,以便非程序员可以编写实体行为、AI 逻辑、动画和其他关键游戏元素的脚本。
    How to create a game engine in C++
  • Lua、Wren、C#、PythonJavaScript是一些广泛用于视频游戏的脚本语言。所有这些语言的运行级别都远高于我们的原生C++ 代码。使用脚本语言编程游戏行为的人应该关注内存管理或核心引擎功能的其他低级方面。他们所要做的就是编写关卡脚本;我们的引擎将解释脚本并在后台处理繁重的工作。
  • Lua小巧、快速,并且非常容易与原生 C 和 C++ 程序集成。Sol 包提供了许多辅助方法来增强默认的Lua C-API,使开始使用 Lua 更加容易。
  • 如果我们启用脚本,我们可以讨论与我们的游戏引擎相关的更复杂的主题。使用外部脚本,我们可以轻松控制AI 逻辑、修改动画帧移动,并定义不需要包含在我们的原生 C++ 代码中的其他游戏行为。

10. 音频

  • 我们还应该考虑集成对音频的支持到一个游戏引擎中。
  • 我们必须再次通过操作系统访问音频设备以传递音频值并生成声音,这应该不足为奇。我们使用一个跨平台库来抽象音频硬件访问,因为我们通常不想创建特定于操作系统的功能
  • SDL这样的跨平台库的扩展可以帮助我们的引擎处理音频元素,如音乐音效
  • 只有当引擎的其他组件正常运行时,才需要考虑音频。生成声音文件可能很简单,但在将音频与动画、事件和其他游戏组件同步时,事情会变得复杂。
  • 如果我们在手动进行操作,音频可能会很困难,因为需要多线程管理。虽然可行,但如果我们想开发一个简单的游戏引擎,我们建议将这项任务交给专门的库。
  • SDL_Mixer、SoLoudFMOD是我们可能考虑集成到游戏引擎中的三个出色的音频库和技术。
    How to create a game engine in C++
  • 游戏《Tiny Combat Arena》使用了FMOD 库来实现音频效果,如压缩杜普勒效应。可以听到其他飞越飞机的3D 效果和加力燃烧器的声音。

11. 人工智能

  • 我们将把AI作为我们讨论的最后一个组成部分。我们可以通过脚本来创建AI,允许关卡设计者编写AI 逻辑。另一种选择是将真正的AI 系统集成到我们的游戏引擎的原生核心代码中。
  • AI应用于游戏元素,以提供响应式、适应性或类似智能的行为。大多数AI 逻辑被引入NPC和敌人,以模仿人类智能。
  • AI在视频游戏中的一个常见应用是敌人。游戏引擎可以抽象寻路算法或有趣的类似人类行为,当敌人在地图上追逐事物时。
  • Ian Millington《游戏人工智能》是一本关于视频游戏人工智能的理论和应用的全面书籍
    How to create a game engine in C++

不要试图一次性完成所有事情

我们最近介绍了几个我们想集成到一个简单的 C++ 游戏引擎中的关键概念。但是,在我们开始将所有这些元素组合在一起之前,请注意以下几点:

  • 开发游戏引擎可能具有挑战性,因为大多数开发人员不会指定明确的参数,而且没有“终点线”的概念。换句话说,程序员会开始一个游戏引擎项目,渲染内容,添加实体,添加组件,然后一切都变得一团糟。没有限制,很容易不断添加功能而失去对更大目标的关注。如果那样做,游戏引擎很可能永远不会被使用。
  • 除了缺乏约束之外,当我们在飞快的速度下看到代码在我们眼前不断扩展时,很容易不知所措。随着新功能的引入,游戏引擎项目随着时间的推移可能会变得越来越复杂。几周之内,C++ 项目可能就会有许多依赖项,需要复杂的构建系统,并且随着新功能的引入而失去可读性。

花点时间,专注于基础知识

  • 如果您正在将游戏引擎作为学习练习来构建,请享受您的小成就。
  • 项目初期,大多数人都很热情,但随着时间的推移,焦虑感会袭来。在从头开始构建游戏引擎时,很容易不知所措并失去动力,尤其是在使用 C++ 这样复杂的语言时。
  • 专注于基础。无论想法多么微小或简单,都要掌握它。