首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> C 語言入門

Linux 環境下以 C 語言實作貪吃蛇遊戲:Framebuffer 與終端控制完整教學

     編輯:C 語言入門

在 Linux 系統上開發圖形化遊戲,不一定非得仰賴 X Window 或 Wayland 等視窗系統。本文將示範如何僅使用 Framebuffer(幀緩衝) 直接操作螢幕像素,搭配 tcgetattr/tcsetattr 實現非阻塞鍵盤輸入,從零打造一款經典的貪吃蛇遊戲。

核心架構設計

動工前先建立清晰的遊戲框架:

  • 遊戲區域:20×20 的二維邏輯座標系,蛇與食物皆以節點為單位
  • 資料結構:食物用單一結構體,蛇身採用鏈表串接多節點
  • 渲染管線:將邏輯座標映射至 Framebuffer 像素座標,逐幀繪製
  • 輸入處理:終端設為非標準模式,即時讀取方向鍵
  • 主迴路:輸入 → 移動 → 碰撞檢測 → 重繪 → 幀率控制

Framebuffer 初始化與像素操作

透過 /dev/fb0 取得螢幕資訊,並以 mmap 將顯存映射至使用者空間:

#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>

#define RGB888(r,g,b) (((0xff & r) << 16) | ((0xff & g) << 8) | (b & 0xff))

struct fb_var_screeninfo vinfo;
int fd = open("/dev/fb0", O_RDWR);
ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
// 螢幕解析度:vinfo.xres × vinfo.yres
unsigned long *fb_ptr = mmap(NULL,
    (vinfo.xres * vinfo.yres * vinfo.bits_per_pixel) >> 3,
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

關鍵提醒:Framebuffer 繪圖只能在文字模式(Ctrl+Alt+F2 切換至 TTY)且 root 權限下運作;虛擬機環境常因解析度偵測異常導致無法顯示。

座標映射與基本繪圖函式

以像素 (200,200) 為遊戲區域左上角,每個邏輯格佔 10×20 像素:

// 在 (xx,yy) 邏輯座標繪製 10×20 矩形
void draw_point(unsigned long *fb, int xx, int yy, int r, int g, int b) {
    int px = xx * 10 + 200;
    int py = yy * 20 + 200;
    for (int y = py; y < py + 20; y++)
        for (int x = px; x < px + 10; x++)
            *(fb + y * 688 + x) = RGB888(r, g, b);  // 688 為實際 stride
}

// 繪製蛇身(頭綠、身紅)
void draw_snake(unsigned long *fb, Snake *head) {
    draw_point(fb, head->x, head->y, 0, 0xff, 0);
    for (Snake *p = head->next; p; p = p->next)
        draw_point(fb, p->x, p->y, 0xff, 0, 0);
}

// 繪製食物(綠色)
void draw_food(unsigned long *fb, Food *f) {
    draw_point(fb, f->x, f->y, 0, 0xff, 0);
}

遊戲邏輯核心實作

蛇的初始化與移動

// 初始蛇:頭隨機生成於內縮 3 格範圍,尾在頭下方
void cre_snake(Snake **head) {
    *head = malloc(sizeof(Snake));
    Snake *tail = malloc(sizeof(Snake));
    srand(time(NULL));
    (*head)->x = rand() % 15 + 3;
    (*head)->y = rand() % 15 + 3;
    (*head)->next = tail;
    tail->x = (*head)->x;
    tail->y = (*head)->y + 1;
    tail->next = NULL;
}

// 移動:頭按方向前進,後續節點依序承接前節點座標
void move(char dir, Snake *head) {
    Snake *p = head->next;
    int prev_x = p->x, prev_y = p->y;
    p->x = head->x; p->y = head->y;
    switch (dir) {
        case 'w': head->y--; break;
        case 's': head->y++; break;
        case 'a': head->x--; break;
        case 'd': head->x++; break;
    }
    while (p->next) {
        int tx = p->next->x, ty = p->next->y;
        p->next->x = prev_x; p->next->y = prev_y;
        prev_x = tx; prev_y = ty;
        p = p->next;
    }
}

高效食物生成演算法

避免「隨機重試」在蛇身極長時效能崩潰,改用可用格子陣列 + 單次隨機:

void cre_food(Food *f, Snake *head) {
    int free_map[20][20] = {0};
    int len = 0;
    for (Snake *p = head; p; p = p->next) {
        free_map[p->y][p->x] = 1;  // 標記蛇身佔用
        len++;
    }
    int n = rand() % (400 - len);   // 400 = 20×20
    int cnt = 0;
    for (int y = 0; y < 20 && cnt <= n; y++)
        for (int x = 0; x < 20; x++)
            if (!free_map[y][x] && cnt++ == n) {
                f->x = x; f->y = y; return;
            }
}

碰撞檢測與遊戲結束

int outside(Snake *h) { return h->x < 0 || h->x > 19 || h->y < 0 || h->y > 19; }
int hit_self(Snake *h) {
    for (Snake *p = h->next; p; p = p->next)
        if (p->x == h->x && p->y == h->y) return 1;
    return 0;
}
void gameover(Snake *h) {
    if (outside(h) || hit_self(h)) {
        system("clear");
        puts("Game Over!");
        recover_keyboard();  // 恢復終端設定
        exit(EXIT_SUCCESS);
    }
}

終端非阻塞鍵盤輸入

使用 termios 關閉回顯與標準模式,並設 O_NONBLOCK:

struct termios saved_tc; int saved_fl;

int init_keyboard(void) {
    tcgetattr(0, &saved_tc);
    struct termios tc = saved_tc;
    tc.c_lflag &= ~(ECHO | ICANON);
    tcsetattr(0, TCSANOW, &tc);
    saved_fl = fcntl(0, F_GETFL);
    fcntl(0, F_SETFL, saved_fl | O_NONBLOCK);
    return 0;
}

int get_key(void) {
    unsigned char buf[3];
    int n = read(0, buf, sizeof(buf));
    if (n <= 0) return -1;
    int key = 0;
    for (int i = 0; i < n; i++) key |= buf[i] << (i * 8);
    return key;
}

// 方向鍵鍵值(ESC [ A/B/C/D)
#define UP    0x415b1b
#define DOWN  0x425b1b
#define LEFT  0x445b1b
#define RIGHT 0x435b1b

主迴程整合

int main(void) {
    // ... Framebuffer 初始化 ...
    draw_side(fb_ptr);           // 畫邊框
    cre_snake(&head);
    cre_food(&food, head);
    draw_snake(fb_ptr, head);
    draw_food(fb_ptr, &food);

    init_keyboard();
    char dir = 'd'; int speed = 1;  // 1=0.4s, 0=0.2s

    while (1) {
        int key = get_key();
        // 處理方向鍵、空格變速、ESC/q 退出
        if (is_left(key)  && if_move('a', dir)) dir = 'a';
        if (is_right(key) && if_move('d', dir)) dir = 'd';
        if (is_up(key)    && if_move('w', dir)) dir = 'w';
        if (is_down(key)  && if_move('s', dir)) dir = 's';
        if (is_space(key)) speed = !speed;
        if (is_esc(key) || key == 'q') break;

        eat(&head, &food);
        move(dir, head);
        gameover(head);

        myclear(fb_ptr);                 // 僅清遊戲區
        draw_snake(fb_ptr, head);
        draw_food(fb_ptr, &food);

        usleep(speed ? 400000 : 200000);
    }
    recover_keyboard();
    return 0;
}

優化方向與心得

  • 移動優化:維護尾節點指標,移動時只將尾節點移至頭部,避免全鏈表遍歷
  • 吃食優化:同理記錄尾部前一節點,O(1) 新增節點
  • 碰撞合併:越界與自撞單次遍歷完成
  • 解析度自適應:利用 vinfo 動態計算繪圖參數,支援多螢幕
  • 功能擴充:暫停、計分、計時、排行榜

本專案完整原始碼包含 list.h、fun_snake.c、main.c 及 Makefile,採模組化設計,編譯後須在 TTY 下以 root 執行。雖然受限於 Framebuffer 權限與模式,但對理解 Linux 底層圖形與終端控制極具教學價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved