Linux Framebuffer 简易五子棋项目
项目简介:基于Linux帧缓冲
/dev/fb0,C语言实现五子棋。不依赖GUI库,直接操作显存进行绘图;32位RGBA8888像素格式;键盘w/a/s/d移动光标,空格落子;支持24位BMP图片弹窗显示胜利画面。 文件组成:framebuffer.h头文件、framebuffer.c功能实现、main.c程序入口
一、framebuffer.h 头文件
摘要
- 定义像素色深宏:
RGB_FMT_888(32位)、RGB_FMT_565(16位); - 声明全局屏幕分辨率
fbx_g/fby_g; - 对外声明全部API:帧缓冲初始化、画点、画线、画棋盘、画圆棋子、BMP图片解析绘制、界面刷新
show_ui、游戏主循环game_start、胜负判定w_b。
对外接口简要说明
| 函数 | 功能 | 返回值 |
|---|---|---|
init_fb() | 打开/dev/fb0,ioctl获取屏幕参数,mmap映射显存 | 0成功,‑1失败 |
draw_point() | 绘制单个像素点,输入0x00RRGGBB颜色 | void |
draw_h_line()/draw_l_line() | 绘制棋盘横向、纵向网格线 | void |
draw_qipan() | 绘制棋盘底色矩形 | void |
draw_beijing() | 全屏填充背景色 | void |
draw_yuan() | 利用圆方程绘制圆形棋子 | void |
draw_bmp() | 解析绘制24位无压缩BMP胜利弹窗,处理BMP行padding、正负高度 | void |
show_ui() | 整体界面刷新:背景、棋盘、光标、全部棋子 | void |
game_start() | 游戏主循环,处理键盘w/s/a/d移动光标,空格落子 | void |
w_b() | 全盘扫描,4方向判断五子连珠 | 1胜利,0无胜利 |
二、framebuffer.c源文件
模块1:帧缓冲初始化 init_fb
逻辑流程
open()打开字符设备/dev/fb0;ioctl(FBIOGET_VSCREENINFO)获取屏幕可变信息:分辨率、色深;赋值全局fbx_g/fby_g;- 计算显存字节大小;
mmap将物理显存映射到用户空间pmem,程序直接操作该虚拟地址即可修改屏幕像素
要点:mmap
MAP_SHARED,用户态修改内存,显卡硬件自动刷新屏幕
模块2:BMP图片绘制 draw_bmp
- 使用
#pragma pack(1)取消结构体对齐,正确读取14字节文件头、40字节信息头; - 校验BMP标识
0x4D42,仅支持24位无压缩BMP; - 处理
biHeight正负:正数图片原点在左下角,需要行逆序绘制;负数原点在左上角; - BMP每行存在4字节对齐padding,读取时完整读入padding,绘图丢弃填充字节;BMP像素存储顺序是B‑G‑R,转换为本项目
0x00RRGGBB格式; - 单行缓冲区,不一次性加载整张图片,节省内存;绘制前做屏幕越界判断。
模块3:基础绘图函数
- draw_point:核心底层绘图函数,根据色深888/565,向映射显存地址写入颜色;做xy越界判断。
- draw_h_line / draw_l_line:双重循环调用
draw_point绘制棋盘横竖网格。 - draw_beijing:双重循环全屏填充背景色。
- draw_qipan:双重循环绘制棋盘底色矩形。
- draw_yuan:圆方程 $(i‑x)^2+(j‑y)^2 \le R^2$,遍历圆包围盒,满足条件就画点,绘制圆形棋子。
- draw_mouse:绘制小方块作为落子光标,颜色随黑白棋切换。
模块4:游戏业务全局变量
int X0,Y0; //光标屏幕像素坐标 int a[21][21]; //棋盘逻辑数组:0空,1白棋,‑1黑棋 int flag; //落子计数,奇数白棋、偶数黑棋定义颜色宏0x00RRGGBB,棋盘格子间距GAP,棋子半径R。
模块5:胜负判定 w_b()
- 方向向量4组:右
{0,1}、下{1,0}、右下斜{1,1}、左下斜{1,-1}; - 遍历21×21棋盘,遇到非空棋子,向4个方向最多延伸4格统计同色连续棋子;
- 一旦
cnt>=5,调用draw_bmp弹出胜利图片,return 1;否则返回0。
模块6:界面刷新 show_ui()
绘制顺序:全屏背景 →棋盘底色 →网格线 →光标方块 →遍历棋盘数组绘制所有黑白棋子。
每次按键落子之后调用,完全重绘整个画面。
模块7:游戏主交互 game_start()
getchar()读取键盘字符;w/s/a/d移动光标,做边界限制,不能移出棋盘;- 空格落子:屏幕像素坐标换算为棋盘数组下标,向
a[i][j]写入1或‑1;flag++切换棋手; - 每次输入后调用
show_ui()刷新界面,调用w_b()做胜负检测。
三、main.c主函数
流程
- 调用
init_fb("/dev/fb0")初始化帧缓冲,初始化失败直接退出程序; - 调用
show_ui()渲染初始游戏画面; - 调用
game_start()进入死循环,键盘交互游戏。
运行前提:程序需要访问帧缓冲设备,需要root权限或者用户加入video用户组
四、整体架构总结
- 底层层:framebuffer设备操作、mmap显存映射、基础绘图原语
draw_point;所有绘图全部建立在画点之上,画线、画圆、填充矩形都是多次调用draw_point。 - 工具层:BMP图片解析绘制函数。
- 游戏逻辑层:棋盘数组、胜负判断
w_b()。 - UI层:
show_ui()统一刷新界面。 - 交互层:
game_start()键盘处理。 - 入口main:初始化,启动游戏。
运行逻辑时序
main → init_fb 映射显存 → show_ui绘制初始界面 → game_start死循环 键盘输入(w/a/s/d/空格) → 更新光标/更新棋盘数组 → show_ui重绘全部画面 → w_b判断五子连珠五、代码展示
头文件
#ifndef __FRAMEBUFFER_H__ #define __FRAMEBUFFER_H__ /* 像素格式宏 */ #define RGB_FMT_888 32 // RGBA8888 32位色深 #define RGB_FMT_565 16 // RGB565 16位色深 /* 屏幕全局分辨率变量 */ extern int fbx_g; // 屏幕横向总像素 extern int fby_g; // 屏幕纵向总像素 /** * init_fb - 初始化帧缓冲设备/dev/fb0,映射显存 * @devname: 帧缓冲设备文件名 * return: 0成功,-1失败 */ extern int init_fb(char *devname); /** * draw_point - 在屏幕(x,y)绘制单个像素点 * @x: 横坐标 * @y: 纵坐标 * @col: 32位RGB888颜色值 0x00RRGGBB */ extern void draw_point(int x, int y, unsigned int col); /** * draw_h_line - 绘制棋盘横向网格线 */ extern void draw_h_line(int x,int y, unsigned int col); /** * draw_l_line - 绘制棋盘纵向网格线 */ extern void draw_l_line(int x,int y,unsigned int col); /** * draw_qipan - 填充棋盘底色矩形 */ extern void draw_qipan(int x,int y,unsigned int col); /** * draw_beijing - 全屏填充背景色 */ extern void draw_beijing(unsigned int col); /** * draw_yuan - 绘制圆形棋子 */ extern void draw_yuan(int x,int y,unsigned int col); /** * draw_bmp - 解析并绘制24位无压缩BMP图片(胜利弹窗) * @x: 图片左上角X * @y: 图片左上角Y * @bmpname: BMP文件路径 */ extern void draw_bmp(int x, int y, char *bmpname); /** * show_ui - 刷新整套游戏界面:背景、棋盘、光标、所有棋子 */ extern void show_ui(); /** * game_start - 游戏主交互循环,处理键盘落子、光标移动 */ extern void game_start(); /** * w_b - 胜负判定函数,全盘扫描判断是否存在五子连珠 * return: 1胜利,0无胜利 */ extern int w_b(); #endif源文件
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <linux/fb.h> #include <sys/mman.h> #include "framebuffer.h" /* 帧缓冲全局资源 */ void *pmem = NULL; // 显存映射内存首地址 int fb; // /dev/fb0 文件描述符 struct fb_var_screeninfo vinfo; // 屏幕可变参数(分辨率、色深) int fbx_g = 0; // 屏幕横向分辨率 int fby_g = 0; // 屏幕纵向分辨率 /** * init_fb - 初始化帧缓冲,mmap映射显存 * @devname: 帧缓冲设备路径 * return: 0成功,-1打开/映射失败 */ 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 get vinfo error"); close(fb); return -1; } printf("屏幕分辨率:xres=%d, yres=%d\n", vinfo.xres, vinfo.yres); printf("单像素位数:bits_per_pixel=%d\n", vinfo.bits_per_pixel); fbx_g = vinfo.xres; fby_g = vinfo.yres; // 3. 计算显存总字节,建立用户态内存映射 size_t vmem_size = vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8; pmem = mmap(NULL, vmem_size, PROT_READ|PROT_WRITE, MAP_SHARED, fb, 0); if (pmem == MAP_FAILED) { perror("mmap fb error"); close(fb); return -1; } return 0; } /** * draw_bmp - 绘制24位无压缩BMP图片(胜利弹窗专用) * 修复问题:行padding对齐、像素偏移lseek、兼容正负高度BMP * @x: 图片左上角横坐标 * @y: 图片左上角纵坐标 * @bmpname: BMP图片文件路径 */ void draw_bmp(int x, int y, char *bmpname) { #pragma pack(1) // BMP文件头 固定14字节 typedef struct tagBITMAPFILEHEADER { short bfType; // 文件标识 固定0x4D42("BM") int bfSize; // 整个bmp文件字节大小 short bfReserved1; // 保留位 short bfReserved2; // 保留位 int bfOffBits; // 像素数据起始偏移字节 } Bmp_file_head_t; // BMP信息头 固定40字节 typedef struct tagBITMAPINFOHEADER { int biSize; int biWidth; // 图片宽度像素 int biHeight; // 图片高度像素,正数原点左下,负数左上 short biPlanes; short biBitCount; // 单像素位数,仅支持24 int biCompression; // 压缩类型,仅支持无压缩0 int biSizeImage; int biXPelsPerMeter; int biYPelsPerMeter; int biClrUsed; int biClrImportant; } Bmp_info_t; #pragma pack() Bmp_file_head_t file_h; Bmp_info_t info_h; // 只读打开图片 int fd = open(bmpname, O_RDONLY); if (fd < 0) { perror("open bmp file fail"); return; } // 读取并校验文件头 read(fd, &file_h, sizeof(file_h)); if (file_h.bfType != 0x4D42) { printf("文件非标准BMP\n"); close(fd); return; } // 读取图像信息头 read(fd, &info_h, sizeof(info_h)); // 仅支持24位无压缩位图 if (info_h.biBitCount != 24 || info_h.biCompression != 0) { printf("仅支持24位无压缩BMP图片\n"); close(fd); return; } int w = info_h.biWidth; int h_raw = info_h.biHeight; int img_h; int reverse_line; // 标记是否逆序绘制行(原点在左下) // 判断图片存储方向 if (h_raw > 0) { img_h = h_raw; reverse_line = 1; } else { img_h = -h_raw; reverse_line = 0; } // 计算每行像素字节 + 4字节对齐填充padding int row_real_byte = w * 3; int row_pad_byte = (row_real_byte + 3) & (~3); int pad = row_pad_byte - row_real_byte; // 偏移到像素数据真正起始位置(修复图片左右错位核心) lseek(fd, file_h.bfOffBits, SEEK_SET); // 单行缓存,不一次性分配整张图片内存,节省堆空间 unsigned char *row_buf = malloc(row_pad_byte); if (row_buf == NULL) { printf("BMP行缓存分配失败\n"); close(fd); return; } // 逐行读取渲染 for (int line = 0; line < img_h; line++) { // 计算当前行对应屏幕Y坐标 int dst_y; if (reverse_line) dst_y = y + (img_h - 1 - line); else dst_y = y + line; // 读取一整行(包含末尾填充padding字节) read(fd, row_buf, row_pad_byte); // 遍历本行所有像素绘制 for (int px = 0; px < w; px++) { int dst_x = x + px; // 屏幕越界直接跳过,不绘制 if (dst_x < 0 || dst_x >= vinfo.xres || dst_y < 0 || dst_y >= vinfo.yres) continue; // BMP像素存储顺序 B G R,转换为程序RGB888格式 R<<16 | G<<8 | B unsigned char b = row_buf[px * 3 + 0]; unsigned char g = row_buf[px * 3 + 1]; unsigned char r = row_buf[px * 3 + 2]; unsigned int color = (r << 16) | (g << 8) | b; draw_point(dst_x, dst_y, color); } } // 释放资源 free(row_buf); close(fd); } /* 游戏全局常量定义 */ #define MAX_X 800 // 屏幕最大横向像素 #define MAX_Y 600 // 屏幕最大纵向像素 #define X_0 100 // 棋盘左上角X起点 #define Y_0 100 // 棋盘左上角Y起点 #define GAP 20 // 棋盘格子间距20像素 #define R 10 // 棋子绘制半径 /* 游戏全局变量 */ int X0 = 100; // 光标当前屏幕X坐标 int Y0 = 100; // 光标当前屏幕Y坐标 int a[21][21] = {0}; // 21*21棋盘数组:0空,1白子,-1黑子 int flag = 1; // 落子标记:奇数白棋,偶数黑棋 /* 颜色宏 0x00RRGGBB 适配32位RGBA8888帧缓冲 */ #define RED 0x00FF0000 #define GREEN 0x0000FF00 #define BLUE 0x000000FF #define BLACK 0x00000000 #define WHITE 0x00FFFFFF #define YELLOW 0x00FFFF00 #define QIPAN 0x00cc9966 // 棋盘底色 #define WANGGE 0x00FF0000 // 网格红线 #define HEIQI 0x00000000 // 黑棋颜色 #define BAIQI 0x00FFFFFF // 白棋颜色 #define BEIJING 0x00ccffcc // 全局背景浅绿色 /** * draw_point - 向显存写入单个像素点 * @x: 像素横坐标 * @y: 像素纵坐标 * @col: RGB888 32位颜色 */ void draw_point(int x, int y, unsigned int col) { // 越界直接放弃绘制 if (x >= vinfo.xres || y >= vinfo.yres || x < 0 || y < 0) return ; if (vinfo.bits_per_pixel == RGB_FMT_888) { unsigned int *p = pmem; *(p + vinfo.xres_virtual * y + x) = col; } else if (vinfo.bits_per_pixel == RGB_FMT_565) { unsigned short *p = pmem; *(p + vinfo.xres_virtual * y + x) = col; } } /** * draw_h_line - 绘制棋盘横向网格 */ void draw_h_line(int x,int y,unsigned int col) { for (int i = X_0; i <= x + X_0; i++) { // 纵向按格子间隔绘制点形成横线 for(int j = Y_0; j <= y + Y_0; j += GAP) { draw_point(i, j, col); } } } /** * draw_l_line - 绘制棋盘纵向网格 */ void draw_l_line(int x,int y,unsigned int col) { for (int i = X_0; i <= x + X_0; i += GAP) { for(int j = Y_0; j <= y + Y_0; j++) { draw_point(i, j, col); } } } /** * draw_beijing - 全屏填充背景色 */ void draw_beijing(unsigned int col) { for(int i = 0; i < MAX_X; i++) { for(int j = 0; j < MAX_Y; j++) { draw_point(i, j, col); } } } /** * draw_qipan - 填充棋盘底色矩形 */ void draw_qipan(int x,int y,unsigned int col) { for(int i = X_0; i < x + X_0; i++) { for(int j = Y_0; j < y + Y_0; j++) { draw_point(i, j, col); } } } /** * draw_yuan - 圆形棋子绘制,圆方程判定像素 * @x,y: 圆心坐标 * @col: 棋子颜色 */ void draw_yuan(int x,int y,unsigned int col) { for(int i = x - R; i <= x + R; i++) { for(int j = y - R; j <= y + R; j++) { // 圆标准方程 (x-a)^2 + (y-b)^2 <= r² if((i - x)*(i - x) + (j - y)*(j - y) <= R * R) { draw_point(i, j, col); } } } } /** * draw_mouse - 绘制落子光标方块,颜色随当前落子方切换 */ void draw_mouse(unsigned int col) { for(int i = X0 - 5; i < X0 + 5; i++) { for(int j = Y0 - 5; j < Y0 + 5; j++) { draw_point(i, j, col); } } } /** * w_b - 本人独立编写核心函数:五子连珠胜负判断 * 遍历21*21棋盘,四个方向检测连续5颗同色棋子 * 方向:右、下、右下斜、左下斜 * return: 1 存在五子连线胜利,0 无胜利 */ int w_b() { // 四组方向向量 {dx, dy} int dir[4][2] = {{0,1},{1,0},{1,1},{1,-1}}; // 遍历棋盘所有点位 for(int i = 0; i < 21; i++) { for(int j = 0; j < 21; j++) { int key = a[i][j]; if(key == 0) // 无棋子跳过 continue; // 分别检测四个直线方向 for(int d = 0; d < 4; d++) { int dx = dir[d][0]; int dy = dir[d][1]; int cnt = 1; // 当前棋子自身计数为1 // 向该方向延伸最多4格,凑5子 for(int k = 1; k <= 4; k++) { int ni = i + dx * k; int nj = j + dy * k; // 超出棋盘边界直接终止该方向检测 if(ni < 0 || ni >= 21 || nj < 0 || nj >= 21) break; // 同色棋子计数+1,异色中断 if(a[ni][nj] == key) cnt++; else break; // 累计5颗判定胜利,弹出对应BMP弹窗 if(cnt >= 5) { draw_bmp(200, 200, key == 1 ? "W_V.bmp" : "B_V.bmp"); return 1; } } } } } // 全盘无五子连线 return 0; } /** * show_ui - 界面刷新总函数 * 依次绘制:背景 -> 棋盘底色 -> 网格线 -> 光标 -> 所有已落棋子 */ void show_ui() { draw_beijing(BEIJING); draw_qipan(400, 400, QIPAN); draw_h_line(400, 400, WANGGE); draw_l_line(400, 400, WANGGE); // 光标颜色随落子方切换 unsigned int cur_color = (flag % 2 == 1) ? BAIQI : HEIQI; draw_mouse(cur_color); // 遍历棋盘数组,绘制所有黑白棋子 for(int i = 0; i < 21; i++) { for(int j = 0; j < 21; j++) { int pix_x = (i + 5) * GAP; int pix_y = (j + 5) * GAP; if(a[i][j] == 1) draw_yuan(pix_x, pix_y, BAIQI); else if(a[i][j] == -1) draw_yuan(pix_x, pix_y, HEIQI); } } } /** * game_start - 游戏主循环,键盘交互逻辑 * w/s/a/d:移动光标;空格:落子切换黑白棋 */ void game_start() { char choose = 0; while(1) { choose = getchar(); switch(choose) { case 'w': // 光标上移 if(Y0 - GAP >= Y_0) Y0 -= GAP; break; case 's': // 光标下移 if(Y0 + GAP <= Y_0 + 400) Y0 += GAP; break; case 'a': // 光标左移 if(X0 - GAP >= X_0) X0 -= GAP; break; case 'd': // 光标右移 if(X0 + GAP <= X_0 + 400) X0 += GAP; break; case ' ': // 空格键落子,切换黑白方 { // 屏幕光标坐标转棋盘数组下标 int arr_i = X0 / GAP - 5; int arr_j = Y0 / GAP - 5; if(flag % 2 != 0) a[arr_i][arr_j] = 1; else a[arr_i][arr_j] = -1; flag++; break; } default: break; } // 更新界面后执行胜负判断 show_ui(); w_b(); } }主函数
#include <stdio.h> #include "framebuffer.h" /** * 程序入口 * 流程:初始化帧缓冲 -> 绘制初始界面 -> 进入游戏交互循环 */ int main(void) { // 打开并映射屏幕帧缓冲 if(init_fb("/dev/fb0") < 0) { printf("帧缓冲初始化失败,程序退出\n"); return -1; } // 绘制初始游戏界面 show_ui(); // 启动键盘交互游戏循环 game_start(); return 0; }