在跟着B站up主Voidmatrix学习的过程中,第一个让我产生深刻印象的概念便是「游戏主循环」(Game Loop
)。
什么是游戏主循环
无论什么游戏(Video Game),在运行过程中,都会反复地执行3个操作:
- 处理用户输入
- 游戏数据/状态更新
- 游戏画面渲染
既然是「反复地」,就需要引入循环,游戏主循环就此诞生。
游戏主循环就是一个持续运行的循环,循环内部就是上文中提到的3个操作。
最基本的结构通常是这样的:
while (游戏正在运行) {
处理输入; // 读取玩家的操作(键盘、鼠标、手柄等)
更新游戏状态; // 根据输入、时间和游戏规则,计算物体位置、物理效果、AI行为、游戏逻辑等
画面渲染; // 将更新后的游戏状态绘制到屏幕上
}
帧(Frame)的概念
游戏主循环的每次完整迭代称为一「帧」。帧率(FPS, Frames Per Second)决定了每秒执行的循环次数,直接影响游戏流畅度。例如,FPS=60 表示每秒更新并渲染游戏状态60次。
为了解决什么问题
游戏与一般的顺序执行程序(比如计算一个数学公式然后输出结果)不同,它是一个动态、交互式、时间驱动的应用程序。为了满足这些要求,就必须解决以下几个问题,而游戏主循环的存在正是为了解决这些问题:
1. 持续的交互和响应
- 问题: 游戏需要不断地检查玩家是否有新的输入(按键、移动鼠标等),并立即做出反应。同时,游戏世界本身也在不断变化,即使玩家没有输入,游戏也需要继续「活」着。一个普通的程序执行完指令就停下了,无法满足这种持续性需求。
- 解决: 游戏主循环提供了一个不停歇的执行流。在循环的每一帧(一次完整的循环),游戏都会处理输入、更新状态,确保游戏始终处于响应玩家和自我演化的状态。
2. 随时间演变的游戏状态
- 问题: 游戏世界中的物体会移动、动画会播放、事件会发生。这些变化都是基于时间的,而不同硬件设备运行速度不同,直接依赖帧数计算会导致游戏速度不一致。例如,每秒移动10单位的物体,在30FPS下每帧移动0.33单位,在60FPS下每帧移动0.16单位,实际移动速度相同。这需要游戏能够以一定的频率(通过循环)来计算这些随时间变化的状态。
- 解决: 游戏主循环通过反复执行「更新游戏状态」这一步,结合 Delta Time(上一帧到当前帧的时间差)驱动更新。例如,物体速度可定义为
速度 * Delta Time
,确保无论帧率高低,物体实际移动距离与时间成正比。
3. 实时显示游戏状态
- 问题: 玩家需要看到游戏世界的当前状态。当游戏世界发生变化时,屏幕上的画面也需要立即更新来反映这些变化。这不像一些程序只需要在最后输出结果。
- 解决: 游戏主循环在每次「更新游戏状态」后,都会执行「渲染」这一步,将当前的游戏状态绘制到屏幕上。通过高频率地重复这个过程,玩家看到的就是一个流畅、动态变化的画面。
4. 同步输入、更新和渲染
- 问题: 游戏需要在处理玩家输入、计算游戏逻辑(更新)、以及显示结果(渲染)之间协调工作。例如,玩家按下了跳跃键,游戏需要接收到这个输入,然后根据物理规则计算角色新的位置,最后将新位置的角色绘制到屏幕上。这些步骤需要在一个连贯的流程中完成。
- 解决: 游戏主循环将这三个核心操作(输入、更新、渲染)组织在一个固定的序列中,每一帧都按顺序执行,确保了游戏逻辑的同步性和一致性。
代码展示
基于EasyX
的游戏主循环示例代码如下(结合了场景管理器概念):
#define FPS 60 // 目标帧率:60帧
while (is_gaming) {
DWORD frame_start_time = GetTickCount(); // 记录当前帧开始时间(毫秒级精度)
// 1. 处理输入(使用EasyX的消息泵)
while (peekmessage(&msg, EX_MOUSE | EX_KEY)) { // 非阻塞获取输入事件
scene_manager.on_input(msg); // 将输入事件传递给场景管理器
}
// 计算时间差(Delta Time)
static DWORD last_tick_time = GetTickCount();
DWORD current_tick_time = GetTickCount();
DWORD delta_tick = current_tick_time - last_tick_time;
// 2. 数据/状态更新(传入时间差实现时间驱动)
scene_manager.on_update(delta_tick);
last_tick_time = current_tick_time; // 更新上一帧时间戳
// 3. 画面渲染
cleardevice(); // 清空画布
scene_manager.on_draw(main_camera); // 场景管理器处理绘制逻辑
FlushBatchDraw(); // 批量绘制提交(避免闪烁)
// 帧率控制:计算本帧耗时并动态休眠
DWORD frame_end_time = GetTickCount();
DWORD frame_delta_time = frame_end_time - frame_start_time;
if (frame_delta_time < 1000 / FPS) {
Sleep(1000 / FPS - frame_delta_time); // 维持稳定帧率
}
}
结语
总的来说,游戏主循环就是让游戏世界「活」起来的核心机制。它保证了输入、更新、渲染这三大环节有条不紊地进行。