从零开始的敲代码生活--C语言篇(五子棋小游戏)

Linux Framebuffer 简易五子棋项目

项目简介:基于Linux帧缓冲/dev/fb0,C语言实现五子棋。不依赖GUI库,直接操作显存进行绘图;32位RGBA8888像素格式;键盘w/a/s/d移动光标,空格落子;支持24位BMP图片弹窗显示胜利画面。 文件组成:framebuffer.h头文件、framebuffer.c功能实现、main.c程序入口

一、framebuffer.h 头文件

摘要

  1. 定义像素色深宏:RGB_FMT_888(32位)RGB_FMT_565(16位)
  2. 声明全局屏幕分辨率fbx_g/fby_g
  3. 对外声明全部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

逻辑流程

  1. open()打开字符设备/dev/fb0
  2. ioctl(FBIOGET_VSCREENINFO)获取屏幕可变信息:分辨率、色深;赋值全局fbx_g/fby_g
  3. 计算显存字节大小;mmap将物理显存映射到用户空间pmem,程序直接操作该虚拟地址即可修改屏幕像素

要点:mmapMAP_SHARED,用户态修改内存,显卡硬件自动刷新屏幕

模块2:BMP图片绘制 draw_bmp

  1. 使用#pragma pack(1)取消结构体对齐,正确读取14字节文件头、40字节信息头;
  2. 校验BMP标识0x4D42,仅支持24位无压缩BMP;
  3. 处理biHeight正负:正数图片原点在左下角,需要行逆序绘制;负数原点在左上角;
  4. BMP每行存在4字节对齐padding,读取时完整读入padding,绘图丢弃填充字节;BMP像素存储顺序是B‑G‑R,转换为本项目0x00RRGGBB格式;
  5. 单行缓冲区,不一次性加载整张图片,节省内存;绘制前做屏幕越界判断。

模块3:基础绘图函数

  1. draw_point:核心底层绘图函数,根据色深888/565,向映射显存地址写入颜色;做xy越界判断。
  2. draw_h_line / draw_l_line:双重循环调用draw_point绘制棋盘横竖网格。
  3. draw_beijing:双重循环全屏填充背景色。
  4. draw_qipan:双重循环绘制棋盘底色矩形。
  5. draw_yuan:圆方程 $(i‑x)^2+(j‑y)^2 \le R^2$,遍历圆包围盒,满足条件就画点,绘制圆形棋子。
  6. draw_mouse:绘制小方块作为落子光标,颜色随黑白棋切换。

模块4:游戏业务全局变量

int X0,Y0;           //光标屏幕像素坐标
int a[21][21];       //棋盘逻辑数组:0空,1白棋,‑1黑棋
int flag;            //落子计数,奇数白棋、偶数黑棋

定义颜色宏0x00RRGGBB,棋盘格子间距GAP,棋子半径R

模块5:胜负判定 w_b()

  1. 方向向量4组:右{0,1}、下{1,0}、右下斜{1,1}、左下斜{1,-1}
  2. 遍历21×21棋盘,遇到非空棋子,向4个方向最多延伸4格统计同色连续棋子;
  3. 一旦cnt>=5,调用draw_bmp弹出胜利图片,return 1;否则返回0。

模块6:界面刷新 show_ui()

绘制顺序:全屏背景 →棋盘底色 →网格线 →光标方块 →遍历棋盘数组绘制所有黑白棋子。

每次按键落子之后调用,完全重绘整个画面。

模块7:游戏主交互 game_start()

  1. getchar()读取键盘字符;
  2. w/s/a/d移动光标,做边界限制,不能移出棋盘;
  3. 空格落子:屏幕像素坐标换算为棋盘数组下标,向a[i][j]写入1或‑1;flag++切换棋手;
  4. 每次输入后调用show_ui()刷新界面,调用w_b()做胜负检测。

三、main.c主函数

流程

  1. 调用init_fb("/dev/fb0")初始化帧缓冲,初始化失败直接退出程序;
  2. 调用show_ui()渲染初始游戏画面;
  3. 调用game_start()进入死循环,键盘交互游戏。

运行前提:程序需要访问帧缓冲设备,需要root权限或者用户加入video用户组

四、整体架构总结

  1. 底层层:framebuffer设备操作、mmap显存映射、基础绘图原语draw_point;所有绘图全部建立在画点之上,画线、画圆、填充矩形都是多次调用draw_point。
  2. 工具层:BMP图片解析绘制函数。
  3. 游戏逻辑层:棋盘数组、胜负判断w_b()
  4. UI层:show_ui()统一刷新界面。
  5. 交互层:game_start()键盘处理。
  6. 入口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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值