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;
}
&spm=1001.2101.3001.5002&articleId=163844231&d=1&t=3&u=67859f4aa76541c783f253c1d02372b6)
1488

被折叠的 条评论
为什么被折叠?



