【嵌入式】探索嵌入式世界:在ARM上构建俄罗斯方块游戏的奇妙之旅

前言:

随着科技的不断进步,嵌入式系统已经渗透到我们生活的方方面面,从家用电器到工业自动化,无处不在。在众多嵌入式应用中,游戏作为一种娱乐形式,不仅能够丰富人们的业余生活,还能有效锻炼逻辑思维和反应能力。本文将详细介绍一款基于ARM开发板GEC6818和嵌入式Linux操作系统开发的俄罗斯方块游戏。这款游戏以其经典的玩法、简洁的界面设计和流畅的运行性能,为用户带来了既富有挑战性又充满乐趣的游戏体验。文章将从设计思路、功能描述、程序流程、各模块实现等方面,全面解析这款游戏的制作过程和关键技术。

giteehttps://gitee.com/q-haodong/test_-arm/tree/master/20240619_test_tetris2
效果演示https://live.csdn.net/v/405152?spm=1001.2014.3001.5501

基于嵌入式Linux俄罗斯方块

1. 简介

随着嵌入式技术的快速发展,嵌入式系统在各个领域的应用日益广泛。本项目以ARM开发板GEC6818为平台,基于嵌入式Linux操作系统,实现了一款具有基本功能的俄罗斯方块游戏。游戏设计遵循模块化思想,将系统分解为图形显示、触摸事件处理、游戏控制、界面显示、链表管理、移动逻辑以及主控等多个模块,以提高代码的可维护性和扩展性。通过C语言编程,利用多线程技术,实现了方块的移动、变形、随机生成、触屏控制、暂停恢复、嵌套消行和计分等功能。游戏界面简洁直观,提供了分数和等级显示,确保玩家能够轻松跟踪游戏进度。在性能方面,游戏运行流畅,代码规范,附有详细注释和文档,便于理解和维护。此外,通过全面测试,确保了游戏的稳定性和可靠性。最终,本项目不仅锻炼了嵌入式系统开发能力,也提供了一个既具有挑战性又富有趣味性的游戏体验。

2. 总体设计思路及功能描述

2.1 设计思路

本俄罗斯方块游戏的设计采用模块化的编程思想,将游戏分解为多个功能模块,每个模块负责不同的任务。主要模块包括图形显示模块、触摸事件处理模块、游戏控制模块、界面显示模块、链表管理模块、移动逻辑模块以及主控模块。程序使用C语言编写,运行在ARM平台上,利用多线程技术来提高游戏的响应速度和性能。

2.2 功能描述

  1. 图形显示模块:负责加载和显示BMP图片到屏幕上,支持指定区域的图片显示,用于游戏方块和背景的绘制。
  2. 触摸事件处理模块:监听触摸屏事件,将用户的触摸操作转换为游戏内的控制指令。
  3. 游戏控制模块:包含游戏的暂停和重启功能,允许玩家在任何时候暂停游戏,并在适当的时候恢复或重新开始。
  4. 界面显示模块:管理游戏的开始界面和结束界面,提供用户交互的界面元素。
  5. 链表管理模块:使用链表数据结构管理游戏中的方块布局,实现方块的动态添加和删除。
  6. 移动逻辑模块:控制方块的移动、变形和消行等逻辑,确保游戏规则的准确执行。
  7. 主控模块:作为程序的入口,初始化游戏环境,创建和管理线程,控制游戏的主循环。

2.3 程序流程图

在这里插入图片描述
程序流程从初始化游戏环境开始,显示欢迎界面,然后进入一个循环等待用户的触摸操作。一旦检测到触摸事件,程序将处理这些输入并更新游戏状态。随后,程序检查游戏是否结束,如果是,则显示游戏结束界面,并等待用户决定是否重启游戏或退出。如果用户选择重启,程序将重新初始化游戏环境;如果选择退出,则程序将结束运行。

3. 各部分程序功能及详细说明

3.1 游戏界面函数

3.1.1 游戏界面中的图片显示

代码中使用BMP文件格式来显示图像资源,这些图像用于游戏的图形界面,如方块、背景、按钮等元素。显示BMP图像的功能主要通过bmp_show.h头文件中声明的函数来实现。以下是与BMP显示相关的代码片段和解释:

  1. BMP显示函数声明 : 在bmp_show.h中,声明了两个函数bmp_show_mix和bmp_show_self,用于显示BMP图像:
    int bmp_show_mix(int x0, int y0, int width, int height, char *name);
    int bmp_show_self(int x0, int y0, int width, int height, char *name);

  2. BMP文件打开与读取 : 在bmp_show.c中,bmp_show_mix函数首先打开BMP文件,并读取文件状态,然后读取BMP图像数据:

int fd_bmp = open(name, O_RDONLY);
struct stat pst;
fstat(fd_bmp, &pst);
char *buf = (char *)malloc(pst.st_size);
lseek(fd_bmp, 54, SEEK_SET); // 跳过BMP文件头
read(fd_bmp, buf, pst.st_size - 54); // 读取BMP像素数据
  1. 内存映射Framebuffer : 使用mmap函数将显示设备的帧缓冲区(Framebuffer)映射到用户空间,以便于直接操作显示内存:
char *p = (char *)mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd_lcd, 0);
  1. 图像数据复制 : 将读取的BMP图像数据复制到Framebuffer的指定位置:
for (j = 0; j < height; j++) {
    for (i = 0; i < width; i++) {
        memcpy(p + lcd_offset, buf + bmp_offset, 3); // 从BMP缓冲区复制到Framebuffer
    }
}
  1. 显示特定区域的BMP图像 : bmp_show_mix函数允许指定显示图像的起始位置(x0, y0)和大小(width, height),这可以用于在界面上显示图像的特定部分:
bmp_show_mix(x0, y0, width, height, name);
  1. 显示整个BMP图像 :bmp_show_self函数用于显示整个BMP图像,通常用于显示背景或全屏图像:
bmp_show_self(x0, y0, width, height, name);
  1. 释放资源 : 在图像显示完成后,需要释放分配的内存并关闭内存映射和文件描述符:
munmap(p, 800 * 480 * 4); // 关闭内存映射
close(fd_lcd); // 关闭Framebuffer文件描述符
close(fd_bmp); // 关闭BMP文件描述符
free(buf); // 释放分配的内存

3.1.2 游戏开始界面

  1. 效果
    在这里插入图片描述

  2. 功能: 展示游戏的初始界面,通常包含游戏的标题、开始游戏的按钮等元素。

  3. 实现: 使用bmp_show_mix函数加载和显示欢迎屏幕的背景图片

  4. 代码

void show_interface_welcome()
{
    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/welcom_bk1.bmp"); // 显示欢迎界面背景

    int x, y, event_type;
    int button_down = 0; // 用于记录按钮是否被按下

    while (1)
    {
        if (capture_touch_events(&x, &y, &event_type) == -1)
        {
            // 触摸事件捕获失败,可能需要处理错误或退出
            break;
        }

        if (event_type == 1)
        { // 触摸按下事件
            if (x > 440 && x < 620 && y > 360 && y < 460)
            {
                // 用户按下了按钮区域
                bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/welcom_bk1_push.bmp"); // 显示按钮按下的图片
                button_down = 1;
            }
            printf("Touch down at (%d, %d)\n", y, y);
        }
        else if (event_type == 0 && button_down)
        {   
            // 触摸离开事件且按钮之前被按下
            bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/welcom_bk1.bmp"); // 恢复按钮正常状态
            button_down = 0;
            printf("Touch up at (%d, %d)\n", y, y);

            if (x > 440 && x < 620 && y > 360 && y < 460)
            {
                break; // 离开循环,进入游戏
            }
        }
    }
}

3.1.3 游戏主界面

  1. 效果
    在这里插入图片描述

  2. 功能: 展示游戏进行中的界面,包括方块下落区域、下一个方块的预览区、分数和等级显示等。

  3. 实现: 在主循环中持续更新界面,显示当前活动方块、分数和等级。

  4. 代码

int main(int argc, char *argv[])
{

    show_interface_welcome();

    struct ls_all *head;

    // 显示背景图片
    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/bck.bmp");

    int rt;

    pthread_t idt, idr;

    // 获取两种随机形状并初始化,得到初始化结构体
    srand((unsigned int)time(NULL));
    shp = ((unsigned int)rand()) % 7 + 1;
    shp_next = ((unsigned int)rand()) % 7 + 1;

    bk = bk_init(shp);
    bk_next = bk_init(shp_next);

    // 初始化掉落方块结构体
    head = ls_init();

    // 初始化分数和速度
    score = 0;
    speed = 0;

    // 显示移动方块 及 提示方块
    the_show(bk);
    the_show_next(bk_next);
    score_show(0); // 显示成绩

    // 创建控制方块移动线程
    pthread_create(&idt, NULL, auto_down, (void *)head);

    // 时间更新线程,时间到且无操作自动更新dir为下落状态
    pthread_create(&idr, NULL, time_out, NULL);

    while (1)
    {

        // 锁定互斥锁以安全地读取 dir
        pthread_mutex_lock(&dir_mutex);
        int current_dir = dir; // 假设这是在循环中读取 dir 变量的地方
        pthread_mutex_unlock(&dir_mutex);

        if (paused == 1 || gameover == 1 || current_dir == -2)
        {
            usleep(100);
            continue;
        }

        if (current_dir == -1)
        { // 变形
            change_type(bk);
            the_show_bck_type(bk);
        }
        else
        { // 移动
            change_dir(bk->p, current_dir);
            the_show_bck_dir(bk->p, current_dir);
        }

        // 移动检查是否越界及掉落到底部
        bk = move_check(head, current_dir);
        if (bk == NULL)
        {
            return -1;
        }

        // 显示方块形状
        the_show(bk);

        pthread_mutex_lock(&dir_mutex);
        dir = -2; // 在主线程中置为 -2 ,表示不动
        pthread_mutex_unlock(&dir_mutex);
    }

    return 0;
}

3.1.4 游戏结束广告界面

  1. 效果
    在这里插入图片描述

  2. 功能: 当游戏结束时展示的界面,通常包含游戏结束的信息、最终得分和“重新开始”或“退出游戏”的选项。

  3. 实现: 使用show_interface_end函数来显示游戏结束的界面,处理用户的选择。

  4. 程序流程图
    在这里插入图片描述

  5. 代码

// 显示时间
void time_show(int n)
{

    int a1, a2, a3;
    char s[3][50];
    char st[3][50];
    int i;

    a1 = n / 100;     // 计算百位数字
    a2 = n / 10 % 10; // 计算十位数字
    a3 = n % 10;      // 计算个位数字

    for (i = 0; i < 3; i++)
    {

        bzero(s[i], 50); // 初始化字符串 s[i], 将其清零
    }

    s[0][0] = a1 + 48; // 将百位数字转换成字符,并存储到s[0]
    s[1][0] = a2 + 48; // 将十位数字转换成字符,并存储到s[1]
    s[2][0] = a3 + 48; // 将个位数字转换成字符,并存储到s[2]

    for (i = 0; i < 3; i++)
    {

        strcat(s[i], ".bmp\0");         // 在每个字符后面添加".bmp"扩展名
        strcpy(st[i], "./tetris_pic/"); // 将路径 "./tetris_pic/" 复制到 st[i]
        strcat(st[i], s[i]);            // 将文件名连接到路径后
        bmp_show_mix(280 + 20 * i, 45, 20, 20, st[i]);
        // printf("%s\n",st[i]);
    }
}

// 全局变量,用于线程间通信
int cut_down = 0;
pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁,用于同步对 seconds_left 的访问
pthread_cond_t count_cond = PTHREAD_COND_INITIALIZER;    // 条件变量,用于线程间同步

void *touch_event_thread(void *args)
{
    int x, y, event_type;
    int button_down = 0;
    while (1)
    {
        if (capture_touch_events(&x, &y, &event_type) == -1)
        {
            // 触摸事件捕获失败,可能需要处理错误或退出
            break;
        }
        // 检查按钮是否被按下
        if (event_type == 1 && x > 440 && x < 620 && y > 360 && y < 460)
        {
                // 用户按下了按钮区域
            bmp_show_self(BUTTON_X, BUTTON_Y+5, BUTTON_W, BUTTON_H-10, "./tetris_pic/bk_end_push.bmp"); // 显示按钮按下的图片
            button_down = 1; // 标记按钮被按下
        }

        // 检查按钮是否被按下并释放

        if (event_type == 0 && button_down)
        {
            // 用户释放按钮,提前重启游戏
            button_down = 0; // 重置按钮状态
            // 触摸离开事件且按钮之前被按下
            bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/bk_end.bmp"); // 恢复按钮正常状态

            if (x > 440 && x < 620 && y > 360 && y < 460)
            {
                pthread_mutex_lock(&count_mutex);
                cut_down = 1;
                pthread_cond_signal(&count_cond); // 发送信号给主线程
                pthread_mutex_unlock(&count_mutex);
                break;
            }
        }
        pthread_mutex_unlock(&count_mutex);
    }
    return NULL;
}

void show_interface_end()
{
    pause_game();
    // 重置倒计时和按钮状态
    int seconds_left = 20; // 20s 倒计时

    // 启动触摸事件线程
    pthread_t touch_thread_id;
    pthread_create(&touch_thread_id, NULL, touch_event_thread, NULL);

    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/bk_end.bmp"); // 显示结束广告界面
    usleep(300000);
    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/bk_end.bmp"); // 显示结束广告界面


    while (1)
    {
        pthread_mutex_lock(&count_mutex);
        // 检查倒计时是否结束或按钮是否被按下
        if (seconds_left <= 0 || cut_down == 1)
        {
            pthread_mutex_unlock(&count_mutex);
            break; // 倒计时结束或按钮被按下,退出循环
        }
        pthread_mutex_unlock(&count_mutex);

        time_show(seconds_left); // 显示剩余时间
        seconds_left--;          // 倒计时减少
        sleep(1);                // 等待一秒
    }

    // 取消触摸事件线程,如果它还在运行
    pthread_cancel(touch_thread_id);
    pthread_join(touch_thread_id, NULL);

    // 倒计时结束或用户提前重启游戏
    restart_game();
}

3.1.5 游戏界面中的触摸反馈

代码中的触摸反馈主要通过capture_touch_events函数来实现,该函数用于捕捉触摸屏的按下和释放(离开)事件,并根据这些事件来改变游戏的状态或者显示效果。以下是触摸反馈相关的关键代码片段和解释

图 3.5 触摸反馈效果展示

  1. 触摸事件捕捉 : capture_touch_events函数通过读取设备输入事件来捕捉触摸操作:
int capture_touch_events(int *x, int *y, int *event_type) {
    // ...
    if (ts.type == EV_KEY && ts.code == BTN_TOUCH) {
        if (ts.value == 1) { // 按下
            *event_type = 1;
            break;
        } else if (ts.value == 0) { // 离开
            *event_type = 0;
            break;
        }
    }
    // ...
}
  1. 触摸按下反馈 : 当用户按下触摸屏时,程序会识别为按下事件,并设置event_type为1:
if (event_type == 1) {
    // 触摸按下事件的处理
    // 例如,改变按钮的显示状态来提供反馈
    bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/bk_end_push.bmp");
    button_down = 1; // 标记按钮被按下
}
  1. 触摸释放反馈 : 当用户释放触摸屏时,程序会识别为离开事件,并设置event_type为0:
else if (event_type == 0 && button_down) {
    // 触摸离开事件的处理
    // 例如,恢复按钮的原始状态
    bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/bk_end.bmp");
    button_down = 0; // 重置按钮状态
}
  1. 按钮状态变化 : 在show_interface_welcome函数中,使用bmp_show_self来显示或隐藏按下的图片,以提供视觉反馈:
void show_interface_welcome() {
    // ...
    if (event_type == 1) {
        // 用户按下了按钮区域
        bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/welcom_bk1_push.bmp");
        button_down = 1;
    }
    // ...
    else if (event_type == 0 && button_down) {
        // 用户释放按钮
        bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/welcom_bk1.bmp");
        button_down = 0;
        // 添加进入游戏的逻辑
    }
    // ...
}
  1. 触摸事件线程 : 在touch_event_thread函数中,创建了一个线程专门处理触摸事件,以实现非阻塞的触摸反馈:
void *touch_event_thread(void *args) {
    // ...
    while (1) {
        // 捕捉触摸事件
        if (capture_touch_events(&x, &y, &event_type) == -1) {
            // 处理错误或退出
            break;
        }
        // 根据触摸事件更新游戏状态或界面
        // ...
    }
    return NULL;
}

3.1.6 游戏界面中的弹窗

弹窗功能主要通过bmp_show_self函数实现,该函数用于在指定位置显示图片资源,模拟弹窗效果。以下是弹窗功能相关的代码片段和解释:

图 3.6 游戏弹窗效果展示

  1. 游戏暂停弹窗 : 当用户触发暂停操作时,会显示一个暂停弹窗:
if (paused == 1)
{
    bmp_show_self(289, 159, 256, 115, "./tetris_pic/pause.bmp"); // 显示暂停弹窗
    show_pause = 1;
}
else if (show_pause == 1)
{
    show_pause = 0;
    bmp_show_self(289, 159, 256, 115, "./tetris_pic/bck.bmp"); // 恢复背景图
}
  1. 游戏结束弹窗 : 当游戏结束条件触发时,会显示一个游戏结束的弹窗:
if (gameover == 1)
{
    bmp_show_self(184, 157, 455, 94, "./tetris_pic/gameover.bmp"); // 显示游戏失败弹窗
}
  1. 按钮按下效果 : 在触摸事件处理中,当用户按下某个按钮区域时,会显示一个按钮按下的图片,这也是一种弹窗效果:
if (event_type == 1)
{
    // 用户按下了按钮区域
    bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/bk_end_push.bmp"); // 显示按钮按下的图片
    button_down = 1; // 标记按钮被按下
}
  1. 触摸事件处理 : capture_touch_events函数用于捕捉触摸屏的按下和离开事件,并返回相应的坐标和事件类型,这是实现弹窗功能的基础:
int capture_touch_events(int *x, int *y, int *event_type)
{
    // ...
    if (ts.value == 1) { // 按下
        *event_type = 1;
        break;
    }
    else if (ts.value == 0) { // 离开
        *event_type = 0;
        break;
    }
    // ...
}
  1. 界面显示函数 : show_interface_welcome和show_interface_end是两个界面显示函数,它们分别用于显示欢迎界面和结束界面,这些界面可以包含弹窗元素:
void show_interface_welcome()
{
    // 显示欢迎界面背景
    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/welcom_bk1.bmp");
    // ...
}

void show_interface_end()
{
    // 显示结束界面
    bmp_show_mix(0, 0, 800, 480, "./tetris_pic/bk_end.bmp");
    // ...
}

3.2 方块显示基本函数

3.2.1 绘制方块

  1. 功能: 根据方块的当前状态在界面上绘制方块。
  2. 实现: 通过the_show函数,根据方块的坐标和形状类型,显示方块的图片。
  3. 代码
// LCD显示移动的方块
void the_show(struct block *bk)
{

    int i;
    int *p = bk->p;
    int shp = bk->shape;

    char s[50];

    switch (shp)
    {

    case 1:
        strcpy(s, "./tetris_pic/O.bmp");
        break;
    case 2:
        strcpy(s, "./tetris_pic/I.bmp");
        break;
    case 3:
        strcpy(s, "./tetris_pic/S.bmp");
        break;
    case 4:
        strcpy(s, "./tetris_pic/Z.bmp");
        break;
    case 5:
        strcpy(s, "./tetris_pic/L.bmp");
        break;
    case 6:
        strcpy(s, "./tetris_pic/J.bmp");
        break;
    case 7:
        strcpy(s, "./tetris_pic/T.bmp");
        break;
    }

    for (i = 0; i < 4; i++)
    {
        bmp_show_mix(p[i * 2], p[i * 2 + 1], 20, 20, s);
    }
}

3.2.2 擦除方块

  1. 功能: 当方块移动或变形后,需要先擦除原来的方块,再在新位置绘制。
  2. 实现: 使用the_show_bck_dir或the_show_bck_type函数显示方块原来位置的背景色。
  3. 代码
// 方块移动后需要把原来的方块--》消失--》显示背景色
void the_show_bck_dir(int *p, int dir)
{ // dir: 0-down  1-left  2-right

    int i;

    for (i = 0; i < 4; i++)
    {
        if (dir == 0)
        {
            bmp_show_self(p[i * 2], p[i * 2 + 1] - 20, 20, 20, "./tetris_pic/bck.bmp");
        }
        else if (dir == 1)
        {
            bmp_show_self(p[i * 2] + 20, p[i * 2 + 1], 20, 20, "./tetris_pic/bck.bmp");
        }
        else if (dir == 2)
        {
            bmp_show_self(p[i * 2] - 20, p[i * 2 + 1], 20, 20, "./tetris_pic/bck.bmp");
        }
    }
}

// 方块变形后让之前的--》消失--》显示背景色
void the_show_bck_type(struct block *bk)
{

    int i;
    if (bk->type == 1)
        bk->type = 5; // 如果是形态1将type改为5使其计算结果正确

    for (i = 0; i < 4; i++)
    { // 还原上一个位置的背景图

        bmp_show_self(bk->p[i * 2] - bk->p[i * 2 + (bk->type - 1) * 8],
                      bk->p[i * 2 + 1] - bk->p[i * 2 + (bk->type - 1) * 8 + 1], 20, 20, "./tetris_pic/bck.bmp");
    }
    if (bk->type == 5)
        bk->type = 1; // 还原回来
}

3.2.3 随机生成一个方块

  1. 功能: 游戏需要不断生成新的方块供玩家操作。
  2. 实现: 在main函数中使用rand函数生成随机数,决定下一个方块的形状类型,并使用bk_init函数初始化方块的属性。
  3. 代码:
    // 获取两种随机形状并初始化,得到初始化结构体
    srand((unsigned int)time(NULL));
    shp = ((unsigned int)rand()) % 7 + 1;
shp_next = ((unsigned int)rand()) % 7 + 1;


// 方块掉落后 得到一个新形状的方块 初始化函数--》得出初始化结构体
struct block *bk_init(int shape)
{

    struct block *bk;
    bk = (struct block *)malloc(sizeof(struct block));

    switch (shape)
    {
    case 1:
        bk->p = arry_init_O();
        bk->shape = 1;
        break;
    case 2:
        bk->p = arry_init_I();
        bk->shape = 2;
        break;
    case 3:
        bk->p = arry_init_S();
        bk->shape = 3;
        break;
    case 4:
        bk->p = arry_init_Z();
        bk->shape = 4;
        break;

    case 5:
        bk->p = arry_init_L();
        bk->shape = 5;
        break;
    case 6:
        bk->p = arry_init_J();
        bk->shape = 6;
        break;
    case 7:
        bk->p = arry_init_T();
        bk->shape = 7;
        break;
    default:
        break;
    }

    bk->type = 1;

    return bk;
}

3.3 方块处理基本函数

3.3.1 左移函数

  1. 功能: 控制方块向左移动一格。
  2. 实现: 使用change_dir函数,设置方向参数为向左移动,更新方块的位置。
  3. 代码:
// 移动方块,仅限三种方向
void change_dir(int *p, int dir)
{ // dir: 0-down  1-left  2-right
    //printf("dir:%d\n", dir);
    int i = 0;
    for (; i < 4; i++)
    {
        if (dir == 0)
        {
            p[i * 2 + 1] += 20;
        }
        else if (dir == 1)
        {
            p[i * 2] -= 20;
        }
        else if (dir == 2)
        {
            p[i * 2] += 20;
        }
    }
}

// 移动越界 需要恢复回原来的坐标--》dir: 0-down  1-left  2-right
void change_dir_off(int *p, int dir)
{ // dir: 0-down  1-left  2-right

    int i = 0;

    // if(dir == 0){
    // printf("change_dir_off error\n");
    // exit(-1);
    // }

    for (; i < 4; i++)
    {

        if (dir == 1)
        {
            p[i * 2] += 20;
        }
        else if (dir == 2)
        {
            p[i * 2] -= 20;
        }
        else if (dir == 0)
        {
            p[i * 2 + 1] -= 20;
        }
    }
}

3.3.2 变形函数
1)	功能: 允许方块在垂直方向上旋转,改变形状。
2)	实现: 使用change_type函数,更新方块的形状状态,并重新绘制方块。
3)	代码:

// 变形
void change_type(struct block *bk)
{

    int i;

    if (bk->shape == 1)
    { // 如果方块直接返回
        return;
    }

    for (i = 0; i < 4; i++)
    {
        // 更新坐标值到下一个形态
        bk->p[i * 2] += bk->p[i * 2 + bk->type * 8]; // 因为用int存贮,
                                                     // 且一个坐标信息占两个int所以要 *8
        bk->p[i * 2 + 1] += bk->p[i * 2 + bk->type * 8 + 1];
        // printf("%d\t%d\t",bk->p[i*2+bk->type*8],bk->p[i*2+bk->type*8+1]);
    }

    // 更新到下一个旋转状态
    bk->type++;

    if (bk->type >= 5)
    {
        bk->type = 1;
    }

    return;
}

3.3.3 碰撞函数
1)	功能: 检测方块移动时是否与其它方块或游戏边界发生碰撞。
2)	实现: 通过bound_check函数检测方块的坐标是否越界,并相应地调整方块的位置。
3)	代码:

// 检查方块左右下移动时有无越界--》下越界返回0、左越界返回-1、右越界返回-2
int bound_check(int *p)
{

    int i;

    for (i = 0; i < 4; i++)
    {

        if (p[i * 2 + 1] > 460)
        {
            return 0; // down out
        }

        if (p[i * 2] > 300)
        {
            return -2; // right out
        }
        else if (p[i * 2] < 0)
        {
            return -1; // left out
        }
    }

    return 1;
}

3.3.4 消行函数

  1. 功能: 当一行为完全填满时,自动消除该行并为玩家增加分数。
  2. 实现: 在ls_check_self函数中扫描整个链表,检测并消除满行,更新分数,并重新绘制界面。
  3. 代码:
// 检查整个链表有无消行--》把整个屏幕行扫描式检测--》方块到顶返回-1
int ls_check_self(struct ls_all *head)
{
    struct ls_all *tmp; // 零时指针,用于遍历链表
    int i = 460;        // 初始化为460,从屏幕底部开始扫描
    int n = 0;          // 统计当前行的方块数量
    tmp = head;
    tmp = tmp->next; // 初始化tmp为链表的第二个节点(链表为带头节点的双向链表)

    while (i >= 40)
    {          // 从底部 460 开始一直扫描到顶部 40
        n = 0; // 每次开始循环将方块数置 0,tmp指向第二个节点
        tmp = head;
        tmp = tmp->next;

        // 1.扫描当前行
        while (tmp != head)
        {
            if (tmp->y0 == i)
            { // 如果方块在当前行
                n++;
                if (i < 80)
                {                          // 如果方块在顶部区域(游戏结束)
                    printf("game over\n"); //
                    gameover = 1;
                    bmp_show_self(184,157,455,94,"./tetris_pic/gameover.bmp"); // 显示游戏失败弹窗
                    sleep(3);
                    return -1; // 返回-1表示游戏结束
                }
            }
            // printf("%d  %d\n",i,tmp->y0);

            tmp = tmp->next;
        }

        // 2.判断当前行已经填满(即有16个方块在同一行)
        if (n == 16)
        {
            score++; // 消一行加一分
            printf("%d line\n", score);
            score_show(score);

            speed = score / 10;

            // 3.重新显示背景图(擦除所有的方块)
            bmp_show_self(0, 33, 320, 447, "./tetris_pic/bck.bmp");

            // 4.再次遍历链表,删除当前行的方块并下移上方的方块
            tmp = head;
            tmp = tmp->next;
            while (tmp != head)
            {
                if (tmp->y0 == i)
                {                      // 如果当前方块在删除行
                    tmp = ls_del(tmp); // 删除当前方块
                }
                // printf("%d  %d\n",i,tmp->y0);
                else if (tmp->y0 < i)
                {                  // 如果当前方块在当前方块之上
                    tmp->y0 += 20; // 将方块下移
                }

                tmp = tmp->next;
            }

            // 5.重新显示所有方块
            ls_all_show(head);
            i += 20; // 因为当前行被删除,需要下移一行重新检测
        }

        i -= 20; // 上移一行
    }

    return 0; // 正常退出,游戏继续进行
}

3.3.5 方块移动中的加速下落

按下向下键通常会导致方块加速下落。这种加速下落的行为可以通过减少方块下落的时间间隔来实现,或者通过覆盖当前下落状态的变量来立即触发下落动作。以下是代码中实现按下向下键后加速下落的相关片段和解释:

  1. 加速按钮的逻辑 : 当用户按下加速按钮时(基于触摸位置判断),如果当前速度小于某个阈值(例如76),则速度会相应增加:
if (x > 760 && x < 840 && y > 390 && y < 470) {
    if (event_type == 1) {
        if (speed < 76) {
            speed = speed + 76; // 增加速度
        }
        bmp_show_self(584, 331, 76, 72, "./tetris_pic/bck_push.bmp"); // 显示按钮按下后的图片
        show_dir = 1;
    }
}
  1. 按下向下键的逻辑 : 当按下向下键时,如果当前方块没有达到最大速度,可以进一步加速下落:
if (event_type == 1 && dir == DOWN_KEY) { // 假设DOWN_KEY是向下键对应的值
    if (speed < MAX_SPEED) { // MAX_SPEED是定义的最大速度常量
        speed = speed + INCREMENT; // INCREMENT是每次加速增加的速度值
    }
    // 可以添加代码以立即下落到底部或更新显示
}
  1. 速度更新 : 在主循环或相关线程中,根据speed变量来调整方块下落的时间间隔,速度越快,下落越频繁:
void *auto_down(void *arg) {
    while (1) {
        // ...
        usleep((400 - speed * 5) * 1000); // 根据速度调整下落间隔
        // 执行下落动作
        // ...
    }
}
  1. 主循环中的处理 : 在主循环中,检测按下向下键或加速按钮的事件,并更新速度和方块状态:
while (1) {
    // 检测按下向下键或加速按钮的逻辑
    // ...

    // 根据当前速度更新方块位置
    // ...

    // 显示当前方块位置
    the_show(bk);
}
  1. 线程间通信 : 如果使用多线程,按下向下键或加速按钮后,需要通过互斥锁更新共享的速度变量,以通知其他线程方块状态的变化:
pthread_mutex_lock(&dir_mutex);
speed = updated_speed; // updated_speed是更新后的速度值
pthread_mutex_unlock(&dir_mutex);

3.4 游戏代码中的链表

链表在游戏中扮演着核心的数据结构角色,用于动态维护和更新俄罗斯方块中各个方块的状态和位置。通过链表,游戏能够有效地追踪每个方块的下落过程,检测方块间的碰撞,实现方块到达底部时的自动堆叠,以及在形成完整行时的消除功能。这种数据组织方式提供了灵活高效的内存管理和访问机制,确保了游戏逻辑的正确执行和流畅的用户体验。

  1. 定义链表节点结构 (struct ls_all): 在list.h文件中定义了链表节点的结构体,每个节点代表一个方块。
struct ls_all {
    struct ls_all *next;
    struct ls_all *pre;
    int shape; // 形状用来区分显示颜色
    int x0;
    int y0;
};
  1. 初始化链表 (ls_init 函数):创建一个新的链表头节点,并使其指向自己,形成一个循环链表。
struct ls_all *ls_init() {
    struct ls_all *head = (struct ls_all *)malloc(sizeof(struct ls_all));
    head->next = head;
    head->pre = head;
    return head;
}
  1. 添加节点到链表 (ls_add 函数):使用尾插法在链表末尾添加新的方块节点。
void ls_add(struct ls_all *head, int x0, int y0, int shape) {
    struct ls_all *node = (struct ls_all *)malloc(sizeof(struct ls_all));
    // ... 省略中间代码 ...
    node->x0 = x0;
    node->y0 = y0;
    node->shape = shape;
}
  1. 删除链表节点 (ls_del 函数):从链表中删除指定的节点,通常用于消行操作。
struct ls_all *ls_del(struct ls_all *node) {
    struct ls_all *tmp = node->pre;
    tmp->next = node->next;
    node->next->pre = tmp;
    free(node);
    return tmp;
}
  1. 检查方块是否到达底部 (ls_check 函数):检查方块是否与链表底部的节点重叠,如果是,则方块到达底部。
int ls_check(struct ls_all *head, int *p) {
    // ... 省略中间代码 ...
    return -1; // 如果到达底部或越界,返回-1
}
  1. 并检查消行 (ls_updata 函数):将方块添加到链表中,并检查是否有完整的行需要消除。
int ls_updata(struct ls_all *head, struct block *bk) {
    // ... 省略中间代码 ...
    if (ls_check_self(head) == -1) {
        return -1; // 如果检测到游戏结束,返回-1
    }
}
  1. 检查并处理消行 (ls_check_self 函数):遍历链表,检查是否有满行,如果有,则进行消行处理。
int ls_check_self(struct ls_all *head) {
    // ... 省略中间代码 ...
    if (n == 16) { // 如果一行中有16个方块,即填满一行
        // 执行消行操作
    }
    return 0; // 正常退出,游戏继续
}
  1. 显示链表中的所有方块 (ls_all_show 函数):遍历链表,显示每一个方块。
void ls_all_show(struct ls_all *head) {
    struct ls_all *tmp = head->next; // 开始遍历
    while (tmp != head) {
        // 显示方块
        tmp = tmp->next;
    }
}

3.5 游戏代码中的多线程

多线程被用于实现游戏的不同功能,如自动下落、触摸事件处理和时间更新等。多线程允许这些功能并发运行,从而提高程序的响应性和性能。以下是对游戏代码中多线程使用的详细解释:

  1. 线程创建 : 在main函数中,使用pthread_create创建了多个线程:
pthread_t idt, idr;

// 创建控制方块移动线程
pthread_create(&idt, NULL, auto_down, (void *)head);

// 时间更新线程,时间到且无操作自动更新dir为下落状态
pthread_mutex_lock(&dir_mutex);
dir = -2; // 初始状态为静止
pthread_mutex_unlock(&dir_mutex);
pthread_create(&idr, NULL, time_out, NULL);
  1. 自动下落线程 (auto_down) : 这个线程负责方块的自动下落逻辑。它在一个无限循环中运行,根据speed变量控制下落的速度:
void *auto_down(void *arg) {
    // 线程内部逻辑
    // ...
}
  1. 时间更新线程 (time_out) : 这个线程负责处理游戏的时间逻辑,例如,当没有用户交互时自动改变方块的下落方向:
void *time_out(void *arg) {
    // 线程内部逻辑
    // ...
}
  1. 触摸事件线程 : 在其他函数中,如show_interface_welcome或touch_event_thread,也可能创建额外的线程来处理触摸事件:
void *touch_event_thread(void *args) {
    // 处理触摸事件的线程逻辑
    // ...
}
  1. 线程同步 : 使用互斥锁(pthread_mutex_t)来同步对共享资源的访问,如方向变量dir:
pthread_mutex_lock(&dir_mutex);
dir = 0; // 设置下落方向
pthread_mutex_unlock(&dir_mutex);
  1. 线程取消 : 在某些情况下,如游戏结束或重启,可能需要取消线程:
pthread_cancel(thread_id);
  1. 线程等待 : 在主函数中,可能需要等待所有子线程完成,以确保资源被正确释放:
pthread_join(idt, NULL);
pthread_join(idr, NULL);
  1. 线程安全的操作 : 在多线程环境中,对共享资源的所有操作都应该是线程安全的。例如,更新分数或处理游戏状态的变量时,需要使用互斥锁来避免竞态条件。

  2. 条件变量 : 有时线程间需要基于某些条件进行同步,这时可以使用条件变量

pthread_cond_signal(&count_cond); // 发送信号给其他线程
pthread_cond_wait(&count_cond, &count_mutex); // 等待信号

总结:

本文详细介绍了一款基于ARM开发板GEC6818的俄罗斯方块游戏的设计和实现。从总体设计思路出发,我们采用了模块化编程方法,将游戏分解为图形显示、触摸事件处理、游戏控制、界面显示、链表管理、移动逻辑和主控等多个模块,以提高代码的可维护性和扩展性。通过C语言编程和多线程技术的应用,游戏实现了方块的移动、变形、随机生成、触屏控制、暂停恢复、嵌套消行和计分等功能,确保了游戏的流畅性和稳定性。

在界面设计上,游戏提供了直观的图形界面和触摸反馈,玩家可以轻松跟踪游戏进度和控制游戏流程。程序流程图清晰地展示了游戏从初始化到运行再到结束的整个过程,使读者能够快速把握游戏的逻辑结构。各模块的详细说明和代码实现,不仅展示了开发团队的技术实力,也为嵌入式系统开发爱好者提供了宝贵的学习资料。

此外,文章还对游戏代码中的链表和多线程技术进行了深入分析,展示了如何使用链表管理方块布局和实现动态内存管理,以及如何利用多线程提高程序的响应速度和性能。这些技术的应用,不仅提升了游戏的运行效率,也为复杂系统的开发提供了可行的解决方案。

总之,这款俄罗斯方块游戏的开发过程,不仅锻炼了嵌入式系统开发能力,也展示了模块化设计和多线程技术在实际应用中的强大功能。通过本文的阅读,读者不仅能够获得游戏开发的全面认识,还能从中学习到嵌入式系统编程的实用技巧和最佳实践。

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q_hd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值