文章目录
- 前言:
- 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 功能描述
- 图形显示模块:负责加载和显示BMP图片到屏幕上,支持指定区域的图片显示,用于游戏方块和背景的绘制。
- 触摸事件处理模块:监听触摸屏事件,将用户的触摸操作转换为游戏内的控制指令。
- 游戏控制模块:包含游戏的暂停和重启功能,允许玩家在任何时候暂停游戏,并在适当的时候恢复或重新开始。
- 界面显示模块:管理游戏的开始界面和结束界面,提供用户交互的界面元素。
- 链表管理模块:使用链表数据结构管理游戏中的方块布局,实现方块的动态添加和删除。
- 移动逻辑模块:控制方块的移动、变形和消行等逻辑,确保游戏规则的准确执行。
- 主控模块:作为程序的入口,初始化游戏环境,创建和管理线程,控制游戏的主循环。
2.3 程序流程图
程序流程从初始化游戏环境开始,显示欢迎界面,然后进入一个循环等待用户的触摸操作。一旦检测到触摸事件,程序将处理这些输入并更新游戏状态。随后,程序检查游戏是否结束,如果是,则显示游戏结束界面,并等待用户决定是否重启游戏或退出。如果用户选择重启,程序将重新初始化游戏环境;如果选择退出,则程序将结束运行。
3. 各部分程序功能及详细说明
3.1 游戏界面函数
3.1.1 游戏界面中的图片显示
代码中使用BMP文件格式来显示图像资源,这些图像用于游戏的图形界面,如方块、背景、按钮等元素。显示BMP图像的功能主要通过bmp_show.h头文件中声明的函数来实现。以下是与BMP显示相关的代码片段和解释:
-
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); -
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像素数据
- 内存映射Framebuffer : 使用mmap函数将显示设备的帧缓冲区(Framebuffer)映射到用户空间,以便于直接操作显示内存:
char *p = (char *)mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd_lcd, 0);
- 图像数据复制 : 将读取的BMP图像数据复制到Framebuffer的指定位置:
for (j = 0; j < height; j++) {
for (i = 0; i < width; i++) {
memcpy(p + lcd_offset, buf + bmp_offset, 3); // 从BMP缓冲区复制到Framebuffer
}
}
- 显示特定区域的BMP图像 : bmp_show_mix函数允许指定显示图像的起始位置(x0, y0)和大小(width, height),这可以用于在界面上显示图像的特定部分:
bmp_show_mix(x0, y0, width, height, name);
- 显示整个BMP图像 :bmp_show_self函数用于显示整个BMP图像,通常用于显示背景或全屏图像:
bmp_show_self(x0, y0, width, height, name);
- 释放资源 : 在图像显示完成后,需要释放分配的内存并关闭内存映射和文件描述符:
munmap(p, 800 * 480 * 4); // 关闭内存映射
close(fd_lcd); // 关闭Framebuffer文件描述符
close(fd_bmp); // 关闭BMP文件描述符
free(buf); // 释放分配的内存
3.1.2 游戏开始界面
-
效果:
-
功能: 展示游戏的初始界面,通常包含游戏的标题、开始游戏的按钮等元素。
-
实现: 使用bmp_show_mix函数加载和显示欢迎屏幕的背景图片
-
代码:
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 游戏主界面
-
效果:
-
功能: 展示游戏进行中的界面,包括方块下落区域、下一个方块的预览区、分数和等级显示等。
-
实现: 在主循环中持续更新界面,显示当前活动方块、分数和等级。
-
代码:
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 游戏结束广告界面
-
效果:
-
功能: 当游戏结束时展示的界面,通常包含游戏结束的信息、最终得分和“重新开始”或“退出游戏”的选项。
-
实现: 使用show_interface_end函数来显示游戏结束的界面,处理用户的选择。
-
程序流程图:
-
代码:
// 显示时间
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函数来实现,该函数用于捕捉触摸屏的按下和释放(离开)事件,并根据这些事件来改变游戏的状态或者显示效果。以下是触摸反馈相关的关键代码片段和解释
- 触摸事件捕捉 : 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;
}
}
// ...
}
- 触摸按下反馈 : 当用户按下触摸屏时,程序会识别为按下事件,并设置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; // 标记按钮被按下
}
- 触摸释放反馈 : 当用户释放触摸屏时,程序会识别为离开事件,并设置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; // 重置按钮状态
}
- 按钮状态变化 : 在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;
// 添加进入游戏的逻辑
}
// ...
}
- 触摸事件线程 : 在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 游戏弹窗效果展示
- 游戏暂停弹窗 : 当用户触发暂停操作时,会显示一个暂停弹窗:
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"); // 恢复背景图
}
- 游戏结束弹窗 : 当游戏结束条件触发时,会显示一个游戏结束的弹窗:
if (gameover == 1)
{
bmp_show_self(184, 157, 455, 94, "./tetris_pic/gameover.bmp"); // 显示游戏失败弹窗
}
- 按钮按下效果 : 在触摸事件处理中,当用户按下某个按钮区域时,会显示一个按钮按下的图片,这也是一种弹窗效果:
if (event_type == 1)
{
// 用户按下了按钮区域
bmp_show_self(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, "./tetris_pic/bk_end_push.bmp"); // 显示按钮按下的图片
button_down = 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;
}
// ...
}
- 界面显示函数 : 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 绘制方块
- 功能: 根据方块的当前状态在界面上绘制方块。
- 实现: 通过the_show函数,根据方块的坐标和形状类型,显示方块的图片。
- 代码:
// 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 擦除方块
- 功能: 当方块移动或变形后,需要先擦除原来的方块,再在新位置绘制。
- 实现: 使用the_show_bck_dir或the_show_bck_type函数显示方块原来位置的背景色。
- 代码:
// 方块移动后需要把原来的方块--》消失--》显示背景色
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 随机生成一个方块
- 功能: 游戏需要不断生成新的方块供玩家操作。
- 实现: 在main函数中使用rand函数生成随机数,决定下一个方块的形状类型,并使用bk_init函数初始化方块的属性。
- 代码:
// 获取两种随机形状并初始化,得到初始化结构体
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 左移函数
- 功能: 控制方块向左移动一格。
- 实现: 使用change_dir函数,设置方向参数为向左移动,更新方块的位置。
- 代码:
// 移动方块,仅限三种方向
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 消行函数
- 功能: 当一行为完全填满时,自动消除该行并为玩家增加分数。
- 实现: 在ls_check_self函数中扫描整个链表,检测并消除满行,更新分数,并重新绘制界面。
- 代码:
// 检查整个链表有无消行--》把整个屏幕行扫描式检测--》方块到顶返回-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 方块移动中的加速下落
按下向下键通常会导致方块加速下落。这种加速下落的行为可以通过减少方块下落的时间间隔来实现,或者通过覆盖当前下落状态的变量来立即触发下落动作。以下是代码中实现按下向下键后加速下落的相关片段和解释:
- 加速按钮的逻辑 : 当用户按下加速按钮时(基于触摸位置判断),如果当前速度小于某个阈值(例如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;
}
}
- 按下向下键的逻辑 : 当按下向下键时,如果当前方块没有达到最大速度,可以进一步加速下落:
if (event_type == 1 && dir == DOWN_KEY) { // 假设DOWN_KEY是向下键对应的值
if (speed < MAX_SPEED) { // MAX_SPEED是定义的最大速度常量
speed = speed + INCREMENT; // INCREMENT是每次加速增加的速度值
}
// 可以添加代码以立即下落到底部或更新显示
}
- 速度更新 : 在主循环或相关线程中,根据speed变量来调整方块下落的时间间隔,速度越快,下落越频繁:
void *auto_down(void *arg) {
while (1) {
// ...
usleep((400 - speed * 5) * 1000); // 根据速度调整下落间隔
// 执行下落动作
// ...
}
}
- 主循环中的处理 : 在主循环中,检测按下向下键或加速按钮的事件,并更新速度和方块状态:
while (1) {
// 检测按下向下键或加速按钮的逻辑
// ...
// 根据当前速度更新方块位置
// ...
// 显示当前方块位置
the_show(bk);
}
- 线程间通信 : 如果使用多线程,按下向下键或加速按钮后,需要通过互斥锁更新共享的速度变量,以通知其他线程方块状态的变化:
pthread_mutex_lock(&dir_mutex);
speed = updated_speed; // updated_speed是更新后的速度值
pthread_mutex_unlock(&dir_mutex);
3.4 游戏代码中的链表
链表在游戏中扮演着核心的数据结构角色,用于动态维护和更新俄罗斯方块中各个方块的状态和位置。通过链表,游戏能够有效地追踪每个方块的下落过程,检测方块间的碰撞,实现方块到达底部时的自动堆叠,以及在形成完整行时的消除功能。这种数据组织方式提供了灵活高效的内存管理和访问机制,确保了游戏逻辑的正确执行和流畅的用户体验。
- 定义链表节点结构 (struct ls_all): 在list.h文件中定义了链表节点的结构体,每个节点代表一个方块。
struct ls_all {
struct ls_all *next;
struct ls_all *pre;
int shape; // 形状用来区分显示颜色
int x0;
int y0;
};
- 初始化链表 (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;
}
- 添加节点到链表 (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;
}
- 删除链表节点 (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;
}
- 检查方块是否到达底部 (ls_check 函数):检查方块是否与链表底部的节点重叠,如果是,则方块到达底部。
int ls_check(struct ls_all *head, int *p) {
// ... 省略中间代码 ...
return -1; // 如果到达底部或越界,返回-1
}
- 并检查消行 (ls_updata 函数):将方块添加到链表中,并检查是否有完整的行需要消除。
int ls_updata(struct ls_all *head, struct block *bk) {
// ... 省略中间代码 ...
if (ls_check_self(head) == -1) {
return -1; // 如果检测到游戏结束,返回-1
}
}
- 检查并处理消行 (ls_check_self 函数):遍历链表,检查是否有满行,如果有,则进行消行处理。
int ls_check_self(struct ls_all *head) {
// ... 省略中间代码 ...
if (n == 16) { // 如果一行中有16个方块,即填满一行
// 执行消行操作
}
return 0; // 正常退出,游戏继续
}
- 显示链表中的所有方块 (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 游戏代码中的多线程
多线程被用于实现游戏的不同功能,如自动下落、触摸事件处理和时间更新等。多线程允许这些功能并发运行,从而提高程序的响应性和性能。以下是对游戏代码中多线程使用的详细解释:
- 线程创建 : 在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);
- 自动下落线程 (auto_down) : 这个线程负责方块的自动下落逻辑。它在一个无限循环中运行,根据speed变量控制下落的速度:
void *auto_down(void *arg) {
// 线程内部逻辑
// ...
}
- 时间更新线程 (time_out) : 这个线程负责处理游戏的时间逻辑,例如,当没有用户交互时自动改变方块的下落方向:
void *time_out(void *arg) {
// 线程内部逻辑
// ...
}
- 触摸事件线程 : 在其他函数中,如show_interface_welcome或touch_event_thread,也可能创建额外的线程来处理触摸事件:
void *touch_event_thread(void *args) {
// 处理触摸事件的线程逻辑
// ...
}
- 线程同步 : 使用互斥锁(pthread_mutex_t)来同步对共享资源的访问,如方向变量dir:
pthread_mutex_lock(&dir_mutex);
dir = 0; // 设置下落方向
pthread_mutex_unlock(&dir_mutex);
- 线程取消 : 在某些情况下,如游戏结束或重启,可能需要取消线程:
pthread_cancel(thread_id);
- 线程等待 : 在主函数中,可能需要等待所有子线程完成,以确保资源被正确释放:
pthread_join(idt, NULL);
pthread_join(idr, NULL);
-
线程安全的操作 : 在多线程环境中,对共享资源的所有操作都应该是线程安全的。例如,更新分数或处理游戏状态的变量时,需要使用互斥锁来避免竞态条件。
-
条件变量 : 有时线程间需要基于某些条件进行同步,这时可以使用条件变量
pthread_cond_signal(&count_cond); // 发送信号给其他线程
pthread_cond_wait(&count_cond, &count_mutex); // 等待信号
总结:
本文详细介绍了一款基于ARM开发板GEC6818的俄罗斯方块游戏的设计和实现。从总体设计思路出发,我们采用了模块化编程方法,将游戏分解为图形显示、触摸事件处理、游戏控制、界面显示、链表管理、移动逻辑和主控等多个模块,以提高代码的可维护性和扩展性。通过C语言编程和多线程技术的应用,游戏实现了方块的移动、变形、随机生成、触屏控制、暂停恢复、嵌套消行和计分等功能,确保了游戏的流畅性和稳定性。
在界面设计上,游戏提供了直观的图形界面和触摸反馈,玩家可以轻松跟踪游戏进度和控制游戏流程。程序流程图清晰地展示了游戏从初始化到运行再到结束的整个过程,使读者能够快速把握游戏的逻辑结构。各模块的详细说明和代码实现,不仅展示了开发团队的技术实力,也为嵌入式系统开发爱好者提供了宝贵的学习资料。
此外,文章还对游戏代码中的链表和多线程技术进行了深入分析,展示了如何使用链表管理方块布局和实现动态内存管理,以及如何利用多线程提高程序的响应速度和性能。这些技术的应用,不仅提升了游戏的运行效率,也为复杂系统的开发提供了可行的解决方案。
总之,这款俄罗斯方块游戏的开发过程,不仅锻炼了嵌入式系统开发能力,也展示了模块化设计和多线程技术在实际应用中的强大功能。通过本文的阅读,读者不仅能够获得游戏开发的全面认识,还能从中学习到嵌入式系统编程的实用技巧和最佳实践。