c_snake_game

第 12 课:游戏循环 🔄

游戏循环是游戏的心脏,驱动整个游戏运行。


12.1 什么是游戏循环?

游戏循环是一个无限循环,不断执行以下步骤:

┌─────────────────────────────────┐
│         游戏开始                 │
└────────────┬────────────────────┘
             ↓
┌────────────────────────────────┐
│    1. 处理输入 (Input)          │
└────────────┬───────────────────┘
             ↓
┌────────────────────────────────┐
│    2. 更新逻辑 (Update)         │
└────────────┬───────────────────┘
             ↓
┌────────────────────────────────┐
│    3. 渲染画面 (Render)         │
└────────────┬───────────────────┘
             ↓
         继续循环?
        ╱         ╲
      是           否
      ↓             ↓
   继续循环      退出游戏

12.2 基本游戏循环

#include <stdbool.h>

int main(void) {
    bool running = true;
    
    // 初始化
    init_game();
    
    // 游戏循环
    while (running) {
        // 1. 处理输入
        handle_input(&running);
        
        // 2. 更新游戏状态
        update_game();
        
        // 3. 渲染画面
        render_game();
        
        // 4. 控制帧率
        sleep_ms(16);  // 约 60 FPS
    }
    
    // 清理
    cleanup_game();
    return 0;
}

12.3 贪吃蛇的游戏循环

void game_run(Game* game) {
    int last_update = 0;
    int current_time = 0;
    
    while (game->running) {
        // 1. 处理输入
        int key = input_get_key();
        if (key != ERR) {
            game_handle_input(game, key);
        }
        
        // 2. 更新游戏(根据难度控制速度)
        current_time++;
        int update_interval = game->speed;
        
        if (game->state == STATE_PLAYING && 
            current_time - last_update >= update_interval) {
            game_update(game);
            last_update = current_time;
        }
        
        // 3. 渲染
        game_render(game);
        
        // 4. 短暂休眠,避免 CPU 占用过高
        sleep_ms(10);
    }
}

12.4 输入处理

void handle_input(bool* running) {
    int key = getch();  // ncurses 获取按键
    
    if (key == 'q') {
        *running = false;
        return;
    }
    
    switch (key) {
        case KEY_UP:
        case 'w':
            snake_set_direction(DIR_UP);
            break;
        case KEY_DOWN:
        case 's':
            snake_set_direction(DIR_DOWN);
            break;
        case KEY_LEFT:
        case 'a':
            snake_set_direction(DIR_LEFT);
            break;
        case KEY_RIGHT:
        case 'd':
            snake_set_direction(DIR_RIGHT);
            break;
        case 'p':
            toggle_pause();
            break;
    }
}

12.5 更新逻辑

void update_game(Game* game) {
    // 1. 移动蛇
    snake_move(game->snake);
    
    // 2. 检查食物碰撞
    if (snake_eats_food(game->snake, game->food)) {
        score_add(&game->score, 10);
        snake_grow(game->snake);
        food_spawn(game->food);
    }
    
    // 3. 检查死亡碰撞
    if (snake_hit_wall(game->snake) || 
        snake_hit_self(game->snake)) {
        game->state = STATE_GAME_OVER;
    }
}

12.6 渲染画面

void render_game(Game* game) {
    // 清屏
    clear();
    
    // 绘制边框
    draw_border();
    
    // 绘制蛇
    snake_render(game->snake);
    
    // 绘制食物
    food_render(game->food);
    
    // 绘制分数
    draw_score(game->score);
    
    // 刷新屏幕
    refresh();
}

12.7 控制游戏速度

// 方法 1:固定延迟
void sleep_ms(int ms) {
    struct timespec ts;
    ts.tv_sec = ms / 1000;
    ts.tv_nsec = (ms % 1000) * 1000000;
    nanosleep(&ts, NULL);
}

// 方法 2:基于时间戳
#include <time.h>

double get_time(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

void game_loop(void) {
    double last_time = get_time();
    double target_fps = 60.0;
    double frame_time = 1.0 / target_fps;
    
    while (running) {
        double current = get_time();
        double delta = current - last_time;
        
        if (delta >= frame_time) {
            update_game();
            last_time = current;
        }
        
        render_game();
    }
}

12.8 帧率(FPS)

FPS (Frames Per Second) = 每秒帧数

60 FPS = 每帧 16.67ms
30 FPS = 每帧 33.33ms
10 FPS = 每帧 100ms

贪吃蛇通常 10-20 FPS 就够了
// 计算并显示 FPS
int frame_count = 0;
double fps_timer = 0;
double fps = 0;

void update_fps(double delta) {
    frame_count++;
    fps_timer += delta;
    
    if (fps_timer >= 1.0) {
        fps = frame_count / fps_timer;
        frame_count = 0;
        fps_timer = 0;
    }
}

void render_fps(void) {
    printf("FPS: %.1f\n", fps);
}

12.9 完整示例

#include <stdio.h>
#include <ncurses.h>
#include <unistd.h>
#include <stdbool.h>

typedef struct {
    int x, y;
    int score;
    bool running;
} Game;

void init(Game* g) {
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    nodelay(stdscr, TRUE);
    
    g->x = 10;
    g->y = 10;
    g->score = 0;
    g->running = true;
}

void handle_input(Game* g) {
    int key = getch();
    switch (key) {
        case 'q': g->running = false; break;
        case KEY_UP: g->y--; break;
        case KEY_DOWN: g->y++; break;
        case KEY_LEFT: g->x--; break;
        case KEY_RIGHT: g->x++; break;
    }
}

void update(Game* g) {
    // 游戏逻辑
}

void render(Game* g) {
    clear();
    mvprintw(g->y, g->x, "O");
    mvprintw(0, 0, "Score: %d | Press 'q' to quit", g->score);
    refresh();
}

void cleanup(void) {
    endwin();
}

int main(void) {
    Game game;
    init(&game);
    
    while (game.running) {
        handle_input(&game);
        update(&game);
        render(&game);
        usleep(100000);  // 100ms = 10 FPS
    }
    
    cleanup();
    return 0;
}

✅ 本课检查清单


📝 作业

  1. 实现一个简单的小球弹跳动画(使用游戏循环)

  2. 为贪吃蛇添加暂停功能(按 P 键)

  3. 添加 FPS 显示


下一课:ncurses 库


附录:状态机简介

游戏通常有多种状态(开始菜单、游戏中、游戏结束):

typedef enum {
    STATE_MENU,
    STATE_PLAYING,
    STATE_PAUSED,
    STATE_GAME_OVER
} GameState;

void game_loop(Game* game) {
    while (game->running) {
        handle_input(game);
        
        switch (game->state) {
            case STATE_MENU:
                update_menu(game);
                render_menu(game);
                break;
            case STATE_PLAYING:
                update_game(game);
                render_game(game);
                break;
            case STATE_PAUSED:
                render_pause_screen(game);
                break;
            case STATE_GAME_OVER:
                render_game_over(game);
                break;
        }
        
        usleep(100000);
    }
}

详细讲解见 第 14 课:状态机