H_oRIZoN_头像
Linux入门DAY18(C 语言基于 Framebuffer 帧缓冲实现 20×20 图形化五子棋)封面图

Linux入门DAY18(C 语言基于 Framebuffer 帧缓冲实现 20×20 图形化五子棋)

文章目录

  • 前言
  • 项目概述
  • 整体设计框图
  • 核心模块代码实现
    • 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)、底层绘图。

项目功能描述

  1. ✅ 20×20 五子棋棋盘,网格线条绘制
  2. ✅ WASD 按键控制红色光标移动,边界限制,不能移出棋盘
  3. ✅ 空格键落子,禁止重复在已有棋子位置落子
  4. ✅ 黑白棋子交替落子,1代表白棋,-1代表黑棋
  5. ✅ 四向双向扫描算法判断五子连珠,横、竖、两条斜线均可判胜
  6. ✅ BMP 图片解析绘制接口,可以绘制背景、棋子、胜利弹窗
  7. ✅ 根据获胜方自动弹出对应胜利 BMP 图片(黑胜 / 白胜两张弹窗)
  8. ✅ 游戏结束等待按键 / 延时,展示胜利画面

使用技术

  • 编程语言: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

运行操作说明

  1. BMP 图片必须为24 位无压缩真彩色 BMP,和可执行程序放在同一目录。
  2. make编译,./game运行程序。
  3. W/A/S/D移动红色光标,空格落子。
  4. 达成五子连珠自动弹出胜利图片,按下任意按键结束游戏。

关键踩坑记录

  1. BMP 结构体内存对齐问题 不加#pragma pack(1)编译器自动内存对齐,结构体多出填充字节,读取 BMP 头全部错乱。并且该预处理指令写在.c内部,不要放在头文件,防止污染其他模块对齐。

  2. BMP 图片上下颠倒 BMP 存储原点是图片左下角,绘制的时候 j 循环从biHeight‑1向下遍历,把图片翻转。像素存储顺序 B‑G‑R,不是 RGB。

  3. 胜负判断后胜利方搞反 判胜之后才执行turn_g=-turn_g,所以判断弹窗图片时,直接使用当前turn_g,不能取反。

  4. getchar 会读取回车换行符 输入按键敲回车,缓冲区残留\n,必须过滤换行,否则会误触发游戏逻辑。

  5. malloc 不能使用局部数组存图片缓存 图片数据大,局部数组在栈,容易栈溢出,使用 malloc 在堆上分配内存,用完必须 free,避免内存泄漏。

  6. 数组越界风险 胜负算法 while 循环,必须先做下标边界判断,再读取数组元素,防止访问越界段错误。

项目可优化的方面

  1. 当前 draw_bmp 没有处理 BMP 行 4 字节对齐,部分非标准宽度 BMP 会花屏,需要增加行填充字节跳过逻辑。
  2. 缺少平局逻辑:棋盘全部填满没有五子连珠判定平局。
  3. 大量全局变量,耦合高,可以封装struct GameState结构体消除全局变量,函数传指针。
  4. 没有悔棋功能,可以使用栈保存每一步落子记录,实现悔棋。
  5. getchar 行缓冲输入,按键响应慢,可修改终端属性实现无缓冲即时按键。
  6. 可以扩展 AI 人机对战,实现电脑自动落子。
  7. 增加游戏菜单界面,支持重新开局,退出游戏。

项目总结与收获

本项目基于 Linux Framebuffer 帧缓冲,不依赖高级 GUI 库完成图形五子棋。

  1. 理解 Framebuffer 底层像素绘图原理,分清屏幕像素坐标与游戏逻辑数组下标之间转换。
  2. 掌握 BMP 图片文件格式,学会 C 语言读取二进制文件,内存对齐#pragma pack(1)的使用场景。
  3. 练习游戏算法:五子棋四向双向扫描判赢,只对最新落子点做判定,相比全盘扫描效率更高。
  4. 理解分层思想:底层绘图模块 (framebuffer.c) 和上层游戏业务逻辑 (game.c) 分离,接口和实现分离。上层游戏不需要关心图片解析、画点画线底层细节。
  5. 排查段错误、图片花屏、逻辑 bug,调试能力得到锻炼。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/H_oRIZoN_/article/details/163697724

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--