在 Linux 系統上開發圖形化遊戲,不一定非得仰賴 X Window 或 Wayland 等視窗系統。本文將示範如何僅使用 Framebuffer(幀緩衝) 直接操作螢幕像素,搭配 tcgetattr/tcsetattr 實現非阻塞鍵盤輸入,從零打造一款經典的貪吃蛇遊戲。
動工前先建立清晰的遊戲框架:
透過 /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;
}
vinfo 動態計算繪圖參數,支援多螢幕本專案完整原始碼包含 list.h、fun_snake.c、main.c 及 Makefile,採模組化設計,編譯後須在 TTY 下以 root 執行。雖然受限於 Framebuffer 權限與模式,但對理解 Linux 底層圖形與終端控制極具教學價值。