文章目录
- 前言
- 项目概述
- 整体设计框图
- 核心模块代码实现
- 1. 宏定义与全局状态
- 2. 棋盘绘制模块
- 3. 五子棋胜负判断算法
- 4. 游戏主交互逻辑
- 5.Framebuffer 封装 BMP 图片解析绘制
- 程序运行效果
- 关键踩坑记录
- 项目可优化方向
- 总结与收获
环境:Linux、Framebuffer 帧缓冲,C 语言;交互:WASD 移动光标,空格落子;支持黑白棋胜利 BMP 弹窗,双人对战。
前言
最近做C语言项目,基于 Linux framebuffer 帧缓冲设备,不用 QT、GTK 等图形库,纯 C 语言实现图形界面五子棋。 实现双人对战、光标移动、落子校验、四向双向判赢、BMP 胜利弹窗。 适合嵌入式 Linux、Linux C 课程设计学习,理解帧缓冲绘图、BMP 文件格式、游戏状态管理、游戏胜负算法。
完整工程结构
plaintext
├── game.c //游戏业务逻辑
├── framebuffer.c //底层绘图 + draw_bmp解析BMP图片
├── framebuffer.h //绘图函数声明头文件
├── win_white.bmp //白棋胜利弹窗(24位真彩色)
├── win_black.bmp //黑棋胜利弹窗(24位真彩色)
└── Makefile
项目概述
项目背景
在嵌入式 Linux 开发中,Framebuffer 帧缓冲是 LCD 显示屏最基础的绘图接口,直接操作显存完成像素绘制。很多课程要求不使用高级 GUI 库,手写图形界面小游戏。五子棋逻辑经典,非常适合练习 C 语言、二维数组状态管理、文件解析 (BMP)、底层绘图。
项目功能描述
- ✅ 20×20 五子棋棋盘,网格线条绘制
- ✅ WASD 按键控制红色光标移动,边界限制,不能移出棋盘
- ✅ 空格键落子,禁止重复在已有棋子位置落子
- ✅ 黑白棋子交替落子,
1代表白棋,-1代表黑棋 - ✅ 四向双向扫描算法判断五子连珠,横、竖、两条斜线均可判胜
- ✅ BMP 图片解析绘制接口,可以绘制背景、棋子、胜利弹窗
- ✅ 根据获胜方自动弹出对应胜利 BMP 图片(黑胜 / 白胜两张弹窗)
- ✅ 游戏结束等待按键 / 延时,展示胜利画面
使用技术
- 编程语言:C 语言
- 平台:Linux Framebuffer 帧缓冲
- 文件 IO:
open/read/close/malloc/free,解析 BMP 文件头、信息头 - 内存对齐:
#pragma pack(1)处理 BMP 二进制结构体 - 算法:四方向双向遍历五子连珠判定
- 数据结构:二维数组存储棋盘游戏状态
- 输入处理:
getchar()获取终端键盘输入 - 编译工具:GCC + Makefile
整体设计框图
键盘输入(W/A/S/D/空格)
↓
game.c begin_game()游戏主循环
↓
1.光标坐标更新|2.坐标转换数组下标|3.落子合法性校验
↓
chess_g[][]二维数组保存棋盘状态
↓
check_win() 四向双向胜负判定
↓
├─未胜利 → turn_g切换棋手 → show_chess_broad()刷新界面
└─胜利 → draw_bmp()绘制对应胜利BMP弹窗 →游戏结束
show_chess_broad():
draw_clear清屏 →绘制棋盘网格 →绘制光标 →遍历数组绘制棋子
framebuffer.c底层绘图:
draw_point / draw_h_line / draw_s_line / draw_circle / draw_bmp
核心模块代码实现
framebuffer.h 对外接口
#ifndef FRAMEBUFFER_H
#define FRAMEBUFFER_H
void draw_point(int x,int y,unsigned int color);
void draw_h_line(int x,int y,int len,unsigned int color);
void draw_s_line(int x,int y,int len,unsigned int color);
void draw_clear(unsigned int color);
void draw_circle(int x0,int y0,int r,unsigned int color);
void draw_full_rect(int x,int y,int w,int h,unsigned int color);
//新增BMP绘图接口
void draw_bmp(int x, int y, char *bmpname);
#endif
framebuffer.c 关键:draw_bmp 解析 24 位 BMP
BMP 要点:文件头 + 信息头;像素 BGR 顺序;图像原点在左下角;
#pragma pack(1)1 字节对齐,防止结构体填充错位。
#include "framebuffer.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#pragma pack(1)
typedef struct tagBITMAPFILEHEADER {
short bfType;
int bfSize;
short bfReserved1;
short bfReserved2;
int bfOffBits;
}Bmp_file_head_t;
typedef struct tagBITMAPINFOHEADER {
int biSize;
int biWidth;
int biHeight;
short biPlanes;
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;
int fd = open(bmpname, O_RDONLY);
if (-1 == fd)
{
perror("open bmp error");
return ;
}
read(fd, &headinfo, sizeof(headinfo));
read(fd, &bmpinfo, sizeof(bmpinfo));
if(bmpinfo.biBitCount !=24){
printf("只支持24位BMP图片\n");
close(fd);
return;
}
unsigned char *buff = malloc(bmpinfo.biHeight*bmpinfo.biWidth*bmpinfo.biBitCount/8);
if(buff == NULL){
perror("malloc fail");
close(fd);
return;
}
read(fd, buff, bmpinfo.biHeight*bmpinfo.biWidth*bmpinfo.biBitCount/8);
close(fd);
unsigned char *p = buff;
unsigned char r, g, b;
//BMP原点左下角,j倒序翻转图片
for (int j = bmpinfo.biHeight-1; j >= 0; j--)
{
for (int i = 0; i < bmpinfo.biWidth; i++)
{
b = *p;++p;
g = *p;++p;
r = *p;++p;
unsigned int col = (r << 16) | (g << 8) | (b << 0);
draw_point(i+x, j+y, col);
}
}
free(buff);
}
game.c 完整核心业务代码
#include "game.h"
#include "framebuffer.h"
#include <stdio.h>
#include <unistd.h>
#define RED 0x00FF0000
#define GREEN 0x0000FF00
#define BLUE 0x000000FF
#define BLACK 0x00000000
#define WHITE 0x00FFFFFF
#define YELLOW 0x00FFFF00
#define BROAD_X_START 100
#define BROAD_Y_START 100
#define BROAD_X_CNT 20
#define BROAD_Y_CNT 20
#define BROAD_W_ONE 25
#define WHITE_CHESS 1
#define BLACK_CHESS -1
//光标屏幕像素坐标
int posx_g = BROAD_X_START;
int posy_g = BROAD_Y_START;
//棋盘状态:0空,1白棋,-1黑棋
int chess_g[BROAD_Y_CNT][BROAD_X_CNT] = {0};
int turn_g = WHITE_CHESS;
//绘制棋盘界面
void show_chess_broad()
{
draw_clear(0xCDB79E);
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);
}
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);
}
//红色小方块光标
draw_full_rect(posx_g-5, posy_g-5, 10, 10, RED);
for (int i = 0; i < BROAD_Y_CNT; ++i)
{
for (int j = 0; j < BROAD_X_CNT; ++j)
{
if (WHITE_CHESS == chess_g[i][j])
{
draw_circle(j*BROAD_W_ONE+BROAD_X_START, i*BROAD_W_ONE + BROAD_Y_START,10, WHITE);
}
else if (BLACK_CHESS == chess_g[i][j])
{
draw_circle(j*BROAD_W_ONE+BROAD_X_START, i*BROAD_W_ONE + BROAD_Y_START,10, BLACK);
}
}
}
}
/**
* @brief 胜负判断,落子之后调用,以最新棋子为中心四向双向计数
* @param dh 棋盘行下标
* @param dl 棋盘列下标
* @return 1胜利,0未胜利
*/
int check_win(int dh,int dl)
{
int color = chess_g[dh][dl];
//四个方向:水平、竖直、主斜线、副斜线
int dir[4][2]={{0,1},{1,0},{1,1},{1,-1}};
for(int i = 0;i<4;++i)
{
int cnt = 1;
int h = dh+dir[i][0];
int l = dl+dir[i][1];
while(h>=0&&h<BROAD_Y_CNT&&l>=0&&l<BROAD_X_CNT&&chess_g[h][l]==color)
{
cnt++;
h+=dir[i][0];
l+=dir[i][1];
}
//反方向遍历
h = dh - dir[i][0];
l = dl - dir[i][1];
while(h>=0&&h<BROAD_Y_CNT&&l>=0&&l<BROAD_X_CNT&&chess_g[h][l]==color)
{
cnt++;
h-=dir[i][0];
l-=dir[i][1];
}
if(cnt>=5)
{
printf("GAME OVER cnt = %d\n",cnt);
return 1;
}
}
return 0;
}
void begin_game()
{
char choose = 0;
show_chess_broad();
while (1)
{
choose = getchar();
if(choose == '\n'||choose=='\r')
{
continue;
}
//WASD移动光标,边界校验
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)
{
//屏幕像素坐标转棋盘数组下标
int dh = (posy_g-BROAD_Y_START)/BROAD_W_ONE;
int dl = (posx_g-BROAD_X_START)/BROAD_W_ONE;
if(chess_g[dh][dl] != 0)
{
show_chess_broad();
continue;
}
chess_g[dh][dl] = turn_g;
int ret = check_win(dh,dl);
if(ret ==1)
{
show_chess_broad();
//根据获胜方绘制对应BMP弹窗,此时还未turn_g取反
if(turn_g == WHITE_CHESS){
draw_bmp(220,180,"win_white.bmp");
}else{
draw_bmp(220,180,"win_black.bmp");
}
printf("GAME OVER\n");
//等待任意按键退出胜利画面
while(getchar()!=EOF);
break;
}
turn_g=-turn_g;
}
show_chess_broad();
}
}
Makefile
game:game.c framebuffer.c
gcc game.c framebuffer.c -o game
clean:
rm -f game
运行操作说明
- BMP 图片必须为24 位无压缩真彩色 BMP,和可执行程序放在同一目录。
make编译,./game运行程序。W/A/S/D移动红色光标,空格落子。- 达成五子连珠自动弹出胜利图片,按下任意按键结束游戏。
关键踩坑记录
-
BMP 结构体内存对齐问题 不加
#pragma pack(1)编译器自动内存对齐,结构体多出填充字节,读取 BMP 头全部错乱。并且该预处理指令写在.c内部,不要放在头文件,防止污染其他模块对齐。 -
BMP 图片上下颠倒 BMP 存储原点是图片左下角,绘制的时候 j 循环从
biHeight‑1向下遍历,把图片翻转。像素存储顺序 B‑G‑R,不是 RGB。 -
胜负判断后胜利方搞反 判胜之后才执行
turn_g=-turn_g,所以判断弹窗图片时,直接使用当前turn_g,不能取反。 -
getchar 会读取回车换行符 输入按键敲回车,缓冲区残留
\n,必须过滤换行,否则会误触发游戏逻辑。 -
malloc 不能使用局部数组存图片缓存 图片数据大,局部数组在栈,容易栈溢出,使用 malloc 在堆上分配内存,用完必须 free,避免内存泄漏。
-
数组越界风险 胜负算法 while 循环,必须先做下标边界判断,再读取数组元素,防止访问越界段错误。
项目可优化的方面
- 当前 draw_bmp 没有处理 BMP 行 4 字节对齐,部分非标准宽度 BMP 会花屏,需要增加行填充字节跳过逻辑。
- 缺少平局逻辑:棋盘全部填满没有五子连珠判定平局。
- 大量全局变量,耦合高,可以封装
struct GameState结构体消除全局变量,函数传指针。 - 没有悔棋功能,可以使用栈保存每一步落子记录,实现悔棋。
- getchar 行缓冲输入,按键响应慢,可修改终端属性实现无缓冲即时按键。
- 可以扩展 AI 人机对战,实现电脑自动落子。
- 增加游戏菜单界面,支持重新开局,退出游戏。
项目总结与收获
本项目基于 Linux Framebuffer 帧缓冲,不依赖高级 GUI 库完成图形五子棋。
- 理解 Framebuffer 底层像素绘图原理,分清屏幕像素坐标与游戏逻辑数组下标之间转换。
- 掌握 BMP 图片文件格式,学会 C 语言读取二进制文件,内存对齐
#pragma pack(1)的使用场景。 - 练习游戏算法:五子棋四向双向扫描判赢,只对最新落子点做判定,相比全盘扫描效率更高。
- 理解分层思想:底层绘图模块 (framebuffer.c) 和上层游戏业务逻辑 (game.c) 分离,接口和实现分离。上层游戏不需要关心图片解析、画点画线底层细节。
- 排查段错误、图片花屏、逻辑 bug,调试能力得到锻炼。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/H_oRIZoN_/article/details/163697724




