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

文章目录

  • 前言:
  • 1. 简介
  • 2. 总体设计思路及功能描述
    • 2.1 设计思路
    • 2.2 功能描述
    • 2.3 程序流程图
  • 3. 各部分程序功能及详细说明
    • 3.1 游戏界面函数
      • 3.1.1 游戏界面中的图片显示
      • 3.1.2 游戏开始界面
      • 3.1.3 游戏主界面
      • 3.1.4 游戏结束广告界面
      • 3.1.5 游戏界面中的触摸反馈
      • 3.1.6 游戏界面中的弹窗
    • 3.2 方块显示基本函数
      • 3.2.1 绘制方块
    • 3.2.2 擦除方块
    • 3.2.3 随机生成一个方块
    • 3.3 方块处理基本函数
      • 3.3.1 左移函数
      • 3.3.4 消行函数
      • 3.3.5 方块移动中的加速下落
    • 3.4 游戏代码中的链表
    • 3.5 游戏代码中的多线程
  • 总结:

前言:

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

gitee:https://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语言编程和多线程技术的应用,游戏实现了方块的移动、变形、随机生成、触屏控制、暂停恢复、嵌套消行和计分等功能,确保了游戏的流畅性和稳定性。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/763490.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring Boot】基于 JPA 开发的文章管理系统(CRUD)

基于 JPA 开发的文章管理系统&#xff08;CRUD&#xff09; 1.实现文章实体2.实现数据持久层3.实现服务接口和服务接口的实现类3.1 创建服务接口3.2 编写服务接口的实现 4.实现增、删、改、查的控制层 API 功能4.1 获取文章列表4.2 根据 id 获取文章对象4.3 新增4.4 保存4.5 删…

第三届环境工程与可持续能源国际会议(EESE 2024)

随着全球气候变化和环境问题日益严峻&#xff0c;环境工程与可持续能源领域的研究和发展显得尤为重要。第三届环境工程与可持续能源国际会议&#xff08;EESE 2024&#xff09;作为这一领域的重要交流平台&#xff0c;将于2024年10月25日至27日在湖南长沙盛大召开。本次会议将汇…

算法实验2.2、2.3

2.2主要内容 比较快速排序&#xff0c;归并排序以及堆排序算法的时间效率。了解影响算法执行时间的 主要因素以及如何降低算法的执行时间。 #include<iostream> using namespace std; #include<stdio.h> #include<malloc.h> #include<stdlib.h> #inc…

vue全局方法plugins/utils

一、在src目录下创建一个plugins文件夹 test.ts文件存放创建的方法&#xff0c;index.ts用于接收所有自定义方法进行统一处理 二、编写自定义方法 // test.ts文件 export default {handleTest(val1: number, val2: number) {// 只是一个求和的方法return val1 val2;}, };三…

MySQL数据库的主从复制与读写分离

一、MySQL数据库的主从复制 1.主从复制的概述及原理 &#xff08;1&#xff09;主从复制的意义 在实际的生产环境中&#xff0c;如果对数据库的读和写都在同一个数据库服务器中操作,无论是在安全性、高可用性还是高并发等各个方面都是完全不能满足实际需求的。因此&#xff…

【Nvidia+AI相机】涂布视觉检测方案专注提高锂电池质量把控标准

锂电池单元的质量在多个生产制造领域都至关重要&#xff0c;特别是在新能源汽车、高端消费电子等行业。这些领域的产品高度依赖锂电池提供持续、稳定的能量供应。优质的锂电池单元不仅能提升产品的性能和用户体验&#xff0c;还能确保使用安全。因此&#xff0c;保证锂电池单元…

微信小程序template模板引入

如图&#xff1a;temp.wxml是template引入的模板 在two.wxml中&#xff1a; import&#xff1a;是引入temp的页面让template中的内容显示出来在two页面中&#xff1b; include:是显示temp页面内容不在template包裹&#xff0c;template以外的view标签文字和不在view的文字让…

探索PcapPlusPlus开源库:网络数据包处理与性能优化

文章目录 0. 本文概要1. PcapPlusPlus介绍1.1 概述1.2主要特性和功能1.3 PcapPlusPlus 主要模块关系和依赖1.4 网络协议层处理过程 2. 实例2.1 基于 PcapPlusPlus 的应用程序设计和封装流程&#xff1a;2.2 多线程示例代码2.3 代码说明&#xff1a; 3. 程序性能进一步优化3.1 避…

Golang内存分配

Go内存分配语雀笔记整理 Golang内存模型设计理念思考核心代码阅读mspanmcachemcentral中心缓存mheap分配过程 Golang内存模型设计理念思考 golang内存分配基于TCmalloc模型&#xff0c;它核心在于&#xff1a;空间换时间&#xff0c;一次缓存&#xff0c;多次复用&#xff1b;…

基于x86+FPGA+AI轴承缺陷视觉检测系统,摇枕弹簧智能检测系统

一、承缺陷视觉检测系统 应用场景 轴类零件自动检测设备&#xff0c;集光、机、软件、硬件&#xff0c;智能图像处理等先进技术于一体&#xff0c;利用轮廓特征匹配&#xff0c;目标与定位&#xff0c;区域选取&#xff0c;边缘提取&#xff0c;模糊运算等算法实现人工智能高…

Linux 高级编程——线程控制

线程控制&#xff1a;互斥与同步 概念&#xff1a; 互斥 》在多线程中对临界资源的排他性访问。 互斥机制 》互斥锁 》保证临界资源的 访问控制。 pthread_mutex_t mutex; 互斥锁类型 互斥锁变量 内核对象 框架&#xff1a; 定义互斥锁 》初始化锁 》加…

Kafka-服务端-副本同步-源码流程

杂 在0.9.0.0之前&#xff0c;Kafka提供了replica lag.max.messages 来控制follower副本最多落后leader副本的消息数量&#xff0c;follower 相对于leader 落后当超过这个数量的时候就判定该follower是失效的&#xff0c;就会踢出ISR&#xff0c;这里的指的是具体的LEO值。 对…

Hadoop权威指南-读书笔记-01-初识Hadoop

Hadoop权威指南-读书笔记 记录一下读这本书的时候觉得有意思或者重要的点~ 第一章—初识Hadoop Tips&#xff1a; 这个引例很有哲理嘻嘻&#x1f604;&#xff0c;道出了分布式的灵魂。 1.1 数据&#xff01;数据&#xff01; 这一小节主要介绍了进入大数据时代&#xff0c;面…

【windows|012】光猫、路由器、交换机详解

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 ​ &#x1f3c5;阿里云ACE认证高级工程师 ​ &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社…

QML学习——Qt Quick Extras Examples 1.4(八)

Qt Quick Extras Examples 阅读官方的源码然后尝试做了下 01 A car dashboard 样例演示&#xff1a; 说明&#xff1a; ValueSource组件控制数值相关的动画&#xff0c;例如图中数值的变化&#xff1b;TurnIndicator组件是控制左右方向灯的闪烁和背景&#xff0c;里面使用…

excel修改批量一列单价的金额并保留1位小数

1.打开表格&#xff0c;要把单价金额变成现在的两倍&#xff0c;数据如下&#xff1a; 2.把单价这一列粘贴到一个新的sheet页面&#xff0c;在B2单元格输入公式&#xff1a;A2*2 然后按enter回车键,这时候吧鼠标放到B2单元格右下角&#xff0c;会出现一个黑色的小加号&#xf…

SQL 注入联合查询之为什么要 and 1=2

在 SQL 注入联合查询中&#xff0c;将 id 先置为假&#xff08;如 id-1 或其他使查询结果为空的条件&#xff09;&#xff0c;通常是为了让前面的查询语句查询不到结果&#xff0c;从而使联合查询中后面的语句结果能够显示在回显位上

【深度学习】pytorch训练中的一个大坑

使用的命令&#xff1a;iostat -x 5 可以看到 ssd的利用率已经满了。 之前在的数据集放在了 hdd上&#xff0c;训练结果特别慢。 所以我把它移动到了ssd上&#xff0c;然后训练参数用的 resume&#xff0c; 但是&#xff01;&#xff01;&#xff01;&#xff01;它把历史记住…

虚拟环境管理

虚拟环境 在使用 Python 时我们一般使用“pip install 第三方包名”来安装第三方包&#xff0c;但是由于pip的特性&#xff0c;系统只能安装每个包的一个版本。而在实际开发中&#xff0c;可能同时开发多个项目&#xff0c;如&#xff1a;上图有三个项目&#xff1b;每个项目需…