ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Linux帧缓冲区的简易五子棋游戏——C语言工程源码深度解析

基于Linux帧缓冲区的简易五子棋游戏——C语言工程源码深度解析 写在前面本项目是一个运行于 Linux 帧缓冲区Framebuffer环境下的简易五子棋游戏使用纯 C 语言编写不依赖任何图形库如 SDL、GTK直接操作/dev/fb0设备进行像素级绘图。本文将从整体架构到核心算法逐层剖析适合嵌入式 Linux 图形编程初学者参考。一、项目概述1.1 项目简介本项目实现了一个简易的人机交替五子棋游戏主要特点如下直接操作 Framebuffer通过mmap将显存映射到用户空间实现像素级绘图键盘交互使用WASD移动光标空格键落子胜负判定四方向横、竖、正斜、反斜连五检测BMP 图片显示胜利时加载 BMP 图片作为提示无第三方图形库依赖仅使用 Linux 系统调用和 C 标准库1.2 开发环境项目说明操作系统Linux需支持/dev/fb0编译器GCC依赖库libm数学库用于draw_circle显示设备Framebuffer 设备800×600 或更高输入方式标准终端键盘输入getchar1.3 文件结构wuziqi/ ├── main.c # 程序入口 ├── game.h # 游戏逻辑头文件棋盘参数、全局变量声明 ├── game.c # 游戏核心逻辑棋盘绘制、输入处理、落子、BMP加载 ├── framebuffer.h # 帧缓冲区头文件绘图函数声明 ├── framebuffer.c # 帧缓冲区实现初始化、画点/线/圆/矩形、清屏 ├── panding.h # 胜负判定头文件 ├── panding.c # 胜负判定实现四方向连五检测 ├── makefile # 编译脚本 ├── baiqi.bmp # 白棋胜利提示图片 └── heiqi.bmp # 黑棋胜利提示图片二、整体架构设计2.1 三层架构本项目采用清晰的分层设计┌─────────────────────────────────────────┐ │ main.c入口层 │ │ 初始化 → 显示棋盘 → 开始游戏 │ ├─────────────────────────────────────────┤ │ game.c游戏逻辑层 │ │ 棋盘绘制 │ 输入处理 │ 落子逻辑 │ BMP显示 │ ├─────────────────────────────────────────┤ │ panding.c胜负判定层 │ │ 四方向连五检测 │ 胜利图片显示 │ ├─────────────────────────────────────────┤ │ framebuffer.c图形驱动层 │ │ 设备初始化 │ 画点 │ 画线 │ 画圆 │ 画矩形 │ │ 清屏 │ 内存映射mmap │ ├─────────────────────────────────────────┤ │ /dev/fb0Linux内核层 │ └─────────────────────────────────────────┘2.2 模块依赖关系main.c ├── framebuffer.h → framebuffer.c └── game.h → game.c ├── framebuffer.h → framebuffer.c └── panding.h → panding.c └── game.h访问棋盘参数各模块之间通过头文件中的extern声明共享全局变量数据流向清晰main.c启动后调用game.c的函数game.c调用framebuffer.c的绘图原语和panding.c的判定逻辑。三、图形驱动层——framebuffer.c / framebuffer.h3.1 头文件定义// framebuffer.h #define RGB_FMT_888 32 #define RGB_FMT_565 16 ​ extern int fbx_g; // 横向分辨率如 800 extern int fby_g; // 纵向分辨率如 600 ​ extern int init_fb(char *devname); extern void draw_point(int x, int y, unsigned int col); extern void draw_h_line(int x, int y, int len, unsigned int col); extern void draw_s_line(int x, int y, int len, unsigned int col); extern void draw_clear(unsigned int col); extern void draw_full_rect(int x, int y, int w, int h, unsigned int col); extern void draw_circle(int x0, int y0, int r, unsigned int col);头文件定义了两种像素格式宏RGB_FMT_888对应 32 位色深RGB_FMT_565对应 16 位色深并声明了所有绘图原语函数。fbx_g和fby_g保存当前屏幕分辨率供其他模块使用。3.2 设备初始化——init_fb()void *pmem NULL; // 显存映射指针 int fb; // 设备文件描述符 struct fb_var_screeninfo vinfo; // 可变屏幕信息 ​ int init_fb(char *devname) { // 1. 打开显示设备 fb open(devname, O_RDWR); if (-1 fb) { perror(open fb error); return -1; } ​ // 2. 获取显示设备参数分辨率、像素格式 int ret ioctl(fb, FBIOGET_VSCREENINFO, vinfo); if (ret 0) { perror(ioctl error); return -1; } ​ fbx_g vinfo.xres; fby_g vinfo.yres; ​ // 3. 建立显存空间与用户空间的内存映射 size_t len vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8; pmem mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0); if (pmem MAP_FAILED) { perror(mmap error); return -1; } ​ return 0; }核心解析初始化分三步走这也是 Linux Framebuffer 编程的标准流程打开设备open(/dev/fb0, O_RDWR)打开帧缓冲设备文件获得文件描述符fb。获取参数通过ioctl(fb, FBIOGET_VSCREENINFO, vinfo)读取屏幕的可变信息包括分辨率xres、yres、色深bits_per_pixel等。FBIOGET_VSCREENINFO是 Linux Framebuffer 子系统定义的 ioctl 命令fb_var_screeninfo结构体保存了所有可变参数。内存映射使用mmap将整个虚拟显存空间映射到用户空间的pmem指针。映射大小计算公式为$$\text{len} \text{xres\_virtual} \times \text{yres\_virtual} \times \frac{\text{bits\_per\_pixel}}{8}$$注意这里使用的是xres_virtual和yres_virtual虚拟分辨率而非物理分辨率因为虚拟分辨率可能大于物理分辨率映射整个虚拟空间才能覆盖所有可见区域。MAP_SHARED标志确保写入操作会同步到实际硬件。3.3 画点——draw_point()void draw_point(int x, int y, unsigned int col) { if (x vinfo.xres || y vinfo.yres) return; // 边界检查防止越界写入 ​ if (vinfo.bits_per_pixel RGB_FMT_888) // 32位色深 { unsigned int *p pmem; *(p vinfo.xres_virtual * y x) col; } else if (vinfo.bits_per_pixel RGB_FMT_565) // 16位色深 { unsigned short *p pmem; *(p vinfo.xres_virtual * y x) col; } }核心解析画点是所有图形绘制的基础。关键在于地址计算像素地址 基址(pmem) y × 虚拟宽度 × 每像素字节数 x × 每像素字节数对于32 位色深RGB888每个像素占 4 字节将pmem强转为unsigned int*偏移量为xres_virtual * y x对于16 位色深RGB565每个像素占 2 字节将pmem强转为unsigned short*偏移量相同函数首先进行边界检查防止坐标超出物理屏幕范围导致段错误。这里使用vinfo.xres物理分辨率而非虚拟分辨率做判断是正确的做法。3.4 画线——draw_h_line()/draw_s_line()// 画水平线 void draw_h_line(int x, int y, int len, unsigned int col) { for (int i x; i x len; i) draw_point(i, y, col); } ​ // 画竖直线 void draw_s_line(int x, int y, int len, unsigned int col) { for (int i y; i y len; i) draw_point(x, i, col); }核心解析水平线在固定 y 坐标上沿 x 方向逐点绘制竖直线在固定 x 坐标上沿 y 方向逐点绘制。实现简单直接逐点调用draw_point。优化提示当前实现逐像素调用函数存在大量函数调用开销。对于帧缓冲区编程可以优化为直接指针写入整行数据或使用memset/memcpy批量写入显著提升性能。3.5 清屏与填充矩形// 清屏用指定颜色填充整个屏幕 void draw_clear(unsigned int col) { for (int i 0; i fby_g; i) draw_h_line(0, i, fbx_g, col); } ​ // 填充矩形 void draw_full_rect(int x, int y, int w, int h, unsigned int col) { for (int i y; i y h; i) draw_h_line(x, i, w, col); }核心解析draw_clear逐行调用draw_h_line填充整个屏幕使用fbx_g和fby_g作为循环范围。draw_full_rect在指定矩形区域内逐行填充本质是连续绘制多条水平线。3.6 画圆——draw_circle()void draw_circle(int x0, int y0, int r, unsigned int col) { int x 0, y 0; int r0 0; ​ for (r0 0; r0 r; r0) // 从圆心向外逐层填充 { for (float si 0; si 360; si 0.01) // 角度从0扫到360度 { x x0 r0 * cos(2 * 3.1416 / 360 * si); y y0 r0 * sin(2 * 3.1416 / 360 * si); draw_point(x, y, col); } } }核心解析这是一个实心圆绘制算法采用极坐标参数方程$$x x_0 r \cdot \cos(\theta), \quad y y_0 r \cdot \sin(\theta)$$外层循环r0从 0 递增到r实现从圆心向外的逐层填充内层循环角度si从 0° 到 360°步长 0.01°扫描完整圆周。性能分析这个实现虽然正确但效率极低。一个半径为 10 的圆需要计算10 × 36000 360000次draw_point调用每次调用都涉及浮点运算和函数调用。更高效的方案是使用Bresenham 画圆算法仅用整数运算或Midpoint Circle Algorithm仅需O(r)次迭代即可完成圆轮廓绘制再配合扫描线填充实现实心圆。四、游戏逻辑层——game.c / game.h4.1 头文件与关键宏定义// game.h #define BROAD_X_START 100 // 棋盘左上角 X 坐标 #define BROAD_Y_START 100 // 棋盘左上角 Y 坐标 #define BROAD_X_CNT 20 // 棋盘横向交叉点数 #define BROAD_Y_CNT 20 // 棋盘纵向交叉点数 #define BROAD_W_ONE 25 // 每个格子边长像素 ​ extern int posx_g; // 当前光标 X 坐标 extern int posy_g; // 当前光标 Y 坐标 extern int chess_g[BROAD_Y_CNT 4*2][BROAD_X_CNT 4*2]; // 棋盘状态数组在game.c中还定义了棋子颜色值和颜色常量#define WHITE_CHESS 1 // 白棋标记 #define BLACK_CHESS -1 // 黑棋标记 ​ #define RED 0x00FF0000 #define GREEN 0x0000FF00 #define BLUE 0x000000FF #define BLACK 0x00000000 #define WHITE 0x00FFFFFF #define YELLOW 0x00FFFF00颜色格式说明颜色值采用0x00RRGGBB格式32 位 RGB888高 8 位为 0Alpha 通道不使用中间 8 位为红色接着 8 位为绿色低 8 位为蓝色。4.2 全局变量int posx_g BROAD_X_START; // 光标初始位置 int posy_g BROAD_Y_START; int col_ 0; // 回合计数器偶数白方奇数黑方 ​ int chess_g[BROAD_Y_CNT 4*2][BROAD_X_CNT 4*2] {0}; // 28×28 棋盘数组棋盘数组设计——关键的边界填充技巧int chess_g[BROAD_Y_CNT 4*2][BROAD_X_CNT 4*2]; // 20 8 28 20 8 28这里数组大小为28×28而实际棋盘为 20×20。两侧各多出4 行/列的填充区域共 8 行/列全部初始化为 0。这是一个非常巧妙的设计实际棋盘区域20×20下标 4~23 ┌──────────────────────────────────┐ │ 填充区4列│ 实际棋盘20列│ 填充区4列│ │ 全部为0 │ │ 全部为0 │ ├──────────────┼──────────────────┼──────────────┤ │ 填充区4行│ │ 填充区 │ ← 填充区全为0 │ 全部为0 │ 实际棋盘20行│ 全部为0 │ ├──────────────┼──────────────────┼──────────────┤ │ 填充区 │ │ 填充区 │ └──────────────┴──────────────────┴──────────────┘为什么需要这个填充在胜负判定时需要从当前位置向四个方向各检查 4 个棋子。如果棋子位于棋盘边缘直接访问a[m-4][n]等会越界。而有了填充区域值为 0既不是白棋也不是黑棋即使棋子在边缘位置访问也不会越界且 0 值不会影响连五判断因为5 × 1 5或5 × (-1) -5才判定胜利0 会打破连子。这是一种用空间换安全的经典做法。4.3 棋盘绘制——show_chess_broad()void show_chess_broad() { // 1. 绘制背景 draw_clear(0xCDB79E); // 棋盘木色背景 ​ // 2. 绘制棋盘网格线横向 for (int i 0; i BROAD_Y_CNT; i) { draw_h_line(BROAD_X_START, BROAD_Y_START i * BROAD_W_ONE, (BROAD_X_CNT - 1) * BROAD_W_ONE, BLACK); } ​ // 3. 绘制棋盘网格线纵向 for (int i 0; i BROAD_X_CNT; i) { draw_s_line(BROAD_X_START i * BROAD_W_ONE, BROAD_Y_START, (BROAD_Y_CNT - 1) * BROAD_W_ONE, BLACK); } ​ // 4. 绘制光标红色小方块 draw_full_rect(posx_g - 5, posy_g - 5, 10, 10, RED); ​ // 5. 绘制所有已落棋子 for (int i 0; i BROAD_Y_CNT 8; i) { for (int j 0; j BROAD_X_CNT 8; j) { if (WHITE_CHESS chess_g[i][j]) { draw_circle((i-4)*BROAD_W_ONE BROAD_Y_START, (j-4)*BROAD_W_ONE BROAD_X_START, 10, WHITE); } else if (BLACK_CHESS chess_g[i][j]) { draw_circle((i-4)*BROAD_W_ONE BROAD_Y_START, (j-4)*BROAD_W_ONE BROAD_X_START, 10, BLACK); } } } }核心解析棋盘绘制分为五步步骤操作说明1draw_clear(0xCDB79E)清屏并填充木色背景2横线循环绘制 20 条水平线间距 25 像素3竖线循环绘制 20 条竖直线间距 25 像素4draw_full_rect在光标位置绘制 10×10 红色方块5双重循环遍历数组根据数组值绘制白棋/黑棋圆坐标映射棋盘数组下标(i, j)到屏幕坐标的映射公式为screen (index - 4) * BROAD_W_ONE BROAD_START。其中-4是因为数组有 4 行/列的填充偏移需要减去才能对应到实际棋盘坐标。注意这里的坐标映射代码中X 和 Y 坐标似乎有交叉使用的情况i对应BROAD_Y_STARTj对应BROAD_X_START这在实际运行中需要结合具体的屏幕方向来理解可能在某些设备上表现为坐标转置。4.4 游戏主循环——begin_game()void begin_game() { char choose 0; ​ while (1) { choose getchar(); // 阻塞等待键盘输入 ​ if (w choose) // 上移 { if (posy_g BROAD_Y_START) posy_g - BROAD_W_ONE; } else if (a choose) // 左移 { if (posx_g BROAD_X_START) posx_g - BROAD_W_ONE; } else if (s choose) // 下移 { if (posy_g BROAD_Y_START (BROAD_Y_CNT - 1) * BROAD_W_ONE) posy_g BROAD_W_ONE; } else if (d choose) // 右移 { if (posx_g BROAD_X_START (BROAD_X_CNT - 1) * BROAD_W_ONE) posx_g BROAD_W_ONE; } else if ( choose) // 空格键落子 { if (col_ % 2 0) { chess_g[(posx_g-BROAD_X_START)/BROAD_W_ONE 4] [(posy_g-BROAD_Y_START)/BROAD_W_ONE 4] 1; // 白棋 } else { chess_g[(posx_g-BROAD_X_START)/BROAD_W_ONE 4] [(posy_g-BROAD_Y_START)/BROAD_W_ONE 4] -1; // 黑棋 } col_; ​ show_chess_broad(); // 刷新棋盘 panding(chess_g); // 胜负判定 if (game_over 1) return; } } }核心解析这是整个游戏的核心控制流采用经典的事件循环Event Loop模式输入处理通过getchar()函数阻塞等待用户输入每次读取一个字符。移动控制使用WASD键分别控制光标向上、下、左、右移动每次移动距离为一个棋盘格子的宽度25 像素。边界限制每次移动前都会检查光标位置是否超出棋盘边界确保光标始终停留在棋盘范围内。落子逻辑按下空格键时触发落子操作根据col_变量的奇偶性决定当前落子方偶数回合为白棋值为 1奇数回合为黑棋值为 -1。坐标转换将屏幕坐标转换为棋盘数组下标的计算公式如下$$ \text{index} \frac{\text{pos} - \text{BROAD\_START}}{\text{BROAD\_W\_ONE}} 4 $$刷新与判定每次落子后刷新棋盘显示并调用panding()检查胜负。设计要点使用col_变量记录回合数偶数回合白方落子奇数回合黑方落子实现了简单的交替机制。注意这里没有处理同一位置重复落子的情况——后续落子会覆盖已有棋子。4.5 BMP 图片显示——draw_bmp()#pragma pack(1) // 取消结构体字节对齐确保与BMP文件格式一致 typedef struct tagBITMAPFILEHEADER { short bfType; // 文件类型标志BM int bfSize; // 文件大小 short bfReserved1; // 保留字段 short bfReserved2; // 保留字段 int bfOffBits; // 图像数据偏移量 } Bmp_file_head_t; ​ typedef struct tagBITMAPINFOHEADER { int biSize; // 信息头大小 int biWidth; // 位图宽度 int biHeight; // 位图高度 short biPlanes; // 位平面数必须为1 short biBitCount; // 每像素位数 int biCompression; // 压缩类型 int biSizeImage; // 图像数据大小 int biXPelsPerMeter; // 水平分辨率 int biYPelsPerMeter; // 垂直分辨率 int biClrUsed; // 使用颜色数 int biClrImportant; // 重要颜色数 } Bmp_info_t; #pragma pack() ​ void draw_bmp(int x, int y, char *bmpname) { Bmp_file_head_t headinfo; Bmp_info_t bmpinfo; ​ // 1. 打开BMP文件 int fd open(bmpname, O_RDONLY); if (-1 fd) { perror(open bmp error); return; } ​ // 2. 读取文件头和信息头 read(fd, headinfo, sizeof(headinfo)); read(fd, bmpinfo, sizeof(bmpinfo)); ​ // 3. 读取像素数据 unsigned char *buff malloc(bmpinfo.biHeight * bmpinfo.biWidth * bmpinfo.biBitCount / 8); read(fd, buff, bmpinfo.biHeight * bmpinfo.biWidth * bmpinfo.biBitCount / 8); close(fd); ​ // 4. 逐像素绘制BMP数据从下到上存储需翻转Y轴 unsigned char *p buff; unsigned char r, g, b; ​ for (int j bmpinfo.biHeight - 1; j 0; j--) // 从最后一行开始 { for (int i 0; i bmpinfo.biWidth; i) { b *p; p; // BMP格式为BGR顺序 g *p; p; r *p; p; unsigned int col (r 16) | (g 8) | (b 0); // 转为RGB888 draw_point(i x, j y, col); } } ​ free(buff); }核心解析这是一个完整的 BMP 24 位真彩色图片解析器处理流程如下打开文件 → 读取文件头 → 读取信息头 → 读取像素数据 → 逐像素绘制 → 释放内存关键技术点#pragma pack(1)取消结构体的默认字节对齐。BMP 文件格式要求结构体成员紧密排列不加填充字节。如果不使用#pragma pack(1)编译器可能会在成员间插入填充字节导致读取的数据错位。BGR → RGB 转换BMP 文件中像素数据的排列顺序是 B-G-R蓝-绿-红而 Framebuffer 使用的是 R-G-B 顺序需要手动转换unsigned int col (r 16) | (g 8) | (b 0);Y 轴翻转BMP 文件的像素数据是从下到上存储的第一行对应图像底部因此外层循环从biHeight - 1递减到 0实现 Y 轴翻转。局限性当前实现仅支持 24 位biBitCount 24无压缩 BMP。对于其他色深如 8 位索引色或压缩格式如 RLE需要额外处理调色板或解压逻辑。五、胜负判定层——panding.c / panding.h5.1 头文件// panding.h extern int game_over; // 游戏结束标志 extern int panding(int a[][28]); // 胜负判定主函数 extern int panding_2(int num); // 胜负结果处理5.2 胜负判定主函数——panding()int game_over 0; ​ int panding(int a[][28]) { int sum[4] {0}; // 四个方向的连子之和 int m, n, step; ​ // 计算最后落子在数组中的坐标 m (posx_g - BROAD_X_START) / BROAD_W_ONE 4; n (posy_g - BROAD_Y_START) / BROAD_W_ONE 4; ​ for (step 0; step 5; step) { // 方向0水平方向左右各检查滑动窗口 for (int i n - 4 step; i n step; i) sum[0] a[m][i]; ​ // 方向1垂直方向 for (int i m - 4 step; i m step; i) sum[1] a[i][n]; ​ // 方向2主对角线左上→右下 for (int i m - 4 step, j n - 4 step; i m step; i, j) sum[2] a[i][j]; ​ // 方向3副对角线右上→左下 for (int i m - 4 step, j n 4 step; i m step; i, j--) sum[3] a[i][j]; ​ // 检查四个方向是否有连五 for (int i 0; i 4; i) { if (panding_2(sum[i]) 1) { game_over 1; return 1; } } ​ // 重置sum数组准备下一轮滑动 for (int j 0; j 4; j) sum[j] 0; } return 0; }核心解析——这是整个项目中最精巧的算法5.2.1 算法思路五子棋的胜负判定核心是检查最后落子的位置在四个方向横、竖、正斜、反斜上是否存在连续 5 个同色棋子。本项目采用的是滑动窗口求和法白棋标记为1黑棋标记为-1空位为0在某个方向上取连续 5 个位置求和若和为5→ 白棋五连 → 白方胜若和为-5→ 黑棋五连 → 黑方胜若和的绝对值小于 5 → 该窗口内未形成五连5.2.2 滑动窗口机制step变量从 0 到 4 循环实现窗口滑动。以水平方向为例假设最后落子位置为 n数组下标step 从 0 到 4 ​ step0: 检查 [n-4, n-3, n-2, n-1, n] ← 窗口最左 step1: 检查 [n-3, n-2, n-1, n, n1] step2: 检查 [n-2, n-1, n, n1, n2] ← 落子位于窗口中央 step3: 检查 [n-1, n, n1, n2, n3] step4: 检查 [n, n1, n2, n3, n4] ← 窗口最右这 5 个窗口覆盖了所有包含落子位置n的连续 5 格组合。只要其中任一窗口的 5 个格子全为同色棋子即可判定胜利。5.2.3 四个方向的检查方向0水平 → 固定行 m列从 n-4step 到 nstep 方向1垂直 ↓ 固定列 n行从 m-4step 到 mstep 方向2主对角 ↘行和列同时递增从 (m-4step, n-4step) 到 (mstep, nstep) 方向3副对角 ↙行递增列递减从 (m-4step, n4step) 到 (mstep, n-step)用图示表示四个方向方向2 ↘ / / ─────●───── 方向0 → /| / | / ↓ 方向3 ↙ 方向1 ↓5.2.4 边界安全回到之前提到的数组填充技巧由于数组大小为 28×28实际棋盘 20×20 居中四周各填充 4 行/列当落子在棋盘边缘时a[m-4][n]这样的访问也不会越界。填充区域的值为 0不会影响求和结果因为0既不是1也不是-1会打破连续性。5.3 胜负结果处理——panding_2()int panding_2(int num) { if (num 5) // 白棋五连 { draw_bmp(100, 100, ./baiqi.bmp); // 显示白棋胜利图片 return 1; } else if (num -5) // 黑棋五连 { draw_bmp(100, 100, ./heiqi.bmp); // 显示黑棋胜利图片 return 1; } else return 0; // 未分胜负 }核心解析根据求和结果判断胜负sum 55 个白棋1 × 5 5→ 白方胜利显示baiqi.bmpsum -55 个黑棋-1 × 5 -5→ 黑方胜利显示heiqi.bmp其他值未形成五连继续游戏设计亮点利用1和-1的对称性使得判断逻辑极为简洁——只需比较求和结果是否等于±5无需额外记录棋子颜色。六、程序入口——main.c#include stdio.h #include framebuffer.h #include game.h ​ int main(void) { init_fb(/dev/fb0); // 初始化帧缓冲设备 ​ #if 0 // 以下是绘图函数的测试代码已禁用 draw_clear(0xCDB79E); draw_point(400, 300, RED); draw_h_line(200, 200, 100, YELLOW); draw_circle(400, 300, 100, RED); #endif ​ show_chess_broad(); // 显示初始棋盘 begin_game(); // 开始游戏主循环 ​ return 0; }核心解析main函数极其简洁体现了良好的分层设计init_fb(/dev/fb0)—— 初始化图形硬件show_chess_broad()—— 绘制初始棋盘begin_game()—— 进入游戏主循环阻塞在此处直到游戏结束#if 0 ... #endif之间是开发阶段的测试代码用于验证各个绘图函数的正确性。使用条件编译而非注释便于随时启用调试。七、编译构建——makefileTARGET a.out CC gcc SRC framebuffer.c main.c game.c panding.c ​ $(TARGET): $(SRC) $(CC) $(SRC) -lm -o $(TARGET) ​ clean: rm -f $(TARGET)核心解析SRC列出所有源文件编译器一次性编译链接-lm链接数学库libm因为framebuffer.c中使用了cos()和sin()-o $(TARGET)输出可执行文件a.outclean清理目标删除编译产物编译命令展开gcc framebuffer.c main.c game.c panding.c -lm -o a.out注意事项此程序需要在有/dev/fb0设备的 Linux 系统上运行如树莓派、开发板或配置了 Framebuffer 的桌面 Linux且通常需要 root 权限。在虚拟终端非图形界面下运行效果最佳。八、核心设计总结与技术要点8.1 全局数据流图用户键盘输入 (WASD/Space) │ ▼ begin_game() ──更新──→ posx_g, posy_g (光标位置) │ ──更新──→ chess_g[][] (棋盘状态) │ ├──→ show_chess_broad() ──调用──→ draw_clear / draw_h_line / draw_s_line │ draw_full_rect / draw_circle │ │ │ ▼ │ draw_point() → pmem (mmap显存) │ └──→ panding(chess_g) ──计算──→ sum[4] (四方向连子和) │ ──判断──→ panding_2(sum) │ │ └──更新──→ game_over ├──→ draw_bmp() (胜利图片) └──→ 返回1 (游戏结束)8.2 关键技术点汇总技术点所在文件说明Framebuffer mmap 内存映射framebuffer.c将显存映射到用户空间实现零拷贝像素写入ioctl 获取屏幕参数framebuffer.cFBIOGET_VSCREENINFO获取分辨率和色深双色深兼容framebuffer.c同时支持 32 位 RGB888 和 16 位 RGB565棋盘数组边界填充game.h/c28×28 数组实际 20×20避免判定越界滑动窗口求和判定panding.c四方向 × 五窗口利用 ±1 对称性简化判断BMP 文件解析game.c#pragma pack(1) BGR→RGB Y轴翻转条件编译调试main.c#if 0控制测试代码的启停回合制控制game.ccol_变量奇偶性决定黑白方8.3 设计亮点数组填充避免边界检查通过在棋盘数组四周填充 4 行/列的 0 值使得胜负判定的四方向扫描永远不会越界省去了大量边界判断代码简洁且安全。±1 编码简化胜负判断白棋用1、黑棋用-1连续 5 子求和结果为±5判断逻辑只需一个if比较极为优雅。分层清晰图形驱动、游戏逻辑、胜负判定三层分离各层通过头文件和全局变量通信便于维护和扩展。零依赖图形编程不依赖任何图形库直接操作 Framebuffer是嵌入式 Linux 图形编程的经典入门范式。九、可改进之处9.1 重复落子检测当前实现中空格键落子时没有检查目标位置是否已有棋子会导致覆盖已有棋子。改进方案else if ( choose) { int idx_m (posx_g - BROAD_X_START) / BROAD_W_ONE 4; int idx_n (posy_g - BROAD_Y_START) / BROAD_W_ONE 4; ​ if (chess_g[idx_m][idx_n] ! 0) // 该位置已有棋子 { // 可以提示此处已有棋子或直接忽略 continue; } ​ chess_g[idx_m][idx_n] (col_ % 2 0) ? WHITE_CHESS : BLACK_CHESS; col_; // ... }9.2 画圆性能优化当前draw_circle使用浮点三角函数 逐点绘制性能极差。建议改用Bresenham 中点画圆算法void draw_circle(int x0, int y0, int r, unsigned int col) { int x 0, y r; int d 3 - 2 * r; // 初始决策参数 ​ while (x y) { // 利用八对称性绘制8个点 draw_point(x0 x, y0 y, col); draw_point(x0 - x, y0 y, col); draw_point(x0 x, y0 - y, col); draw_point(x0 - x, y0 - y, col); draw_point(x0 y, y0 x, col); draw_point(x0 - y, y0 x, col); draw_point(x0 y, y0 - x, col); draw_point(x0 - y, y0 - x, col); ​ if (d 0) d 4 * x 6; else { d 4 * (x - y) 10; y--; } x; } }该算法仅使用整数运算复杂度从O(r² × 36000)降至O(r)。9.3 输入方式改进当前使用getchar()获取终端输入需要按回车确认。在嵌入式设备上可改用非阻塞键盘读取如termios设置原始模式或读取/dev/input/event*实现即时响应#include termios.h ​ struct termios oldt, newt; tcgetattr(STDIN_FILENO, oldt); newt oldt; newt.c_lflag ~(ICANON | ECHO); // 原始模式无回显 tcsetattr(STDIN_FILENO, TCSANOW, newt); // ... 游戏循环 ... tcsetattr(STDIN_FILENO, TCSANOW, oldt); // 恢复终端设置9.4 资源释放程序结束时没有调用munmap释放映射内存和close关闭设备文件。虽然进程退出时操作系统会自动回收但在长期运行的场景下应显式释放void fini_fb(void) { if (pmem ! MAP_FAILED pmem ! NULL) munmap(pmem, vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8); if (fb 0) close(fb); }9.5 头文件重复包含与宏重复定义game.c中#include game.h出现了两次第 1 行和第 9 行虽有#ifndef保护不会出错但属于冗余代码。BROAD_X_START等宏在game.h和game.c中重复定义应统一在头文件中定义一次。十、完整调用关系图main() │ ├── init_fb(/dev/fb0) │ ├── open() │ ├── ioctl(FBIOGET_VSCREENINFO) │ └── mmap() │ ├── show_chess_broad() │ ├── draw_clear() ──→ draw_h_line() ──→ draw_point() │ ├── draw_h_line() × 20 │ ├── draw_s_line() × 20 │ ├── draw_full_rect() ──→ draw_h_line() ──→ draw_point() │ └── draw_circle() × N ──→ draw_point() │ └── begin_game() ├── getchar() [循环] ├── [落子时] show_chess_broad() [同上] └── [落子时] panding(chess_g) ├── 4方向 × 5窗口 求和 └── panding_2(sum) ├── [白胜] draw_bmp(baiqi.bmp) │ ├── open() → read(文件头) → read(信息头) │ ├── malloc() → read(像素数据) │ └── draw_point() × W×H → free() ├── [黑胜] draw_bmp(heiqi.bmp) [同上] └── [继续] return 0十一、总结本项目虽然代码量不大约 300 行 C 代码但涵盖了嵌入式 Linux 图形编程的多个核心知识点Linux Framebuffer 编程设备打开、参数获取、内存映射、像素写入基础图形算法画点、画线、画圆、填充矩形、清屏BMP 文件解析文件头解析、像素数据读取、颜色格式转换、Y 轴翻转游戏逻辑设计回合制控制、键盘输入处理、棋盘状态管理算法设计滑动窗口求和的胜负判定、数组边界填充技巧工程组织分层架构、头文件接口设计、Makefile 构建这是一个非常适合嵌入式 Linux 初学者的练手项目——从硬件底层到游戏逻辑每一层都可以深入学习。在此基础上可以进一步扩展为支持触摸屏输入、添加 AI 对手、实现悔棋功能、增加音效等逐步构建一个完整的五子棋游戏。声明本文基于项目实际源码进行分析所有代码片段均来自工程文件解析为作者原创。如有疏漏或不当之处欢迎指正交流。
返回列表