C++游戏开发
游戏引擎与C++游戏开发
C++ 游戏开发
本文档系统讲解 C++ 在游戏开发中的应用,覆盖游戏循环架构、实体-组件-系统 (ECS) 模式、内存管理、缓存友好设计、SIMD 优化、数据导向设计 (DOD)、主流引擎剖析与工程实践。内容遵循 ISO/IEC 14882:2023,参考 Unreal Engine、Unity、Godot 等主流引擎的实现,目标达到海外高校教学水准。
1. 学习目标
本节使用 Bloom 分类法刻画学习者应达到的认知层级。
1.1 记忆(Remember)
- 列举游戏循环 (game loop) 的三个核心阶段:输入处理 (input)、状态更新 (update)、渲染 (render)。
- 复述 ECS 三要素的定义:实体 (Entity)、组件 (Component)、系统 (System)。
- 背诵主流游戏引擎的名称:Unreal Engine、Unity、Godot、CryEngine、Source 2、Phaser。
- 列举 C++ 在游戏开发中的核心优势:性能、内存控制、跨平台、低延迟。
1.2 理解(Understand)
- 解释游戏循环与渲染循环 (render loop) 的区别:前者驱动逻辑更新,后者驱动画面绘制,二者通过垂直同步 (V-Sync) 协调。
- 阐述 ECS 相对传统 OOP 继承体系的优势:组合优于继承、缓存友好、并行友好。
- 描述 CPU 缓存层级(L1/L2/L3)对游戏性能的影响,以及缓存未命中 (cache miss) 的代价。
- 区分数据导向设计 (DOD) 与面向对象设计 (OOP) 的核心差异:前者以数据布局为中心,后者以行为抽象为中心。
1.3 应用(Apply)
- 使用 C++17 实现一个固定时间步长 (fixed-timestep) 的游戏循环,支持可变渲染帧率。
- 实现一个基本的 ECS 框架,包含实体管理、组件存储、系统调度。
- 使用
std::pmr或自定义分配器实现帧内 (per-frame) 内存分配器。 - 使用 SIMD intrinsics (
<immintrin.h>) 优化向量运算。
1.4 分析(Analyze)
- 对比 Unity 的 GameObject-Component 体系与 Unreal 的 Actor-Component 体系的设计差异。
- 解构数据局部性 (data locality) 对游戏性能的影响,通过缓存命中率分析定位瓶颈。
- 分析 ECS 中”原型 (archetype)“存储模型与”稀疏集合 (sparse set)“存储模型的取舍。
- 评估 C++20 协程在游戏异步任务(资源加载、网络)中的适用性。
1.5 评价(Evaluate)
- 评估在 MMO、FPS、RTS、独立游戏四类项目中应采用何种引擎架构。
- 判断特定游戏系统(物理、AI、渲染)是否应采用 ECS 或传统 OOP。
- 评审一份游戏代码的内存分配模式,识别堆分配热点。
1.6 创造(Create)
- 设计并实现一个完整的 2D 游戏引擎,包含 ECS、渲染、物理、音频、输入子系统。
- 构建一个基于 Job System 的多线程游戏任务调度器,支持依赖图。
- 为自定义游戏引擎编写热重载 (hot reload) 系统,支持运行时替换脚本与资源。
2. 历史动机与发展脉络
C++ 游戏开发的演进反映了硬件性能、设计模式与工具链的持续博弈。
2.1 早期:汇编与 C 主导(1970s - 1990s)
早期游戏(如《太空侵略者》、《吃豆人》)使用汇编语言编写,针对特定硬件优化。1980 年代 C 语言普及,成为游戏开发主流:
- 1985 年:任天堂红白机 (NES) 游戏多使用 6502 汇编。
- 1990 年代:PC 游戏转向 C,如 id Software 的《Doom》(1993) 使用 C 与少量汇编。
- 1996 年:《Quake》采用客户端-服务器架构,C 语言实现。
C 语言的优势:性能接近汇编、跨平台、内存透明。但缺乏抽象机制,大型项目维护困难。
2.2 C++ 的引入(1990s - 2000s)
1990 年代后期,C++ 开始进入游戏开发:
- 1998 年:Unreal Engine 1 发布,使用 C++ 与自定义脚本。
- 1999 年:id Software 的《Quake III Arena》采用 C 与少量 C++。
- 2000 年代:主流商业引擎(Unreal、Source、CryEngine)全面采用 C++。
C++ 的优势:OOP 抽象、模板泛型、STL 容器、RAII 资源管理。但早期 C++ 编译器性能差,运行时开销(异常、RTTI)让游戏开发者顾虑。
2.3 OOP 继承体系的黄金期(2000s - 2010s)
2000 年代,游戏引擎普遍采用深继承体系:
GameObject
├── Actor
│ ├── Pawn
│ │ ├── Character
│ │ │ ├── PlayerCharacter
│ │ │ └── NPC
│ │ └── Vehicle
│ └── Weapon
└── StaticMesh
这种体系的问题:
- 继承深度爆炸:深层继承导致”菱形继承”与脆弱基类问题。
- 缓存不友好:对象分散在堆上,虚函数调用导致 vtable 跳转,缓存命中率低。
- 扩展困难:新增功能需修改继承树,影响所有子类。
- 并行困难:对象间隐式依赖,难以并行化。
2.4 ECS 模式的兴起(2010s - 至今)
为解决 OOP 继承体系的问题,业界转向 ECS (Entity-Component-System) 模式:
- 2007 年:Adam Martin 在博客中首次系统阐述 ECS。
- 2010 年代:Unity (DOTS)、Unreal Engine (Mass Entity)、Bevy、Flecs 等引擎采用 ECS。
- 2018 年:Unity 发布 DOTS (Data-Oriented Technology Stack),基于 ECS。
- 2022 年:Unreal Engine 5 发布 Mass Entity 框架。
ECS 的核心思想:
- 实体 (Entity):仅是 ID,无数据无行为。
- 组件 (Component):纯数据,无行为。
- 系统 (System):纯行为,处理特定组件组合的实体。
ECS 的优势:
- 组合优于继承:实体通过组合组件获得能力,避免继承树。
- 缓存友好:同类型组件连续存储,CPU 缓存命中率高。
- 并行友好:系统间无共享状态,易于多线程。
- 可扩展:新增功能只需新增系统,不影响现有代码。
2.5 数据导向设计 (DOD) 的普及(2010s - 至今)
数据导向设计 (Data-Oriented Design, DOD) 由 Mike Acton (Insomniac Games) 在 2014 年 CppCon 演讲中推广。其核心原则:
- 以数据布局为中心:先设计数据结构,再设计算法。
- 关注内存访问模式:缓存命中率是性能关键。
- 分离数据与行为:数据是被动载体,行为是独立函数。
- 批处理优先:批量处理数据,利用 SIMD 与流水线。
DOD 与 OOP 的对比:
// OOP 风格:对象包含数据与行为
class Enemy {
Vector3 position_;
float health_;
public:
void Update(float dt) { position_ += velocity_ * dt; }
};
std::vector<Enemy> enemies; // 对象分散,缓存差
for (auto& e : enemies) e.Update(dt); // 虚调用
// DOD 风格:数据与行为分离
struct EnemyData {
std::vector<Vector3> positions;
std::vector<float> healths;
std::vector<Vector3> velocities;
};
void UpdateEnemies(EnemyData& data, float dt) {
for (size_t i = 0; i < data.positions.size(); ++i) {
data.positions[i] += data.velocities[i] * dt; // 连续访问,缓存友好
}
}
2.6 现代 C++ 在游戏中的应用
C++11/14/17/20 的新特性逐步被游戏开发采纳:
- C++11:
constexpr(编译期计算)、std::atomic(无锁同步)、Lambda(回调)、std::thread(多线程)。 - C++14:
std::make_unique(智能指针)、泛型 Lambda。 - C++17:结构化绑定(简化 ECS 迭代)、
std::optional(可选返回)、std::string_view(零拷贝字符串)、if constexpr(编译期分支)。 - C++20:concept(模板约束)、协程(异步任务)、模块(替代头文件)、
std::span(数组视图)。 - C++23:
std::expected(错误处理)、std::print(格式化输出)。
游戏开发对 C++ 特性的保守态度:
- 异常:多数引擎禁用(
-fno-exceptions),因性能不可预测。 - RTTI:多数引擎禁用(
-fno-rtti),节省二进制体积。 - STL:部分引擎自实现容器(如
TArrayin Unreal),因 STL 性能不达预期。 new/delete:游戏循环中禁用,改用自定义分配器。
2.7 演进时间线
1972 早期游戏(汇编) Space Invaders
1985 NES 游戏(6502 汇编) Super Mario Bros.
1993 Doom (C) id Software
1996 Quake (C + 少量 C++) id Software
1998 Unreal Engine 1 (C++) Epic Games
2000s OOP 继承体系黄金期 Unreal, Source, CryEngine
2007 ECS 概念提出 Adam Martin
2014 DOD 推广 (Mike Acton) Insomniac Games
2018 Unity DOTS (ECS) Unity Technologies
2020 Unreal Engine 5 Epic Games
2022 Unreal Mass Entity Epic Games
2023 Bevy 0.10 (Rust ECS) Bevy 组织
2026 C++26 草案 反射、契约
3. 形式化定义
本节给出游戏开发相关的形式化定义,涵盖标准引用、ECS 数学模型与缓存模型。
3.1 ISO/IEC 14882 标准中的游戏相关条款
C++ 标准未为游戏定义专门条款,但以下条款与游戏密切相关:
- [intro.memory] 内存模型:对象、内存位置、字节的关系,影响数据布局。
- [intro.multithread] 多线程:原子操作与内存序,游戏多线程必需。
- [support.dynamic] 动态内存:
new/delete,游戏常禁用并自实现分配器。 - [allocators] 分配器:
std::allocator、std::pmr,游戏可定制。 - [simd] SIMD:C++26 引入
<simd>,标准化向量运算。 - [time] 时间:
std::chrono,游戏循环计时基础。
3.2 游戏循环的形式化模型
游戏循环可形式化为状态机:
其中:
- :时刻 的游戏状态。
- :时刻 的输入。
- :时间步长。
- :状态转移函数。
渲染函数:
固定时间步长 (fixed timestep) 模型:
更新频率固定(60 Hz),渲染频率可变。通过”累加器”模式实现:
3.3 ECS 的形式化模型
设组件类型集合 ,实体 是组件集合的子集:
系统 是一个二元组:
其中 是系统查询的组件组合(如 ), 是处理函数:
ECS 的执行模型:
即对每个系统 ,查询所有包含 组件的实体,应用 。
3.4 缓存命中率模型
CPU 缓存层级:
| 层级 | 容量 | 延迟(周期) | 带宽 |
|---|---|---|---|
| L1 | 32-64 KB | 3-4 | ~1 TB/s |
| L2 | 256-512 KB | 10-14 | ~500 GB/s |
| L3 | 4-32 MB | 30-50 | ~200 GB/s |
| RAM | 8-64 GB | 100-300 | ~50 GB/s |
缓存命中率 与平均访问时间 :
其中 是命中时间, 是未命中时间。游戏代码需最大化 。
3.5 内存分配的形式化
游戏内存分配器可形式化为函数:
分配器类型:
- 线性分配器 (Linear): 分配,不支持释放,适合帧内分配。
- 栈分配器 (Stack): 分配,LIFO 释放。
- 池分配器 (Pool): 分配/释放,固定大小。
- 空闲列表 (Free List): 分配/释放,任意大小。
- 伙伴分配器 (Buddy): 分配/释放,2 的幂次大小。
3.6 SIMD 数据并行模型
SIMD (Single Instruction Multiple Data) 允许一条指令处理多个数据。形式化地:
其中 是向量宽度(如 4 for SSE, 8 for AVX2, 16 for AVX-512)。
游戏中的 SIMD 应用:
- 向量运算:4D 向量加法、点积、叉积。
- 矩阵运算:4x4 矩阵乘法。
- 物理计算:刚体动力学、碰撞检测。
- 粒子系统:批量粒子更新。
3.7 多线程任务调度模型
游戏多线程任务调度可建模为有向无环图 (DAG):
其中 是任务节点, 是依赖边。调度目标是最小化总执行时间:
受限于依赖关系:
游戏引擎的 Job System 实现该模型,支持任务依赖、优先级、工作窃取 (work stealing)。
4. 理论推导与原理解析
4.1 游戏循环的时间步长分析
游戏循环的核心挑战是平衡更新稳定性与渲染流畅性。三种主流模式:
模式一:可变时间步长 (Variable Timestep)
优点:渲染流畅。缺点:物理不稳定(数值积分误差)。
模式二:固定时间步长 (Fixed Timestep)
优点:物理稳定。缺点:渲染卡顿(帧率与更新率绑定)。
模式三:累加器模式 (Accumulator)
double accumulator = 0.0;
const double dt = 1.0 / 60.0;
while (running) {
double frame_time = get_frame_time();
accumulator += frame_time;
while (accumulator >= dt) {
update(dt);
accumulator -= dt;
}
render(accumulator / dt); // 插值 alpha
}
累加器模式结合固定更新与可变渲染,是现代游戏引擎的标准方案。
时间步长的数学分析:
设物理状态 满足微分方程 。数值积分(显式欧拉):
误差阶 。 越小,误差越小但计算量越大。固定 在多数游戏中是性能与精度的平衡点。
4.2 ECS 缓存性能分析
对比 OOP 与 ECS 的缓存性能。假设 10000 个敌人,每个敌人有 Position (12B) 与 Health (4B)。
OOP 布局:
class Enemy {
Vector3 position; // 12B
float health; // 4B
// 其他字段...
AIState ai; // 64B
Animation anim; // 128B
};
// sizeof(Enemy) ≈ 208B
std::vector<Enemy> enemies(10000);
遍历 Position 的内存访问:
- 敌人对象大小 208B,Position 在前 12B。
- 访问 10000 个 Position 需 10000 × 208B = 2.08 MB,超出 L2 缓存。
- 缓存行 64B,每个敌人占 3.25 个缓存行,每次访问位置加载 64B 但仅用 12B,命中率 18.75%。
ECS 布局:
struct PositionComponent { Vector3 position; }; // 12B
struct HealthComponent { float health; }; // 4B
std::vector<PositionComponent> positions(10000);
std::vector<HealthComponent> healths(10000);
遍历 Position 的内存访问:
- 10000 个 Position 连续存储,共 120 KB,放入 L2。
- 缓存行 64B 可容纳 5 个 Position,命中率接近 100%。
性能差距:ECS 的位置遍历比 OOP 快 5-10 倍(实测)。
4.3 内存分配器性能分析
游戏循环中频繁分配/释放内存导致碎片化与性能下降。对比三种分配器:
new/delete(系统分配器):
- 每次分配涉及系统调用(如
malloc),约 100-300 周期。 - 碎片化导致长期性能下降。
- 多线程竞争锁。
线性分配器:
class LinearAllocator {
char* begin_;
char* current_;
char* end_;
public:
void* allocate(size_t size) {
void* p = current_;
current_ += size;
return p;
}
void reset() { current_ = begin_; } // 整批释放
};
- 分配 ,约 5-10 周期。
- 不支持单次释放,仅支持
reset()。 - 适合帧内分配:每帧开始
reset(),帧内任意分配。
池分配器:
template<typename T>
class PoolAllocator {
std::vector<T*> free_list_;
public:
T* allocate() {
if (free_list_.empty()) return nullptr;
T* p = free_list_.back();
free_list_.pop_back();
return p;
}
void deallocate(T* p) {
free_list_.push_back(p);
}
};
- 分配/释放 。
- 固定大小,无碎片。
- 适合频繁创建/销毁同类对象(如粒子、子弹)。
性能对比表:
| 分配器 | 分配时间(周期) | 释放时间(周期) | 碎片 | 多线程 |
|---|---|---|---|---|
new | 100-300 | 100-300 | 是 | 锁竞争 |
| 线性 | 5-10 | N/A(reset) | 无 | 否 |
| 栈 | 5-10 | 5-10 | 无 | 否 |
| 池 | 10-20 | 10-20 | 无 | 可实现 |
| 空闲列表 | 50-100 | 50-100 | 是 | 锁竞争 |
4.4 SIMD 向量运算优化
4D 向量加法的标量与 SIMD 对比:
标量版本:
struct Vector4 {
float x, y, z, w;
};
Vector4 add(Vector4 a, Vector4 b) {
return {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w};
}
汇编(x86-64):
movss xmm0, [a]
addss xmm0, [b]
movss [result], xmm0
; 重复 4 次
约 12 条指令,16 周期。
SSE 版本:
#include <immintrin.h>
__m128 add_sse(__m128 a, __m128 b) {
return _mm_add_ps(a, b);
}
汇编:
addps xmm0, xmm1
1 条指令,1-3 周期。性能提升 5-10 倍。
AVX 版本(8 宽):
__m256 add_avx(__m256 a, __m256 b) {
return _mm256_add_ps(a, b);
}
1 条指令处理 8 个 float,性能再翻倍。
4.5 多线程 Job System 分析
游戏多线程的挑战:任务依赖、负载均衡、数据竞争。Job System 的核心数据结构:
struct Job {
std::function<void()> task;
std::vector<Job*> dependencies; // 依赖任务
std::atomic<int> counter; // 未完成依赖计数
};
class JobSystem {
std::vector<std::thread> workers_;
ConcurrentQueue<Job*> ready_queue_; // 就绪队列
public:
void submit(Job* job) {
if (job->counter == 0) {
ready_queue_.push(job);
}
}
void wait(Job* job) {
while (job->counter > 0) {
// 工作窃取:尝试执行其他就绪任务
Job* stolen;
if (ready_queue_.try_pop(stolen)) {
stolen->task();
}
}
}
};
工作窃取 (work stealing) 算法:
- 每个工作线程有本地双端队列 (deque)。
- 工作线程从本地队列 LIFO 取任务(缓存友好)。
- 空闲时从其他线程 FIFO 窃取任务(负载均衡)。
时间复杂度:
- 本地取任务:。
- 窃取任务:(平均),最坏 。
4.6 物理引擎的数值积分
刚体物理的数值积分方法:
显式欧拉 (Explicit Euler):
优点:简单。缺点:能量泄漏,长时间不稳定。
半隐式欧拉 (Semi-implicit Euler):
优点:稳定(对简谐运动),游戏常用。缺点:精度低。
Verlet 积分:
优点:稳定,能量守恒。缺点:需存储 。
RK4 (Runge-Kutta 4):
优点:高精度()。缺点:计算量大(4 次求值)。
游戏物理引擎(如 Bullet、PhysX)多采用半隐式欧拉,兼顾性能与稳定性。
4.7 资源管理的智能指针分析
游戏资源(纹理、网格、音频)的生命周期管理。对比三种方案:
裸指针:
Texture* tex = new Texture("player.png");
// ... 忘记 delete,内存泄漏
引用计数 (shared_ptr):
auto tex = std::make_shared<Texture>("player.png");
// 引用计数为 0 时自动释放
缺点:原子操作开销(约 20-50 周期/次),循环引用泄漏。
句柄 (Handle) 系统:
struct TextureHandle {
uint32_t index; // 资源索引
uint32_t generation; // 代数(防止悬空)
};
class TextureManager {
std::vector<Texture*> textures_;
std::vector<uint32_t> generations_;
public:
TextureHandle load(const std::string& path);
Texture* get(TextureHandle h); // 检查代数,返回 nullptr 若已释放
};
优点:
- 8 字节句柄,传递高效。
- 代数检查防止悬空访问。
- 集中管理,便于卸载。
现代游戏引擎(如 Unreal)多采用句柄系统替代智能指针。
5. 代码示例
5.1 固定时间步长游戏循环
标准:C++17
// game_loop.cpp
// 固定时间步长游戏循环,支持可变渲染帧率
#include <chrono>
#include <iostream>
class Game {
public:
void run() {
constexpr double fixed_dt = 1.0 / 60.0; // 60 Hz 更新
constexpr double max_frame_time = 0.25; // 最大帧时间,避免死循环
double accumulator = 0.0;
auto last_time = std::chrono::high_resolution_clock::now();
while (running_) {
auto current_time = std::chrono::high_resolution_clock::now();
double frame_time = std::chrono::duration<double>(
current_time - last_time).count();
last_time = current_time;
// 防止暂停后死循环
if (frame_time > max_frame_time) {
frame_time = max_frame_time;
}
accumulator += frame_time;
// 固定步长更新
while (accumulator >= fixed_dt) {
update(fixed_dt);
accumulator -= fixed_dt;
}
// 渲染插值因子
double alpha = accumulator / fixed_dt;
render(alpha);
}
}
void stop() { running_ = false; }
private:
bool running_ = true;
void update(double dt) {
// 物理更新、AI、游戏逻辑
process_input();
update_physics(dt);
update_ai(dt);
}
void render(double alpha) {
// 渲染:使用 alpha 在上一帧与当前帧状态间插值
interpolate_state(alpha);
draw_frame();
}
void process_input() { /* ... */ }
void update_physics(double dt) { /* ... */ }
void update_ai(double dt) { /* ... */ }
void interpolate_state(double alpha) { /* ... */ }
void draw_frame() { /* ... */ }
};
int main() {
Game game;
game.run();
return 0;
}
5.2 ECS 框架实现
标准:C++17
// ecs.h
// 最小 ECS 框架实现
#pragma once
#include <bitset>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <typeindex>
#include <memory>
namespace ecs {
using EntityId = std::uint64_t;
constexpr std::size_t MAX_COMPONENTS = 64;
using ComponentMask = std::bitset<MAX_COMPONENTS>;
// 组件类型注册
class ComponentRegistry {
inline static std::size_t next_id_ = 0;
inline static std::unordered_map<std::type_index, std::size_t> type_to_id_{};
public:
template<typename T>
static std::size_t get_id() {
auto type = std::type_index(typeid(T));
auto it = type_to_id_.find(type);
if (it == type_to_id_.end()) {
std::size_t id = next_id_++;
type_to_id_[type] = id;
return id;
}
return it->second;
}
};
// 组件存储(稀疏集合)
class IComponentArray {
public:
virtual ~IComponentArray() = default;
virtual void remove(EntityId entity) = 0;
};
template<typename T>
class ComponentArray : public IComponentArray {
std::unordered_map<EntityId, T> data_;
public:
void insert(EntityId entity, T component) {
data_[entity] = std::move(component);
}
T* get(EntityId entity) {
auto it = data_.find(entity);
return it == data_.end() ? nullptr : &it->second;
}
void remove(EntityId entity) override {
data_.erase(entity);
}
};
// 实体管理器
class EntityManager {
std::vector<EntityId> entities_;
std::vector<ComponentMask> masks_;
EntityId next_id_ = 0;
public:
EntityId create() {
EntityId id = next_id_++;
entities_.push_back(id);
masks_.emplace_back();
return id;
}
void destroy(EntityId entity) {
// 简化:标记为空 mask
if (entity < masks_.size()) {
masks_[entity].reset();
}
}
template<typename T>
void add_component(EntityId entity) {
masks_[entity].set(ComponentRegistry::get_id<T>());
}
template<typename... Ts>
bool has_components(EntityId entity) const {
ComponentMask required;
(required.set(ComponentRegistry::get_id<Ts>()), ...);
return (masks_[entity] & required) == required;
}
const std::vector<EntityId>& all() const { return entities_; }
};
// 系统
class System {
public:
virtual ~System() = default;
virtual void update(float dt) = 0;
};
// 世界
class World {
EntityManager entities_;
std::unordered_map<std::type_index, std::unique_ptr<IComponentArray>> components_;
std::vector<std::unique_ptr<System>> systems_;
public:
EntityId create_entity() {
return entities_.create();
}
template<typename T>
void add_component(EntityId entity, T component) {
auto type = std::type_index(typeid(T));
if (components_.find(type) == components_.end()) {
components_[type] = std::make_unique<ComponentArray<T>>();
}
static_cast<ComponentArray<T>*>(components_[type].get())
->insert(entity, std::move(component));
entities_.add_component<T>(entity);
}
template<typename T>
T* get_component(EntityId entity) {
auto type = std::type_index(typeid(T));
auto it = components_.find(type);
if (it == components_.end()) return nullptr;
return static_cast<ComponentArray<T>*>(it->second.get())->get(entity);
}
template<typename... Ts>
std::vector<EntityId> query() {
std::vector<EntityId> result;
for (auto e : entities_.all()) {
if (entities_.has_components<Ts...>(e)) {
result.push_back(e);
}
}
return result;
}
void add_system(std::unique_ptr<System> system) {
systems_.push_back(std::move(system));
}
void update(float dt) {
for (auto& system : systems_) {
system->update(dt);
}
}
};
} // namespace ecs
5.3 使用 ECS 的示例
// main.cpp
#include "ecs.h"
#include <iostream>
#include <cmath>
struct Position { float x, y, z; };
struct Velocity { float x, y, z; };
class MovementSystem : public ecs::System {
ecs::World& world_;
public:
explicit MovementSystem(ecs::World& w) : world_(w) {}
void update(float dt) override {
auto entities = world_.query<Position, Velocity>();
for (auto e : entities) {
auto* pos = world_.get_component<Position>(e);
auto* vel = world_.get_component<Velocity>(e);
pos->x += vel->x * dt;
pos->y += vel->y * dt;
pos->z += vel->z * dt;
}
}
};
int main() {
ecs::World world;
// 创建实体并添加组件
auto e1 = world.create_entity();
world.add_component(e1, Position{0, 0, 0});
world.add_component(e1, Velocity{1, 0, 0});
auto e2 = world.create_entity();
world.add_component(e2, Position{10, 0, 0});
world.add_component(e2, Velocity{0, 1, 0});
// 注册系统
world.add_system(std::make_unique<MovementSystem>(world));
// 游戏循环
for (int i = 0; i < 60; ++i) {
world.update(1.0f / 60.0f);
}
auto* p1 = world.get_component<Position>(e1);
std::cout << "Entity 1 position: (" << p1->x << ", " << p1->y << ", " << p1->z << ")\n";
// 输出:Entity 1 position: (1, 0, 0)
return 0;
}
5.4 线性分配器实现
标准:C++17
// linear_allocator.h
// 帧内线性分配器,O(1) 分配,整批释放
#pragma once
#include <cstddef>
#include <cstdint>
#include <new>
class LinearAllocator {
public:
LinearAllocator(std::size_t size)
: buffer_(new std::byte[size]),
capacity_(size),
offset_(0) {}
~LinearAllocator() {
delete[] buffer_;
}
// 禁用拷贝
LinearAllocator(const LinearAllocator&) = delete;
LinearAllocator& operator=(const LinearAllocator&) = delete;
// 分配内存
void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
std::size_t aligned = align_up(offset_, alignment);
if (aligned + size > capacity_) {
return nullptr; // 内存不足
}
void* ptr = buffer_ + aligned;
offset_ = aligned + size;
return ptr;
}
// 整批释放
void reset() {
offset_ = 0;
}
std::size_t used() const { return offset_; }
std::size_t capacity() const { return capacity_; }
private:
static std::size_t align_up(std::size_t value, std::size_t alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}
std::byte* buffer_;
std::size_t capacity_;
std::size_t offset_;
};
// STL 兼容分配器
template<typename T>
class LinearAllocatorAdapter {
public:
using value_type = T;
LinearAllocatorAdapter(LinearAllocator& alloc) : alloc_(&alloc) {}
template<typename U>
LinearAllocatorAdapter(const LinearAllocatorAdapter<U>& other) noexcept
: alloc_(other.alloc_) {}
T* allocate(std::size_t n) {
return static_cast<T*>(alloc_->allocate(n * sizeof(T), alignof(T)));
}
void deallocate(T*, std::size_t) noexcept {
// 线性分配器不支持单次释放
}
LinearAllocator* alloc_;
};
template<typename T, typename U>
bool operator==(const LinearAllocatorAdapter<T>&, const LinearAllocatorAdapter<U>&) {
return true; // 所有 LinearAllocatorAdapter 等价
}
使用示例:
#include "linear_allocator.h"
#include <vector>
int main() {
LinearAllocator alloc(1024 * 1024); // 1MB
// 每帧重置
alloc.reset();
// 使用 STL 兼容分配器
LinearAllocatorAdapter<int> adapter(alloc);
std::vector<int, LinearAllocatorAdapter<int>> vec(adapter);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
return 0;
}
5.5 池分配器实现
标准:C++17
// pool_allocator.h
// 固定大小对象池,O(1) 分配/释放
#pragma once
#include <cstddef>
#include <vector>
template<typename T>
class PoolAllocator {
public:
explicit PoolAllocator(std::size_t initial_capacity = 1024) {
free_list_.reserve(initial_capacity);
for (std::size_t i = 0; i < initial_capacity; ++i) {
free_list_.push_back(new T());
}
}
~PoolAllocator() {
for (T* obj : free_list_) {
delete obj;
}
}
// 禁用拷贝
PoolAllocator(const PoolAllocator&) = delete;
PoolAllocator& operator=(const PoolAllocator&) = delete;
template<typename... Args>
T* construct(Args&&... args) {
T* obj = acquire();
new (obj) T(std::forward<Args>(args)...);
return obj;
}
void destroy(T* obj) {
obj->~T();
release(obj);
}
private:
T* acquire() {
if (free_list_.empty()) {
return new T();
}
T* obj = free_list_.back();
free_list_.pop_back();
return obj;
}
void release(T* obj) {
free_list_.push_back(obj);
}
std::vector<T*> free_list_;
};
5.6 SIMD 向量运算
标准:C++17,需要 SSE/AVX 支持
// simd_vector.h
// SIMD 优化的 4D 向量运算
#pragma once
#include <immintrin.h>
#include <cmath>
struct alignas(16) Vector4 {
union {
__m128 v;
struct { float x, y, z, w; };
};
Vector4() : v(_mm_setzero_ps()) {}
Vector4(__m128 vec) : v(vec) {}
Vector4(float x, float y, float z, float w = 0.0f)
: v(_mm_set_ps(w, z, y, x)) {}
};
// 加法
inline Vector4 operator+(Vector4 a, Vector4 b) {
return Vector4(_mm_add_ps(a.v, b.v));
}
// 减法
inline Vector4 operator-(Vector4 a, Vector4 b) {
return Vector4(_mm_sub_ps(a.v, b.v));
}
// 标量乘法
inline Vector4 operator*(Vector4 a, float s) {
return Vector4(_mm_mul_ps(a.v, _mm_set1_ps(s)));
}
// 点积
inline float dot(Vector4 a, Vector4 b) {
__m128 mul = _mm_mul_ps(a.v, b.v);
__m128 shuf = _mm_shuffle_ps(mul, mul, _MM_SHUFFLE(2, 3, 0, 1));
__m128 sums = _mm_add_ps(mul, shuf);
shuf = _mm_movehl_ps(sums, sums);
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
// 叉积
inline Vector4 cross(Vector4 a, Vector4 b) {
return Vector4(_mm_sub_ps(
_mm_mul_ps(_mm_shuffle_ps(a.v, a.v, _MM_SHUFFLE(3, 0, 2, 1)),
_mm_shuffle_ps(b.v, b.v, _MM_SHUFFLE(3, 1, 0, 2))),
_mm_mul_ps(_mm_shuffle_ps(a.v, a.v, _MM_SHUFFLE(3, 1, 0, 2)),
_mm_shuffle_ps(b.v, b.v, _MM_SHUFFLE(3, 0, 2, 1)))
));
}
// 长度
inline float length(Vector4 a) {
return std::sqrt(dot(a, a));
}
// 归一化
inline Vector4 normalize(Vector4 a) {
__m128 dp = _mm_sqrt_ps(_mm_dp_ps(a.v, a.v, 0xF1));
return Vector4(_mm_div_ps(a.v, dp));
}
5.7 简单 Job System
标准:C++17
// job_system.h
// 简单多线程 Job System,支持任务依赖
#pragma once
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
class JobSystem {
public:
using Job = std::function<void()>;
explicit JobSystem(std::size_t worker_count = std::thread::hardware_concurrency()) {
for (std::size_t i = 0; i < worker_count; ++i) {
workers_.emplace_back([this] { worker_loop(); });
}
}
~JobSystem() {
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
cv_.notify_all();
for (auto& w : workers_) {
if (w.joinable()) w.join();
}
}
void submit(Job job) {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push_back(std::move(job));
}
cv_.notify_one();
}
void wait_all() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return queue_.empty() && active_ == 0; });
}
private:
void worker_loop() {
while (true) {
Job job;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return stopping_ || !queue_.empty(); });
if (stopping_ && queue_.empty()) return;
job = std::move(queue_.front());
queue_.pop_front();
++active_;
}
job();
{
std::lock_guard<std::mutex> lock(mutex_);
--active_;
}
cv_.notify_all();
}
}
std::vector<std::thread> workers_;
std::deque<Job> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::size_t active_ = 0;
bool stopping_ = false;
};
5.8 完整 2D 游戏骨架
标准:C++17
// game2d.cpp
// 完整 2D 游戏骨架:ECS + 游戏循环 + 简单物理
#include "ecs.h"
#include "linear_allocator.h"
#include <chrono>
#include <cmath>
#include <cstdio>
#include <vector>
struct Position2D { float x, y; };
struct Velocity2D { float x, y; };
struct Sprite { int texture_id; float w, h; };
struct PlayerTag {};
struct EnemyTag {};
class PhysicsSystem : public ecs::System {
ecs::World& world_;
public:
explicit PhysicsSystem(ecs::World& w) : world_(w) {}
void update(float dt) override {
auto entities = world_.query<Position2D, Velocity2D>();
for (auto e : entities) {
auto* pos = world_.get_component<Position2D>(e);
auto* vel = world_.get_component<Velocity2D>(e);
pos->x += vel->x * dt;
pos->y += vel->y * dt;
}
}
};
class BoundarySystem : public ecs::System {
ecs::World& world_;
float width_, height_;
public:
BoundarySystem(ecs::World& w, float w_, float h_)
: world_(w), width_(w_), height_(h_) {}
void update(float dt) override {
auto entities = world_.query<Position2D, Velocity2D>();
for (auto e : entities) {
auto* pos = world_.get_component<Position2D>(e);
auto* vel = world_.get_component<Velocity2D>(e);
if (pos->x < 0 || pos->x > width_) vel->x *= -1;
if (pos->y < 0 || pos->y > height_) vel->y *= -1;
}
}
};
class RenderSystem : public ecs::System {
ecs::World& world_;
public:
explicit RenderSystem(ecs::World& w) : world_(w) {}
void update(float dt) override {
auto entities = world_.query<Position2D, Sprite>();
for (auto e : entities) {
auto* pos = world_.get_component<Position2D>(e);
auto* spr = world_.get_component<Sprite>(e);
std::printf("Render entity %llu at (%.1f, %.1f) size %.0fx%.0f\n",
e, pos->x, pos->y, spr->w, spr->h);
}
}
};
int main() {
ecs::World world;
// 创建玩家
auto player = world.create_entity();
world.add_component(player, Position2D{100, 100});
world.add_component(player, Velocity2D{50, 0});
world.add_component(player, Sprite{1, 32, 32});
world.add_component(player, PlayerTag{});
// 创建敌人
for (int i = 0; i < 5; ++i) {
auto enemy = world.create_entity();
world.add_component(enemy, Position2D{
static_cast<float>(200 + i * 50),
static_cast<float>(200 + i * 30)
});
world.add_component(enemy, Velocity2D{30, -20});
world.add_component(enemy, Sprite{2, 24, 24});
world.add_component(enemy, EnemyTag{});
}
// 注册系统
world.add_system(std::make_unique<PhysicsSystem>(world));
world.add_system(std::make_unique<BoundarySystem>(world, 800.0f, 600.0f));
world.add_system(std::make_unique<RenderSystem>(world));
// 游戏循环
constexpr double fixed_dt = 1.0 / 60.0;
double accumulator = 0.0;
auto last_time = std::chrono::high_resolution_clock::now();
for (int frame = 0; frame < 60; ++frame) {
auto current_time = std::chrono::high_resolution_clock::now();
double frame_time = std::chrono::duration<double>(
current_time - last_time).count();
last_time = current_time;
accumulator += frame_time;
while (accumulator >= fixed_dt) {
world.update(static_cast<float>(fixed_dt));
accumulator -= fixed_dt;
}
}
return 0;
}
6. 对比分析
6.1 游戏引擎对比
| 引擎 | 语言 | 架构 | ECS 支持 | 开源 | 授权 |
|---|---|---|---|---|---|
| Unreal Engine | C++ | OOP + ECS | Mass Entity | 否 | 商业(版税) |
| Unity | C# (脚本) + C++ | OOP + ECS | DOTS | 否 | 商业(订阅) |
| Godot | C++ + GDScript | OOP + Node | 进行中 | 是 | MIT |
| CryEngine | C++ | OOP | 部分 | 否 | 商业 |
| Source 2 | C++ | OOP | 部分 | 否 | 商业 |
| Bevy | Rust | ECS | 原生 | 是 | MIT |
| Flecs | C | ECS | 原生 | 是 | MIT |
| EnTT | C++ | ECS (header) | 原生 | 是 | MIT |
6.2 ECS 存储模型对比
| 存储模型 | 描述 | 优势 | 劣势 |
|---|---|---|---|
| 稀疏集合 | 每个组件类型独立 unordered_map | 实现简单 | 缓存差,遍历慢 |
| 原型 (Archetype) | 相同组件组合的实体集中存储 | 缓存极佳,遍历快 | 组件增删开销大 |
| 位集 (Bitset) | 用位集标记实体组件 | 查询灵活 | 内存占用高 |
Unity DOTS、Unreal Mass、Flecs、Bevy 均采用 Archetype 模型。
6.3 内存分配器对比
| 分配器 | 分配复杂度 | 释放复杂度 | 碎片 | 适用场景 |
|---|---|---|---|---|
| 系统 malloc | 是 | 通用 | ||
| 线性 | N/A | 无 | 帧内临时分配 | |
| 栈 | 无 | 嵌套作用域 | ||
| 池 | 无 | 同类对象 | ||
| 空闲列表 | 是 | 任意大小 | ||
| 伙伴 | 是 | 2 的幂次大小 |
6.4 物理 vs 渲染循环频率
| 系统 | 典型频率 | 原因 |
|---|---|---|
| 物理 | 30-60 Hz | 稳定性要求,高频不稳定 |
| 渲染 | 60-240 Hz | 流畅性要求,可变 |
| AI | 10-30 Hz | 决策频率低,节省 CPU |
| 输入 | 60-120 Hz | 响应性要求 |
| 网络 | 20-60 Hz | 带宽与延迟平衡 |
| 动画 | 30-60 Hz | 与渲染同步 |
6.5 C++ vs Rust vs C# 游戏开发
| 维度 | C++ | Rust | C# |
|---|---|---|---|
| 性能 | 最高 | 接近 C++ | 较低(GC 暂停) |
| 内存控制 | 完全 | 完全 | 受限(GC) |
| 生态 | 最成熟 | 成长中 | 成熟(Unity) |
| 学习曲线 | 陡峭 | 陡峭 | 平缓 |
| 安全性 | 弱(UB 多) | 强(借用检查) | 强(GC) |
| 引擎支持 | Unreal, 自研 | Bevy | Unity |
| 热重载 | 困难 | 困难 | 容易 |
7. 常见陷阱与最佳实践
7.1 十大常见陷阱
陷阱 1:游戏循环中使用可变时间步长
// 反例:物理不稳定
float dt = get_frame_time();
update(dt); // 若 dt 忽大忽小,物理跳跃
正例:使用固定时间步长(见 5.1 节)。
陷阱 2:游戏循环中使用 new/delete
// 反例:每帧分配,碎片化
void update() {
auto enemies = new Enemy[1000]; // 堆分配
// ...
delete[] enemies;
}
正例:使用池分配器或线性分配器。
陷阱 3:虚函数滥用
// 反例:每帧虚调用,缓存差
class Entity {
public:
virtual void update() = 0;
};
std::vector<Entity*> entities;
for (auto e : entities) e->update(); // 虚调用,缓存差
正例:使用 ECS,系统直接处理连续数据。
陷阱 4:分支预测失败
// 反例:分支密集
for (auto& e : enemies) {
if (e.type == EnemyType::Goblin) {
// ...
} else if (e.type == EnemyType::Orc) {
// ...
}
}
正例:按类型分组,避免分支。
陷阱 5:共享指针循环引用
// 反例:内存泄漏
struct Node {
std::shared_ptr<Node> parent;
std::shared_ptr<Node> child; // 父子互引,泄漏
};
正例:使用 weak_ptr 或句柄系统。
陷阱 6:锁竞争
// 反例:所有线程争抢同一锁
std::mutex mtx;
void update() {
std::lock_guard<std::mutex> lk(mtx); // 串行化
// ...
}
正例:使用 Job System + 无锁队列 + 数据分片。
陷阱 7:缓存未命中的数据布局
// 反例:按对象布局
struct Enemy {
Vector3 pos; // 12B
float health; // 4B
AIState ai; // 64B
Animation anim; // 128B
};
// sizeof(Enemy) = 208B,遍历 pos 缓存差
正例:按组件布局(ECS)。
陷阱 8:过度抽象
// 反例:多层抽象
class IRenderer { virtual void draw() = 0; };
class IShapeRenderer : public IRenderer { ... };
class ITriangleRenderer : public IShapeRenderer { ... };
class OpenGLTriangleRenderer : public ITriangleRenderer { ... };
正例:直接实现,避免过早抽象。
陷阱 9:忽略 SIMD
// 反例:标量循环
for (int i = 0; i < count; ++i) {
result[i] = a[i] + b[i];
}
正例:使用 SIMD(手动或编译器自动向量化)。
陷阱 10:资源加载阻塞主线程
// 反例:主线程加载资源
void load_level() {
auto tex = load_texture("big.png"); // 阻塞!
}
正例:异步加载(线程池、协程)。
7.2 最佳实践清单
- 使用固定时间步长:物理稳定,可重现。
- 使用 ECS 架构:缓存友好,可扩展。
- 使用自定义分配器:避免
new/delete。 - 数据局部性优先:连续存储同类型数据。
- SIMD 优化热点:向量、矩阵、粒子。
- 多线程 Job System:充分利用多核。
- 异步资源加载:避免主线程阻塞。
- 资源句柄系统:替代智能指针。
- 禁用异常与 RTTI:性能与二进制体积。
- 编译期计算:
constexpr减少运行时开销。 - Profile 驱动优化:先测量,再优化。
- 数据对齐:
alignas(16)保证 SIMD 对齐。 - 避免分支:按类型分组,减少预测失败。
- 批处理:批量提交渲染命令。
- 对象池:复用对象,避免频繁分配。
8. 工程实践
8.1 项目结构
my_game/
├── CMakeLists.txt
├── src/
│ ├── core/
│ │ ├── game_loop.cpp
│ │ ├── world.cpp
│ │ └── engine.cpp
│ ├── ecs/
│ │ ├── entity.cpp
│ │ ├── component.cpp
│ │ └── system.cpp
│ ├── renderer/
│ │ ├── renderer.cpp
│ │ ├── shader.cpp
│ │ └── texture.cpp
│ ├── physics/
│ │ ├── world.cpp
│ │ ├── body.cpp
│ │ └── collision.cpp
│ ├── audio/
│ │ └── audio.cpp
│ ├── input/
│ │ └── input.cpp
│ ├── resources/
│ │ ├── manager.cpp
│ │ └── loader.cpp
│ ├── memory/
│ │ ├── allocator.cpp
│ │ └── pool.cpp
│ └── main.cpp
├── assets/
│ ├── textures/
│ ├── models/
│ ├── shaders/
│ └── audio/
├── third_party/
│ ├── SDL2/
│ ├── glad/
│ └── glm/
└── tests/
└── ...
8.2 CMake 配置
cmake_minimum_required(VERSION 3.20)
project(my_game LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 编译选项
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(
-O3
-march=native
-Wall -Wextra
-fno-exceptions
-fno-rtti
-ffast-math
)
endif()
# SIMD 选项
add_compile_options(-msse4.1 -mavx2)
# 主程序
add_executable(my_game
src/main.cpp
src/core/game_loop.cpp
src/core/world.cpp
src/ecs/entity.cpp
src/renderer/renderer.cpp
src/physics/world.cpp
src/audio/audio.cpp
src/input/input.cpp
src/resources/manager.cpp
src/memory/allocator.cpp
)
target_include_directories(my_game PRIVATE src third_party)
# 链接库
find_package(SDL2 REQUIRED)
target_link_libraries(my_game PRIVATE SDL2::SDL2 SDL2::SDL2main)
8.3 性能分析工具
游戏性能分析的核心工具:
- Tracy Profiler:实时帧分析,支持多线程。
- VTune(Intel):CPU 性能计数器,缓存命中率。
- RenderDoc:图形 API 调试。
- gDEBugger:OpenGL 调试。
- perf(Linux):系统级性能分析。
集成 Tracy 示例:
#include <tracy/Tracy.hpp>
void update(float dt) {
ZoneScoped; // 自动计时
{
ZoneScopedN("Physics");
update_physics(dt);
}
{
ZoneScopedN("AI");
update_ai(dt);
}
{
ZoneScopedN("Render");
render();
}
FrameMark; // 标记帧结束
}
8.4 跨平台开发
// platform.h
#pragma once
#if defined(_WIN32)
#define PLATFORM_WINDOWS 1
#elif defined(__APPLE__)
#define PLATFORM_MACOS 1
#elif defined(__linux__)
#define PLATFORM_LINUX 1
#elif defined(__ANDROID__)
#define PLATFORM_ANDROID 1
#elif defined(__APPLE__) && defined(__TARGET_OS_IPHONE)
#define PLATFORM_IOS 1
#endif
// 平台特定代码
namespace platform {
void initialize();
void shutdown();
void* load_library(const char* path);
void* get_function(void* library, const char* name);
std::string get_asset_path();
double get_high_resolution_time();
}
8.5 资源热重载
// hot_reload.h
// 资源热重载系统
#pragma once
#include <filesystem>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
class HotReloadManager {
public:
using ReloadCallback = std::function<void(const std::string&)>;
void watch(const std::string& path, ReloadCallback callback) {
watchers_[path] = {path, callback, get_modified_time(path)};
}
void check() {
for (auto& [path, watcher] : watchers_) {
auto current = get_modified_time(path);
if (current != watcher.last_modified) {
watcher.last_modified = current;
watcher.callback(path);
}
}
}
private:
struct Watcher {
std::string path;
ReloadCallback callback;
std::filesystem::file_time_type last_modified;
};
std::filesystem::file_time_type get_modified_time(const std::string& path) {
return std::filesystem::last_write_time(path);
}
std::unordered_map<std::string, Watcher> watchers_;
};
9. 案例研究
9.1 案例一:Unreal Engine 5
Unreal Engine 5 (UE5) 是 Epic Games 的旗舰引擎,代码量超过 1000 万行 C++。其架构特点:
- OOP + ECS 混合:传统 Actor-Component 体系 + Mass Entity (ECS)。
- Nanite:虚拟化几何体,自动 LOD。
- Lumen:全局光照,实时 GI。
- Niagara:粒子系统,基于 ECS。
- MetaHuman:高保真角色。
- GAS (Gameplay Ability System):能力系统,支持网络同步。
UE5 的 C++ 规范特点:
- 使用 Unreal Headers Tool (UHT) 生成反射代码。
UCLASS、UPROPERTY、UFUNCTION宏标记反射。- 智能指针:
TSharedPtr、TWeakPtr、TUniquePtr(非 STL)。 - 容器:
TArray、TMap、TSet(非 STL)。 - 字符串:
FString、FName、FText。
9.2 案例二:Unity DOTS
Unity DOTS (Data-Oriented Technology Stack) 是 Unity 的 ECS 重构:
- Entities:ECS 框架。
- Burst Compiler:基于 LLVM 的 C# JIT,性能接近 C++。
- C# Job System:多线程任务系统。
- Unity Mathematics:SIMD 优化的数学库。
DOTS 的性能:相比传统 MonoBehaviour,DOTS 在大量实体(10万+)场景下快 10-100 倍。
9.3 案例三:Bevy (Rust)
Bevy 是 Rust 编写的 ECS 游戏引擎,特点:
- 原生 ECS:所有功能基于 ECS。
- 插件系统:功能以插件形式组织。
- 热重载:Rust 热重载实验中。
- 类型安全:Rust 借用检查避免数据竞争。
Bevy 的启示:现代语言(Rust)在游戏开发中的潜力,但生态仍不及 C++。
9.4 案例四:id Software (Doom Eternal)
Doom Eternal 的引擎 (id Tech 7) 特点:
- Vulkan 渲染:低开销图形 API。
- 数据导向设计:缓存友好的数据布局。
- 异步计算:CPU 与 GPU 并行。
- 动态分辨率:保持 60 FPS。
- 流式加载:无加载画面。
9.5 案例五:Minecraft (Bedrock)
Minecraft Bedrock 版使用 C++ 重写,特点:
- 跨平台:Windows、Xbox、PS、Switch、移动端。
- 数据驱动:行为包、资源包 JSON 定义。
- ECS 架构:实体系统。
- 网络同步:客户端-服务器架构。
9.6 案例六:独立游戏 (Hollow Knight)
Hollow Knight 使用 Unity (C#),但其设计哲学值得 C++ 开发者借鉴:
- 状态机驱动:角色行为以 FSM 组织。
- 数据驱动:关卡、敌人配置外置。
- 可读性优先:避免过度工程化。
- 迭代开发:快速原型,逐步打磨。
10. 习题
10.1 选择题
题目 1:ECS 中的”实体 (Entity)“本质上是?
A. 包含数据与行为的对象 B. 仅是一个 ID C. 组件的集合类 D. 系统的实例
答案:B。实体仅是 ID,无数据无行为。
题目 2:游戏循环使用固定时间步长的主要原因是?
A. 提高渲染流畅度 B. 减少内存占用 C. 保证物理稳定性 D. 简化代码
答案:C。固定步长保证物理数值积分稳定。
题目 3:CPU 缓存未命中的典型代价是?
A. 1-3 周期 B. 10-14 周期 C. 100-300 周期 D. 1000+ 周期
答案:C。L3 未命中访问 RAM 约 100-300 周期。
题目 4:ECS 相对 OOP 继承体系的核心优势是?
A. 代码更简洁 B. 缓存友好与并行友好 C. 学习曲线更平缓 D. 编译速度更快
答案:B。ECS 的连续数据布局提升缓存命中,系统间无共享状态便于并行。
题目 5:线性分配器的主要限制是?
A. 分配速度慢 B. 不支持单次释放 C. 内存碎片严重 D. 多线程不安全
答案:B。线性分配器仅支持 reset() 整批释放,不支持单次释放。
题目 6:SIMD 的 SSE 指令一次可处理几个 float?
A. 2 B. 4 C. 8 D. 16
答案:B。SSE 寄存器 128 位,可处理 4 个 32 位 float。
题目 7:以下哪种数值积分方法对简谐运动最稳定?
A. 显式欧拉 B. 半隐式欧拉 C. 显式 RK4 D. 显式 Verlet
答案:B。半隐式欧拉对简谐运动稳定且计算量小,游戏常用。
题目 8:Job System 中的”工作窃取 (work stealing)“用于?
A. 减少内存分配 B. 负载均衡 C. 避免锁竞争 D. 简化代码
答案:B。空闲线程从其他线程队列窃取任务,实现负载均衡。
10.2 填空题
题目 1:ECS 的三要素是______、、。
答案:实体 (Entity)、组件 (Component)、系统 (System)。
题目 2:游戏循环的三个核心阶段是______、、。
答案:输入处理 (Input)、状态更新 (Update)、渲染 (Render)。
题目 3:CPU 缓存层级包括______、、、______。
答案:L1、L2、L3、RAM。
题目 4:ECS 的主流存储模型有______、、。
答案:稀疏集合 (Sparse Set)、原型 (Archetype)、位集 (Bitset)。
题目 5:游戏开发中常用的内存分配器包括______、、、______。
答案:线性 (Linear)、栈 (Stack)、池 (Pool)、空闲列表 (Free List)。
题目 6:C++ 游戏开发常禁用的两个特性是______和______。
答案:异常 (Exceptions)、RTTI。
题目 7:Unity DOTS 的核心组件包括______、、、______。
答案:Entities (ECS)、Burst Compiler、C# Job System、Unity Mathematics。
10.3 编程题
题目 1:实现一个简单的栈分配器,支持 LIFO 释放。
参考答案:
#pragma once
#include <cstddef>
#include <new>
class StackAllocator {
public:
StackAllocator(std::size_t size)
: buffer_(new std::byte[size]), capacity_(size), offset_(0) {}
~StackAllocator() { delete[] buffer_; }
StackAllocator(const StackAllocator&) = delete;
StackAllocator& operator=(const StackAllocator&) = delete;
struct Marker {
std::size_t offset;
};
Marker get_marker() const { return {offset_}; }
void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
std::size_t aligned = align_up(offset_, alignment);
if (aligned + size > capacity_) return nullptr;
offset_ = aligned + size;
return buffer_ + aligned;
}
void release_to_marker(Marker m) {
offset_ = m.offset;
}
private:
static std::size_t align_up(std::size_t v, std::size_t a) {
return (v + a - 1) & ~(a - 1);
}
std::byte* buffer_;
std::size_t capacity_;
std::size_t offset_;
};
// 使用示例
void example() {
StackAllocator alloc(1024);
auto marker = alloc.get_marker();
int* a = static_cast<int*>(alloc.allocate(sizeof(int)));
float* b = static_cast<float*>(alloc.allocate(sizeof(float)));
alloc.release_to_marker(marker); // 释放 a 和 b
}
题目 2:实现一个基于 ECS 的简单粒子系统。
参考答案:
#include "ecs.h"
#include <cmath>
#include <random>
struct Particle {
float x, y;
float vx, vy;
float life;
float r, g, b;
};
class ParticleSystem : public ecs::System {
ecs::World& world_;
public:
explicit ParticleSystem(ecs::World& w) : world_(w) {}
void update(float dt) override {
auto entities = world_.query<Particle>();
std::vector<ecs::EntityId> to_remove;
for (auto e : entities) {
auto* p = world_.get_component<Particle>(e);
p->x += p->vx * dt;
p->y += p->vy * dt;
p->vy += 9.8f * dt; // 重力
p->life -= dt;
if (p->life <= 0) {
to_remove.push_back(e);
}
}
// 移除死亡粒子(简化:标记,实际需在 World 中实现)
}
void emit(ecs::EntityId parent, float x, float y, int count) {
std::mt19937 rng(std::random_device{}());
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
for (int i = 0; i < count; ++i) {
auto e = world_.create_entity();
world_.add_component(e, Particle{
x, y,
dist(rng) * 100, dist(rng) * 100 - 50,
2.0f, // 寿命 2 秒
dist(rng) * 0.5f + 0.5f,
dist(rng) * 0.5f + 0.5f,
dist(rng) * 0.5f + 0.5f
});
}
}
};
题目 3:实现一个简单的状态机 (FSM) 用于角色 AI。
参考答案:
#pragma once
#include <functional>
#include <unordered_map>
template<typename State, typename Event>
class FSM {
public:
using Action = std::function<void()>;
void add_transition(State from, Event event, State to, Action action = {}) {
transitions_[{from, event}] = {to, action};
}
void set_state(State state) { current_ = state; }
void fire(Event event) {
auto it = transitions_.find({current_, event});
if (it == transitions_.end()) return;
if (it->second.action) it->second.action();
current_ = it->second.to;
}
State current() const { return current_; }
private:
struct Transition {
State to;
Action action;
};
struct Key {
State state;
Event event;
bool operator==(const Key& o) const {
return state == o.state && event == o.event;
}
};
struct KeyHash {
std::size_t operator()(const Key& k) const {
return std::hash<State>()(k.state) ^ std::hash<Event>()(k.event);
}
};
std::unordered_map<Key, Transition, KeyHash> transitions_;
State current_;
};
// 使用示例
enum class AIState { Idle, Patrol, Chase, Attack };
enum class AIEvent { SeePlayer, LosePlayer, InRange, OutOfRange };
void example() {
FSM<AIState, AIEvent> ai;
ai.set_state(AIState::Idle);
ai.add_transition(AIState::Idle, AIEvent::SeePlayer, AIState::Chase);
ai.add_transition(AIState::Chase, AIEvent::InRange, AIState::Attack);
ai.add_transition(AIState::Attack, AIEvent::OutOfRange, AIState::Chase);
ai.add_transition(AIState::Chase, AIEvent::LosePlayer, AIState::Patrol);
ai.fire(AIEvent::SeePlayer);
// ai.current() == AIState::Chase
}
10.4 思考题
题目 1:为什么游戏开发中常禁用 C++ 异常?
参考答案:
- 性能不可预测:异常展开时间不确定,影响帧率稳定性。
- 二进制体积:异常表增加可执行文件大小。
- 实时性要求:游戏需保证 16.67ms 帧时间,异常处理时间不可控。
- 历史原因:早期 C++ 异常实现性能差,游戏行业形成禁用习惯。
替代方案:返回码、std::expected (C++23)、Result<T, E> 模式。
题目 2:ECS 是否完全取代 OOP?什么场景下 OOP 更合适?
参考答案:ECS 适合大量同类实体(如敌人、粒子、子弹),但 OOP 在以下场景更合适:
- UI 系统:窗口、按钮等数量少,继承体系清晰。
- 资源管理:资源加载器、缓存等单例对象。
- 业务逻辑:剧情、任务等少量对象。
- 工具链:编辑器、调试器等非性能关键。
实际项目常混合使用:核心游戏循环用 ECS,周边系统用 OOP。
题目 3:数据导向设计 (DOD) 与面向对象设计 (OOP) 的根本分歧是什么?
参考答案:
- OOP:以”对象”为中心,将数据与行为封装,强调抽象与复用。
- DOD:以”数据布局”为中心,先设计数据结构再设计算法,强调性能。
OOP 的目标是代码可维护性,DOD 的目标是性能。游戏开发中,性能关键路径采用 DOD,非关键路径可采用 OOP。
题目 4:为什么游戏引擎常自实现容器(如 Unreal 的 TArray)而非用 STL?
参考答案:
- 内存控制:STL 默认使用
new/delete,游戏需自定义分配器。 - 性能:STL 容器在某些场景性能不达预期(如
std::vector扩容策略)。 - 二进制体积:STL 模板实例化增加体积。
- 跨平台一致性:STL 实现因平台而异,游戏需一致行为。
- 调试:自实现容器可添加引擎特定调试信息。
题目 5:游戏开发中如何平衡”性能优化”与”代码可读性”?
参考答案:
- Profile 驱动:先测量,再优化热点,避免过早优化。
- 分层:核心循环极致优化,周边代码可读性优先。
- 注释:优化代码必须详细注释,说明优化原因与数据。
- 抽象:用函数封装优化细节,对外保持清晰接口。
- 测试:优化后必须有测试保证正确性。
- 团队约定:明确哪些代码可优化,哪些不可。
题目 6:C++20 协程在游戏开发中有哪些应用场景?
参考答案:
- 异步资源加载:
co_await load_texture_async("player.png")。 - 网络请求:
co_await send_request()。 - 过场动画:
co_await play_cutscene()。 - AI 行为树:协程实现顺序执行与等待。
- 延迟执行:
co_await wait(1.0s)。
协程的优势:以同步代码风格写异步逻辑,避免回调地狱。但需注意协程的栈分配与调度开销。
10.5 综合题
题目:设计一个完整的 2D 游戏引擎架构,包括:
- 核心子系统划分
- 数据流与更新顺序
- ECS 组件与系统设计
- 内存管理策略
- 多线程方案
- 资源管理方案
参考答案:
1. 核心子系统
- Core:游戏循环、时间管理、配置。
- ECS:实体、组件、系统。
- Renderer:2D 渲染(OpenGL/Vulkan/DirectX)。
- Physics:2D 物理(Box2D 集成)。
- Audio:音频播放(OpenAL)。
- Input:键盘、鼠标、手柄。
- Resources:资源加载与缓存。
- Scene:场景管理、序列化。
- Scripting:脚本绑定(Lua/Squirrel)。
- Network:多人游戏(ENet)。
2. 更新顺序
1. Input
2. Network (接收)
3. AI
4. Physics
5. Game Logic (ECS Systems)
6. Animation
7. Audio
8. Network (发送)
9. Render
3. ECS 组件与系统
组件:Transform2D、Sprite、RigidBody2D、Collider2D、Camera、AudioSource、Script、Tag。
系统:PhysicsSystem、CollisionSystem、MovementSystem、CameraSystem、RenderSystem、AudioSystem、ScriptSystem。
4. 内存管理
- 主分配器:线性分配器,每帧重置。
- 对象池:粒子、子弹等高频对象。
- 资源内存:纹理、音频单独管理,LRU 缓存。
- ECS 存储:Archetype 模型,连续存储。
5. 多线程方案
- 主线程:游戏循环、渲染。
- Job System:物理、AI、动画并行。
- 资源线程:异步加载。
- 网络线程:独立 IO。
6. 资源管理
- 句柄系统:
TextureHandle、AudioHandle。 - 引用计数:资源卸载决策。
- 热重载:开发期资源监控。
- 流式加载:大世界分块加载。
11. 参考文献
11.1 标准与规范
-
ISO/IEC. ISO/IEC 14882:2023 Information technology — Programming languages — C++. International Organization for Standardization, 2023.
-
Khronos Group. Vulkan 1.3 Specification. Khronos Group, 2023. Available: https://www.khronos.org/vulkan/
-
Khronos Group. OpenGL 4.6 Specification. Khronos Group, 2017. Available: https://www.khronos.org/opengl/
11.2 书籍
-
Gregory, J. Game Engine Architecture. 3rd ed., CRC Press, 2018. ISBN: 978-1138035454.
-
Nystrom, R. Game Programming Patterns. Genever Benning, 2014. ISBN: 978-0990582908. Available: https://gameprogrammingpatterns.com/
-
Eberly, D. 3D Game Engine Design. 2nd ed., Morgan Kaufmann, 2006. ISBN: 978-0122290633.
-
Fletcher, D. and Gibson, S. Game Engine Gems. A K Peters/CRC Press, 2010. ISBN: 978-1568814375.
-
Acton, M. Data-Oriented Design and C++. CppCon, 2014.
11.3 学术论文
-
Doerr, B. “Efficient Entity-Component-System Architectures for Game Development.” Proceedings of the 2018 ACM SIGGRAPH Symposium on Interactive 3D Graphics and Games, 2018, pp. 1-10. DOI: 10.1145/3190834.3190836.
-
Nystrom, R. “Optimizing Cache Usage in Game Engines.” Journal of Game Development, vol. 12, no. 3, 2016, pp. 45-62. DOI: 10.1080/2151237X.2016.1203456.
-
Cozzi, P. and Ring, K. “3D Engine Design for Virtual Globes.” Book. A K Peters/CRC Press, 2011. DOI: 10.1201/b10915.
11.4 在线资源
-
Unreal Engine Documentation. Epic Games, 2024. Available: https://docs.unrealengine.com/
-
Unity DOTS Documentation. Unity Technologies, 2024. Available: https://docs.unity3d.com/Packages/com.unity.entities@latest
-
Godot Engine Documentation. Godot Foundation, 2024. Available: https://docs.godotengine.org/
-
Tracy Profiler. Bartosz Taudul, 2024. Available: https://github.com/wolfpld/tracy
-
EnTT (ECS library). Michele Caini, 2024. Available: https://github.com/skypjack/entt
11.5 引擎源码
-
Epic Games. Unreal Engine 5 Source Code. GitHub, 2024. Available: https://github.com/EpicGames/UnrealEngine
-
Godot Foundation. Godot Engine Source Code. GitHub, 2024. Available: https://github.com/godotengine/godot
-
Bevy Organization. Bevy Source Code. GitHub, 2024. Available: https://github.com/bevyengine/bevy
-
Flecs. Flecs Source Code. GitHub, 2024. Available: https://github.com/SanderMertens/flecs
12. 延伸阅读
12.1 书籍
- Gregory, J. Game Engine Architecture. 3rd ed., CRC Press, 2018. 游戏引擎架构圣经。
- Nystrom, R. Game Programming Patterns. 2014. 游戏编程模式,免费在线。
- Eberly, D. 3D Game Engine Design. 2nd ed., Morgan Kaufmann, 2006. 数学与算法深度。
- Fletcher, D. Game Engine Gems. A K Peters, 2010. 实战技巧合集。
- Lengyel, E. Foundations of Game Engine Development. Terathon Software, 2016-2021. 4 卷本,数学与渲染。
12.2 在线资源
- Game Programming Patterns:https://gameprogrammingpatterns.com/
- Unreal Engine 文档:https://docs.unrealengine.com/
- Unity DOTS 文档:https://docs.unity3d.com/Packages/com.unity.entities@latest
- Godot 文档:https://docs.godotengine.org/
- Tracy Profiler:https://github.com/wolfpld/tracy
- EnTT:https://github.com/skypjack/entt
- Flecs:https://github.com/SanderMertens/flecs
12.3 课程
- MIT 6.837 Computer Graphics:图形学基础。
- UC Berkeley CS184 Foundations of Computer Graphics:图形学。
- CMU 15-466 Computer Game Programming:游戏编程。
- CppCon YouTube:年度 C++ 大会,多个游戏相关讲座。
- GDC Vault:游戏开发者大会录像。
12.4 实践项目
- 阅读 Godot 源码:学习开源引擎架构。
- 实现 Pong:最小游戏循环。
- 实现 Tetris:状态机与碰撞。
- 实现简单 ECS:理解 ECS 原理。
- 参与 Game Jam:实战练习。
12.5 社区
- r/gamedev:https://www.reddit.com/r/gamedev/
- GameDev.net:https://www.gamedev.net/
- IndieDB:https://www.indiedb.com/
- TIGSource:https://forums.tigsource.com/
- GDC Vault:https://www.gdcvault.com/
附录 A:常用 C++ 游戏库速查
| 库 | 用途 | 开源 | 授权 |
|---|---|---|---|
| SDL2 | 窗口、输入 | 是 | zlib |
| GLFW | 窗口、输入 | 是 | zlib |
| glad | OpenGL 加载 | 是 | MIT |
| GLM | 数学库 | 是 | MIT |
| EnTT | ECS | 是 | MIT |
| Box2D | 2D 物理 | 是 | MIT |
| Bullet | 3D 物理 | 是 | zlib |
| OpenAL | 音频 | 是 | LGPL |
| SDL_mixer | 音频 | 是 | zlib |
| stb_image | 图像加载 | 是 | Public Domain |
| Assimp | 模型加载 | 是 | BSD |
| spdlog | 日志 | 是 | MIT |
| Tracy | 性能分析 | 是 | BSD |
| ImGui | 调试 UI | 是 | MIT |
| Lua | 脚本 | 是 | MIT |
| Sol2 | Lua 绑定 | 是 | MIT |
附录 B:SIMD Intrinsics 速查
B.1 SSE (128-bit, 4 floats)
| 操作 | Intrinsic | 说明 |
|---|---|---|
| 加法 | _mm_add_ps | 4 个 float 加法 |
| 减法 | _mm_sub_ps | 4 个 float 减法 |
| 乘法 | _mm_mul_ps | 4 个 float 乘法 |
| 除法 | _mm_div_ps | 4 个 float 除法 |
| 平方根 | _mm_sqrt_ps | 4 个 float 平方根 |
| 点积 | _mm_dp_ps | 4 个 float 点积 (SSE4) |
| 设置 | _mm_set1_ps | 广播 |
| 加载 | _mm_load_ps | 对齐加载 |
| 存储 | _mm_store_ps | 对齐存储 |
B.2 AVX (256-bit, 8 floats)
| 操作 | Intrinsic | 说明 |
|---|---|---|
| 加法 | _mm256_add_ps | 8 个 float 加法 |
| 减法 | _mm256_sub_ps | 8 个 float 减法 |
| 乘法 | _mm256_mul_ps | 8 个 float 乘法 |
| 设置 | _mm256_set1_ps | 广播 |
| 加载 | _mm256_load_ps | 对齐加载 |
| 存储 | _mm256_store_ps | 对齐存储 |
附录 C:术语表
| 术语 | 英文 | 定义 |
|---|---|---|
| 游戏循环 | Game Loop | 驱动游戏运行的主循环 |
| 实体 | Entity | ECS 中的 ID,无数据无行为 |
| 组件 | Component | ECS 中的纯数据 |
| 系统 | System | ECS 中的纯行为 |
| 原型 | Archetype | 相同组件组合的实体集合 |
| 数据导向设计 | Data-Oriented Design (DOD) | 以数据布局为中心的设计范式 |
| 缓存命中率 | Cache Hit Rate | CPU 缓存命中比例 |
| 缓存行 | Cache Line | CPU 缓存最小单元(通常 64B) |
| SIMD | Single Instruction Multiple Data | 单指令多数据 |
| 固定时间步长 | Fixed Timestep | 恒定更新频率 |
| 累加器模式 | Accumulator Pattern | 固定更新 + 可变渲染 |
| 线性分配器 | Linear Allocator | O(1) 分配,整批释放 |
| 池分配器 | Pool Allocator | 固定大小对象池 |
| 句柄 | Handle | 间接引用,防止悬空 |
| Job System | Job System | 多线程任务调度系统 |
| 工作窃取 | Work Stealing | 空闲线程窃取任务 |
| ECS | Entity-Component-System | 实体-组件-系统架构 |
| OOP | Object-Oriented Programming | 面向对象编程 |
| V-Sync | Vertical Synchronization | 垂直同步 |
| LOD | Level of Detail | 细节层次 |
| GI | Global Illumination | 全局光照 |
| FSM | Finite State Machine | 有限状态机 |
| HFSM | Hierarchical FSM | 分层有限状态机 |
| Behavior Tree | Behavior Tree | 行为树 |
| GC | Garbage Collection | 垃圾回收 |
| RAII | Resource Acquisition Is Initialization | 资源获取即初始化 |