理解游戏循环的工作原理,学会如何控制游戏节奏和状态更新。
游戏循环 = 游戏的心脏
所有游戏(从俄罗斯方块到 3A 大作)都有一个核心循环,不断重复以下流程:
┌─────────────────┐
│ 1. 处理输入 │ ← 玩家按键
│ (Input) │
└────────┬────────┘
↓
┌─────────────────┐
│ 2. 更新状态 │ ← 方块下落、碰撞检测
│ (Update) │
└────────┬────────┘
↓
┌─────────────────┐
│ 3. 渲染画面 │ ← 绘制到屏幕
│ (Render) │
└────────┬────────┘
↓
继续循环?
╱ ╲
是 否
↓ ↓
继续 退出
// ❌ 错误做法:只执行一次
input_handle(); // 处理一次输入
game_update(); // 更新一次状态
render(); // 绘制一次画面
// 然后程序就结束了!
结果: 画面静止,无法响应输入,游戏无法进行。
// ✅ 正确做法:持续循环
while (game->running) {
input_handle();
game_update();
render();
sleep(50ms); // 控制速度
}
好处:
// main.c
int main(void) {
// 1. 初始化(只执行一次)
render_init();
load_high_scores();
Game* game = game_create();
game_init(game);
// 2. 游戏主循环(重复执行)
while (game->running) {
// 2.1 处理输入
int key = input_get_key();
if (key != ERR) {
input_handle_game(game, key);
}
// 2.2 更新游戏状态
game_update(game);
// 2.3 渲染画面
game_render(game);
// 2.4 控制速度
napms(game->paused ? 100 : 50);
}
// 3. 清理(只执行一次)
game_destroy(game);
render_cleanup();
return 0;
}
初始化 → 循环 → 清理 是标准程序结构:
┌─────────────────────────────────────┐
│ 初始化 │
│ - 分配内存 │
│ - 设置初始状态 │
│ - 加载资源 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 主循环 (while running) │
│ - 处理输入 │
│ - 更新状态 │
│ - 渲染画面 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 清理 │
│ - 释放内存 │
│ - 保存数据 │
│ - 关闭资源 │
└─────────────────────────────────────┘
// input.c
int input_get_key(void) {
return getch(); // ncurses 函数
}
关键点:非阻塞
// 在 render_init() 中设置
nodelay(stdscr, TRUE); // getch() 不等待,立即返回
对比:
| 模式 | 行为 | 适用场景 |
|---|---|---|
| 阻塞 | getch() 等待用户按键 |
菜单、对话 |
| 非阻塞 | getch() 立即返回 |
动作游戏、实时响应 |
为什么俄罗斯方块用非阻塞?
int key = input_get_key();
if (key == ERR) {
// 没有按键,继续循环
// ERR 是 ncurses 常量,表示无输入
} else {
// 有按键,处理
input_handle_game(game, key);
}
// game.c
void game_update(Game* game) {
if (!game || game->paused || game->game_over) return;
// 方块下落一格
game->current.y++;
// 检查碰撞
if (board_check_collision(&game->board, &game->current)) {
// 碰撞了,退回一格
game->current.y--;
// 锁定方块到游戏板
board_lock_tetromino(&game->board, &game->current);
// 检查并消除行
int lines = board_clear_lines(&game->board);
// 生成新方块
game->current = game->next;
tetromino_init(&game->next, random_tetromino());
// 检查游戏结束
if (board_check_collision(&game->board, &game->current)) {
game->game_over = true;
}
}
}
步骤分解:
1. 假设方块在 y=5
┌────┐
│ ██ │ y=5
└────┘
2. 先下落:y++ → y=6
┌────┐
│ │ y=5
│ ██ │ y=6 ← 可能已经碰撞
└────┘
3. 检查碰撞
- 如果碰撞:y-- 退回 y=5,锁定方块
- 如果没碰撞:保持在 y=6
4. 继续循环...
为什么这样设计?
if 判断// render.c
void render_game(Game* game) {
clear(); // 1. 清屏
// 2. 绘制影子(最底层)
render_ghost(&game->ghost);
// 3. 绘制已固定方块(中间层)
render_board(&game->board);
// 4. 绘制当前方块(最上层)
render_tetromino(&game->current);
// 5. 绘制 UI
render_next_piece(&game->next);
render_score(game->score, ...);
refresh(); // 6. 刷新屏幕
}
为什么需要这个顺序?
图层叠加原理:
清屏 → 影子 → 固定方块 → 当前方块 → UI
↓ ↓ ↓ ↓ ↓
空白 灰色点点 彩色方块 彩色方块 文字
(底层) (中层) (上层) (最上)
如果顺序错了:
- 先画当前方块,再画影子 → 影子会覆盖当前方块 ❌
- 先画 UI,再画方块 → 方块会覆盖 UI ❌
// 如果没有延迟
while (game->running) {
update();
render();
// 没有延迟 → 循环飞快 → 方块瞬间到底!
}
结果: 游戏运行太快,无法玩。
napms(game->paused ? 100 : 50); // 暂停时 100ms,正常 50ms
napms 是什么?
n + a + p + ms = nap millisecondssleep() 更精确(毫秒级)假设 napms(50):
1 次循环 = 50ms
1 秒 = 1000ms ÷ 50ms = 20 次循环
但这不是方块下落速度!
方块下落由 game_update() 中的逻辑控制:
- 每次循环 y++(下落一格)
- 但只有碰撞时才真正下落
实际下落速度 = drop_interval(约 3000ms 初始)
// 在游戏循环中
if (game->paused) {
napms(100); // 暂停时也保持循环,但速度慢
continue; // 跳过更新和渲染
}
// 或者在 input_handle_game 中
case 'p':
case 'P':
game->paused = true;
break;
// render_game() 中
if (game->paused) {
attron(A_REVERSE | A_BOLD);
mvprintw(10, 20, "+-------------+");
mvprintw(11, 20, "| PAUSED |");
mvprintw(12, 20, "| P-Resume |");
mvprintw(13, 20, "+-------------+");
attroff(A_REVERSE | A_BOLD);
}
FPS = Frames Per Second(每秒帧数)
60 FPS = 每秒渲染 60 次 = 每帧 16.67ms
30 FPS = 每秒渲染 30 次 = 每帧 33.33ms
20 FPS = 每秒渲染 20 次 = 每帧 50ms ← 我们的游戏
| 游戏类型 | 推荐 FPS | 原因 |
|---|---|---|
| 俄罗斯方块 | 20-30 | 回合制,不需要快速反应 |
| 动作游戏 | 60 | 需要流畅动画 |
| 射击游戏 | 144+ | 竞技,需要极高流畅度 |
为什么 20 FPS 够用?
┌─────────────────────────────────────────────┐
│ main() 启动 │
└───────────────────┬─────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 初始化 │
│ - render_init() │
│ - load_high_scores() │
│ - game_create() │
│ - game_init() │
└───────────────────┬─────────────────────────┘
↓
┌──────────────────────┐
│ game->running ? │←────────┐
└──────────┬───────────┘ │
Yes │ No │
↓ │ ↓ │
┌───────────────┴─────┐│ │
│ input_get_key() ││ │
│ if (key != ERR) ││ │
│ input_handle_game()││ │
└───────────┬─────────┘│ │
↓ │ │
┌─────────────────────┐│ │
│ game_update() ││ │
│ - current.y++ ││ │
│ - check_collision() ││ │
│ - lock/lines/spawn ││ │
└───────────┬─────────┘│ │
↓ │ │
┌─────────────────────┐│ │
│ game_render() ││ │
│ - clear() ││ │
│ - ghost/board/current│ │
│ - UI/next/score ││ │
│ - refresh() ││ │
└───────────┬─────────┘│ │
↓ │ │
┌─────────────────────┐│ │
│ napms(50) ││ │
│ (控制速度) ││ │
└───────────┬─────────┘│ │
│ │ │
└──────────┴───────────────┘
↓
┌────────────────────┐
│ 清理 │
│ - game_destroy() │
│ - render_cleanup()│
│ - show_high_scores()│
└────────────────────┘
修改 napms(50) 为 napms(100),观察游戏有什么变化
game_update() 开头添加调试输出:
printf("DEBUG: 方块位置 (%d, %d)\n", game->current.x, game->current.y);
运行游戏,观察输出
game_update(),会发生什么?为什么?下一课:碰撞检测