跳转到内容

12 · 游戏开发专项概念

定位:第 7 周扫盲章。目标不是会写游戏代码,而是能在面试时把每个概念讲 1 分钟、说出 1-2 句亮点。游戏循环、固定/可变步长、ECS、碰撞与空间分区是最高频四项。 学完标准:能对着面试官讲 1 分钟“游戏主循环”;能说清固定/可变步长的取舍;能讲 ECS 比继承体系好在哪;知道四叉树/八叉树/BVH 分别用在哪。


1. 游戏循环(Game Loop)——所有游戏的心脏(必考)

Section titled “1. 游戏循环(Game Loop)——所有游戏的心脏(必考)”
// 伪代码:所有游戏(引擎)的骨架
while (running) {
处理输入(ProcessInput); // 读取键盘/鼠标/手柄/网络
更新(Update, dt); // 游戏逻辑:AI、物理、动画、状态
渲染(Render); // 把场景画到屏幕
}
阶段 做什么 频率考虑
处理输入 收集用户输入事件 越快越好(低延迟)
更新 推进游戏世界状态(逻辑) 逻辑频率一般固定(如 30/60/120Hz)
渲染 提交绘制命令给 GPU 与显示器刷新率相关(60/120/144Hz)

1.2 固定步长 vs 可变步长(必考,重点)

Section titled “1.2 固定步长 vs 可变步长(必考,重点)”
// 可变步长(Variable):每帧用真实流逝时间 dt
while (running) {
float dt = 获取真实帧间隔(); // 每帧不同
更新(dt); 渲染();
}
// 固定步长(Fixed):逻辑按固定 dt 推进,渲染随意
while (running) {
while (accumulator >= FIXED_DT) { // FIXED_DT = 1/60s
更新(FIXED_DT); // 物理/逻辑以固定频率推进
accumulator -= FIXED_DT;
}
渲染(accumulator 做插值); // 渲染用插值对齐显示
}
可变步长 固定步长
实现 简单直接 稍复杂(accumulator + 插值)
物理稳定性 差:帧率波动 → 物理结果抖动 好:物理/逻辑确定性强
跨机器一致性 好(同输入 → 同结果,联机必须)
表现 帧率高时快(跑得快),低时慢 表现一致

游戏主流做法(混合)渲染可变步长(跟随显示器)+ 逻辑/物理固定步长。物理用固定步长保证稳定与确定性,渲染层把逻辑状态插值后绘制,兼顾流畅。

面试金句:固定步长保证物理稳定 + 确定性(联机公平),可变步长简单但受帧率波动影响;现代引擎用“渲染可变 + 逻辑固定 + 插值”混合方案。


概念 一句话 游戏用途
向量 有方向有长度的量 位置、速度、力的表示
点积 投影关系,`a·b = a
叉积 垂直于两向量的向量 求法线、判断左右/平面
矩阵 线性变换(缩放/旋转/平移) 模型/视图/投影变换、骨骼蒙皮
齐次坐标 4 维表示 3 维点(w=1),矩阵可含平移 变换统一成 4x4 矩阵乘法

2.2 四元数(Quaternion)——高频必考

Section titled “2.2 四元数(Quaternion)——高频必考”

它解决什么问题?

欧拉角(pitch/yaw/roll) 四元数
万向节死锁 有:两个轴对齐时失去一个自由度,旋转变“卡住/乱转”
插值 插值不平滑(角度路径怪异) slerp 平滑插值
存储 3 个浮点数 4 个浮点数(w, x, y, z)
拼接 顺序敏感易错 乘法拼接稳定
人读性 直观(角度) 不直观(但引擎内部通用)

一句话亮点:四元数用 4 个分量(w 标量 + xyz 向量)表示旋转,避免了欧拉角的万向节死锁,且能做平滑的球面插值(slerp),是现代引擎内部旋转的标准表示;欧拉角只在“给美术/玩家看数值”时才用。

  • 变换层级:局部坐标 → 世界坐标(父物体的变换叠加)
  • 插值:lerp(线性)、slerp(四元数球面)、平滑阻尼(smoothdamp)
  • 浮点陷阱:帧间浮点累计误差(用固定步长缓解)

3. ECS(Entity-Component-System)——游戏架构明星(必考)

Section titled “3. ECS(Entity-Component-System)——游戏架构明星(必考)”
// 传统 OOP:物体 = 类继承
class Entity { ... };
class Enemy : public Entity { ... }; // 想加"会飞"?加"可被光照"?
class FlyingEnemy : public Enemy { ... }; // 多继承爆炸、脆弱的深继承树
// 新增一种组合 → 新建一个类 → 类爆炸
要素 是什么 例子
Entity(实体) 只是一个 ID(整数),没有数据没有行为 entity 1024
Component(组件) 纯数据(struct),描述“有什么” Position{x,y}Health{100}Velocity{dx,dy}
System(系统) 纯逻辑函数,处理一类组件 移动系统(Position+Velocity)伤害系统(Health)
// 组件是纯数据
struct Position { float x, y; };
struct Velocity { float vx, vy; };
// 实体 = ID + 组件集合
// entity 1024 = { Position, Velocity, Health }
// 系统 = 遍历"拥有这些组件"的所有实体,处理逻辑
移动系统() {
for each entity 拥有(Position, Velocity):
entity.Position += entity.Velocity * dt; // 数据连续 → 缓存友好
}
  1. 缓存友好:同类组件连续存储(SoA,见 11 章)→ 遍历快
  2. 灵活组合:加能力 = 加组件,不用改类继承树(“飞行的敌人” = 加个 Fly 组件)
  3. 易并行:不同系统处理不同数据,天然可多线程并行(job system 直接调度)
  4. 无深继承:告别“菱形继承/脆弱的类树”,逻辑清晰

缺点/代价:组件间通信要小心、调试时“对象”信息分散、上手门槛高、不适合“对象行为差异极大”的场景。

面试金句:ECS 把“对象是啥”(数据/组件)和“对象干啥”(逻辑/系统)彻底分离,用“数据连续 + 批量处理”换缓存友好与并行度,是《守望先锋》等现代引擎(如 Unity DOTS)的核心架构。


4. 碰撞检测与空间分区(必考)

Section titled “4. 碰撞检测与空间分区(必考)”
粗检测(Broad Phase)→ 排除绝大多数不可能碰的 → 用空间分区快速筛选候选对
细检测(Narrow Phase)→ 对候选对做精确检测 → AABB/球/OBB 相交测试、多边形分离轴

4.2 空间分区结构(重点:各自适用场景)

Section titled “4.2 空间分区结构(重点:各自适用场景)”
结构 原理 适用场景 亮点一句话
网格/空间哈希 把世界切成格子,物体登记进格子 物体均匀分布(子弹、大量小物体) 实现最简单
四叉树(2D) 递归四等分空间,动态分割 2D 游戏、地形、单位查询 稀疏分布也能自适应
八叉树(3D) 四叉树的 3D 版(八等分) 3D 场景、体素、视锥剔除 3D 空间查询的标准结构
BVH(包围体层次) 用包围盒树包住物体,动态更新 动态物体(角色、刚体) 实时更新友好,物理引擎核心
BSP 树 平面切分空间 室内场景(早期 FPS)、寻路 静态场景专用

一句话选择逻辑:静态场景 → 四叉树/八叉树/BSP;动态物体 → BVH(或网格/空间哈希);均匀分布 → 网格哈希。

  • AABB(轴对齐包围盒):与坐标轴对齐的盒子,检测最便宜
  • OBB(有向包围盒):可旋转,更贴合物体,检测较贵
  • 球体/胶囊体:角色碰撞常用胶囊体(便宜 + 贴合人形)

面试金句:碰撞查询的本质是“空间索引”——用结构先粗筛、再细查,把 O(n²) 的全对全检测降为接近 O(n);物理引擎(如 Bullet、PhysX)都内置了 broad phase 的 BVH/网格。


概念 一句话
刚体动力学 用牛顿定律模拟物体的力/速度/碰撞响应
重力/摩擦/阻尼 常见力:重力加速度、地面摩擦、速度阻尼
碰撞响应 弹性/非弹性碰撞、冲量、摩擦、穿透修正
触发器 Trigger 不产生物理阻挡,只检测“进入/离开”(做事件:捡拾、区域)
射线检测 Raycast 从一点发射射线查“撞到谁”(射击、瞄准、AI 视线)
连续碰撞 CCD 高速物体防止“穿墙”(子弹、快车)
确定性 固定步长 + 同输入同结果(联机/回放依赖)
物理引擎 PhysX(UE/Unity 默认)、Bullet(开源)、Havok

引擎里物理的层次:Collider(碰撞形状)→ Rigidbody(刚体,受物理影响)→ 碰撞回调(onCollision/onTrigger)→ 约束(关节、铰链)。

亮点一句:物理是“确定性优先”的系统——所以必须固定步长;游戏里 90% 的碰撞需求用 AABB/胶囊体 + 空间分区就能满足,不需要精确凸包。


6. 高频面试题 Q&A(合上书能讲)

Section titled “6. 高频面试题 Q&A(合上书能讲)”

Q1:游戏主循环一般是什么结构? 处理输入 → 更新(固定步长逻辑 + 物理)→ 渲染。循环持续到退出。混合方案:逻辑固定步长(accumulator 累积)、渲染可变(跟随刷新率 + 插值)。

Q2:固定步长和可变步长怎么选? 物理/逻辑用固定步长(稳定、确定性、联机公平);渲染用可变步长(流畅)。纯可变实现简单但帧率波动影响表现,还会让物理抖动。

Q3:什么是 ECS?为什么比传统继承好? 实体=ID、组件=纯数据、系统=逻辑。好:缓存友好(数据连续)、灵活组合(加组件即加能力)、易并行、无深继承。代价:调试分散、上手门槛。

Q4:四元数解决什么问题?为什么不用欧拉角? 欧拉角有万向节死锁 + 插值不平滑;四元数无死锁、slerp 平滑、拼接稳定,是现代引擎旋转的标准表示。

Q5:碰撞检测一般怎么做? 两阶段:broad phase 用空间分区(四叉树/八叉树/BVH/网格哈希)粗筛候选对,narrow phase 用包围体(AABB/球/OBB)精确检测。

Q6:四叉树、八叉树、BVH 分别适合什么场景? 四叉树 2D 稀疏场景、八叉树 3D 场景、BVH 动态物体(物理引擎)、网格哈希均匀分布。

Q7:为什么要用固定步长做物理? 确定性:同输入必然同结果(联机、回放、测试依赖);稳定性:可变步长下帧率波动会让物理抖动甚至发散。

Q8:什么是空间分区?解决什么问题? 把空间划分成区域,物体登记到区域,查询只搜邻近区域 → 把碰撞检测从 O(n²) 降到接近 O(n)。

Q9:欧拉角转四元数/四元数插值为什么重要? 角色朝向、相机旋转都要做旋转插值;slerp 保证最短弧线平滑旋转,欧拉角插值会转出奇怪路径。

Q10:引擎里的 Collider 和 Rigidbody 分别干嘛? Collider 定义碰撞形状(不移动);Rigidbody 受物理(力/速度)影响并产生碰撞响应。触发器 = 只检测进入/离开不做阻挡。


7. 本章自测(10 题,限时 15 分钟,先自己答再看答案)

Section titled “7. 本章自测(10 题,限时 15 分钟,先自己答再看答案)”

1. 手写游戏主循环骨架(混合步长,含 accumulator 和插值)。

2. 固定步长解决了哪两个问题?

3. ECS 的三个要素分别是什么?各举一例。

4. 为什么说 ECS 缓存友好?

我的原答:和 SoA 什么关系?

5. 四元数的 4 个分量是什么?为什么能避免万向锁?

6. 碰撞检测的两阶段分别干什么?

7. 四叉树和 BVH 的区别(一个场景各举一个)?

8. 什么是触发器(Trigger)?

我的原答:举一个游戏场景。

9. 为什么物理必须确定性?

我的原答:不固定步长会怎样?

10. 点积和叉积在游戏里各有什么用途?

📖 答案(先自己答完再展开)
  1. while(running){ 输入; while(acc>=FIXED_DT){ 更新(FIXED_DT); acc-=FIXED_DT; } 渲染(插值); }(详见 1.2)
  2. 物理稳定性(帧率波动不抖动)和确定性(联机/回放同输入同结果)。
  3. Entity=ID(如 1024)、Component=纯数据(Position{x,y})、System=逻辑(移动系统处理 Position+Velocity)。
  4. 同类组件连续存储,批量处理时缓存行全是目标数据;ECS 的组件数组天然是 SoA 布局。
  5. w + (x,y,z),4 个分量,在 4D 超球面上表示旋转;不会出现欧拉角两个轴对齐锁死的情况。
  6. Broad phase 用空间分区粗筛候选对;narrow phase 用包围体精确求交。
  7. 四叉树:静态地形/2D 单位查询;BVH:动态角色/刚体碰撞(包围盒树实时更新)。
  8. 只检测进入/离开不阻挡,如:玩家走进拾取区域触发加血。
  9. 联机同步、回放、物理测试都要“同输入同结果”;不固定步长会导致不同机器物理表现不同、联机不同步。
  10. 点积:判断朝向/光照(cosθ);叉积:求法线、判断左右侧。

## 8. 进阶追问(答不上来就回来复习)
  • accumulator 插值为什么能让“固定逻辑 + 可变渲染”不卡顿?
  • ECS 的 archetype 模型(Unity DOTS)是什么?组件分布怎么按“实体拥有哪些组件”分块?
  • 四元数的乘法为什么等于旋转拼接?slerp 和 nlerp 区别?
  • 物理引擎的 broad phase 一般用什么结构组合(动态 BVH + 静态网格)?
  • 蒙皮动画的数学(骨骼矩阵、权重、GPU 蒙皮)和“每帧每骨骼矩阵”的性能问题?
  • 空间哈希为什么适合大量小物体(子弹)?格子大小怎么定?