流畅高移植性的STM32贪吃蛇游戏
目录
一、一些前言:
自己用2.4寸TFTSPI的屏写了个贪吃蛇,蛇行走画面流畅,不会有一顿一顿的感觉,且增加了便捷移植性,如果几个外部调用函数一样的话,就只用改改头文件就可以直接用了。
在写的时候写了两种实现方法,一种是直接用二维数组实现,一种是用了双向循环链表实现,在程序中都有保留下来,以便学习。
PS: 二维数组实现的都注释掉了,想用的可以把注释去掉,然后把双向循环链表的代码注释掉就可以了,但是二维数组的方法是肯定不好的,当蛇身到一定长度程序就会变得很卡。所以还是用双向循环链表
其实应该不需要双向链表,单向链表就可以了应该,还省空间,不过也算是学习双向链表嘛
为了方便看,该程序中我也像学习的资料代码一样加了很多详细的注释,还且贪吃蛇其实挺简单的,功能一点一点造轮子就可以了,所以就不多说了,看注释应该都可以明白。也不分开一点一点发和说了,直接上完整代码,要用直接CV工程师就可以了☺
对了,该程序实现的蛇的移动是相对会流畅很多的,不会有一顿一顿的移动(除非延时调的有问题)
某站的演示视频:STM32贪吃蛇(顺滑流畅,高移植性)+街机版2048_哔哩哔哩_bilibili
二、完整代码
2.1、snake.c
/***********************************************************************************************************************************
** 【文件名称】 snake.c
***********************************************************************************************************************************
** 【文件功能】 贪吃蛇游戏
**
** 【适用平台】 STM32F103 + 标准库v3.5 + keil5
**
** 【本地函数】
**
** 【更新记录】 2022-60-22 完善代码结构、完善注释格式
**
************************************************************************************************************************************/
#include "snake.h"
_border_limit border_limit;
_food_Info food_Info;
_snake_Info snake_Info; //蛇的身体信息
_snake_Info snake_Info_next; //蛇下一坐标信息(存放蛇下一步要走的位置)
// ( BORDER_LIMIT_DOWN - BORDER_LIMIT_UP - 1 ) / SNAKE_LENGTH_PIXELS *
// ( BORDER_LIMIT_RIGHT - BORDER_LIMIT_LEFT - 1 ) / SNAKE_LENGTH_PIXELS; 蛇身数组大小为这两个的乘积
//int snake_body [2080][2] = {0}; //蛇全身坐标
u8 keyFlag; //按键
u8 startGame; //开始游戏标志位(0不可以开始 1可以开始)
u8 speed = 50;
struct List* snake_list; // 创建双向循环链表
//链表结构体,节点
struct Node
{
u16 x;
u16 y;
struct Node* left;
struct Node* right;
};
//创建节点
struct Node* createNode( u16 x, u16 y )
{
struct Node* newNode = (struct Node*)malloc( sizeof(struct Node) );
newNode->x = x;
newNode->y = y;
newNode->left = newNode->right = NULL;
return newNode;
};
//链表
//用指针来存储链表哪一个节点是第一、最后一个节点
struct List
{
int size;
struct Node* firstNode; //第一个节点
struct Node* lastNode; //最后一个节点
};
//创造双向链表
static struct List* createList()
{
//申请内存
struct List* list = (struct List*)malloc( sizeof(struct List) );
//初始化变量
list->size = 0;
list->firstNode = list->lastNode = NULL;
return list;
}
//求大小,判断是否为NULL
static u8 listSize( struct List* list )
{
return list->size;
}
static u8 emptyList( struct List* list )
{
if( list->size == 0 )
return 0;
else
return 1;
}
//检查链表中数据是否有与传入的数据相同,0 没有相同的数据,1 有相同的数据
static u8 listCheck( struct List* list, u16 x, u16 y )
{
if( list->size == 0 )
Snake_printf( "无法比较,链表为空\n" );
else
{
struct Node* pMove = list->firstNode; //创建移动链表指针
list->lastNode->right = NULL; //断开双向循环链表
while( pMove )
{
if( pMove->x == x && pMove->y == y )
{
list->lastNode->right = list->firstNode;
return 1; //重新建立双向循环链表,并返回1
}
else
pMove = pMove->right;
}
list->lastNode->right = list->firstNode; //重新建立双向循环链表,并返回0
return 0;
}
return NULL;
}
//打印链表
static void printfList( struct List* list )
{
if( list->size == 0 )
Snake_printf( "无法打印,链表为空\n" );
else
{
struct Node* pMove = list->firstNode;
list->lastNode->right = NULL; //断开双向循环链表
while( pMove )
{
Snake_printf( "%d, %d \n", pMove->x, pMove->y );
pMove = pMove->right;
}
Snake_printf( "打印完成\n\n" );
list->lastNode->right = list->firstNode; //重新建立双向循环链表
}
}
//销毁双向链表
static void ListDestory( struct List* list )
{
if( list->size == 0)
Snake_printf( "无法销毁,链表为空\n" );
else
{
struct Node* pMove = list->firstNode;
list->lastNode->right = NULL; //断开双向循环链表
while( pMove )
{
Snake_printf( "%d, %d 销毁\n", pMove->x, pMove->y );
free( pMove );
pMove = pMove->right;
}
Snake_printf( "销毁完成,链表已为空\n\n" );
}
}
//表头插入
static void InsertNodeByHead( struct List* list, u16 x, u16 y )
{
struct Node* newNode = createNode( x, y );
if( list->firstNode == NULL ) //如果链表为空,则头尾节点都是新节点
{
list->lastNode = newNode;
}
else //否则则头节点的左边是新节点,新节点右边是头节点
{
list->firstNode->left = newNode;
newNode->right = list->firstNode;
}
list->firstNode = newNode; //设新节点为头节点
list->firstNode->left = list->lastNode; //建立表头表尾循环
list->lastNode->right = list->firstNode;
list->size++;
}
//表尾插入
static void InsertNodeByTail( struct List* list, u16 x, u16 y )
{
struct Node* newNode = createNode( x, y );
if( list->lastNode == NULL ) //如果链表为空,则头尾节点都是新节点
{
list->firstNode = newNode;
}
else //否则则尾节点的右边是新节点,新节点左边是尾节点
{
list->lastNode->right = newNode;
newNode->left = list->lastNode;
}
list->lastNode = newNode; //设新节点为尾节点
list->firstNode->left = list->lastNode; //建立表头表尾循环
list->lastNode->right = list->firstNode;
list->size++;
}
//双向链表移动,原表尾移动为新表头,并重新赋值
static void ListTailMoveToHead( struct List* list, u16 x, u16 y )
{
list->firstNode = list->lastNode; //把表头赋值为现表尾
list->lastNode = list->lastNode->left; //把表尾重新赋值为现表尾的左边那个节点
list->firstNode->x = x; //对新表头重新赋值
list->firstNode->y = y;
}
// 本地MS粗略延时函数,减少移植时对外部文件依赖;
static void Delay_ms( u32 ms )
{
ms=ms*6500;
for(u32 i=0; i<ms; i++); // 72MHz系统时钟下,多少个空循环约耗时1ms
}
/*****************************************************************
* 函 数: Snake_Draw_Erase_OneJoint
* 功 能: 蛇的一个关节/食物绘画,一个关节/食物擦除
* 参 数: u16 sX, sY 起点坐标
* u16 color 绘画,或者擦除成的颜色
*
* 返回值: 无
*
* 备 注:
******************************************************************/
static void Snake_Draw_Erase_OneJoint( u16 sX, u16 sY, u16 color )
{
u16 sX2, sY2;
sX2 = sX + SNAKE_LENGTH_PIXELS-1;
sY2 = sY + SNAKE_LENGTH_PIXELS-1;
Snake_LCD_Fill( sX, sY, sX2, sY2, color );
}
/*****************************************************************
* 函 数: Snake_Draw_Erase
* 功 能: 绘画,擦除蛇一个关节的一格像素
* 参 数: u16 sX, sY 起点坐标
* u16 color 绘画,或者擦除成的颜色
*
* 返回值: 无
*
* 备 注: 为了蛇移动画面显示流畅,所以一个关节画一个像素头边擦除一个尾边
******************************************************************/
static void Snake_Draw_Erase( u16 sX, u16 sY, u8 dir, u16 color )
{
u16 sX2 = sX, sY2 = sY;
if( dir == 3 || dir == 4 )
sY2 = sY + SNAKE_LENGTH_PIXELS-1;
else
sX2 = sX + SNAKE_LENGTH_PIXELS-1;
Snake_LCD_Fill( sX, sY, sX2, sY2, color );
}
/*****************************************************************
* 函 数: Snake_Update_HeadXY
* 功 能: 更新蛇头显示,显示蛇头
* 参 数: u8 i 显示蛇头关节的第几个像素块
*
* 返回值: 无
*
* 备 注:
******************************************************************/
static void Snake_Update_HeadXY( u8 i )
{
if( snake_Info.sHeadDir == 4 ) //蛇头向左
Snake_Draw_Erase( snake_Info_next.sHeadx + i, snake_Info_next.sHeady, snake_Info.sHeadDir, SNAKE_COLOR ); //显示蛇头
else if( snake_Info.sHeadDir == 3 ) //蛇头向右
Snake_Draw_Erase( snake_Info_next.sHeadx + SNAKE_MOVE_DISTANCE - i -1, snake_Info_next.sHeady, snake_Info.sHeadDir, SNAKE_COLOR );
else if( snake_Info.sHeadDir == 2 ) //蛇头向下
Snake_Draw_Erase( snake_Info_next.sHeadx, snake_Info_next.sHeady + i, snake_Info.sHeadDir, SNAKE_COLOR );
else if( snake_Info.sHeadDir == 1 ) //蛇头向上
Snake_Draw_Erase( snake_Info_next.sHeadx, snake_Info_next.sHeady + SNAKE_MOVE_DISTANCE - i -1, snake_Info.sHeadDir, SNAKE_COLOR );
}
/*****************************************************************
* 函 数: Snake_Update_EndXY
* 功 能: 更新蛇尾显示,擦除蛇尾
* 参 数: u8 i 擦除蛇尾关节的第几个像素块
*
* 返回值: 无
*
* 备 注:
******************************************************************/
static void Snake_Update_EndXY( u8 i ) //更新蛇尾显示,擦除蛇尾
{
// if( snake_body[snake_Info.sLength-2][1] == snake_Info.sEndy ) // 蛇尾如果和上一节蛇身在同一y轴,则是往左右走
// {
// if( snake_body[snake_Info.sLength-2][0] - snake_Info.sEndx > 0 ) // 如果蛇尾的上一节蛇身x坐标减去蛇尾x坐标大于0,则是左走
// Snake_Draw_Erase( snake_Info.sEndx + i, snake_Info.sEndy, 4, Snake_Erase_COLOR ); //擦除蛇尾
// else // 反之右走
// Snake_Draw_Erase( snake_Info.sEndx + SNAKE_MOVE_DISTANCE - i -1, snake_Info.sEndy, 3, Snake_Erase_COLOR ); //擦除蛇尾
// }
// else // 蛇尾如果和上一节蛇身在同一x轴,则是往左右走
// {
// if( snake_body[snake_Info.sLength-2][1] - snake_Info.sEndy > 0 ) // 如果蛇尾的上一节蛇身y坐标减去蛇尾y坐标大于0,则是左走下走
// Snake_Draw_Erase( snake_Info.sEndx , snake_Info.sEndy + i, 2, Snake_Erase_COLOR ); //擦除蛇尾
// else // 反之上走
// Snake_Draw_Erase( snake_Info.sEndx, snake_Info.sEndy + SNAKE_MOVE_DISTANCE - i -1, 1, Snake_Erase_COLOR ); //擦除蛇尾
// }
if( snake_list->lastNode->y == snake_list->lastNode->left->y ) // 蛇尾如果和上一节蛇身在同一y轴,则是往左右走
{
if( snake_list->lastNode->left->x - snake_list->lastNode->x > 0 ) // 如果蛇尾的上一节蛇身x坐标减去蛇尾x坐标大于0,则是左走
Snake_Draw_Erase( snake_Info.sEndx + i, snake_Info.sEndy, 4, Snake_Erase_COLOR ); //擦除蛇尾
else // 反之右走
Snake_Draw_Erase( snake_Info.sEndx + SNAKE_MOVE_DISTANCE - i -1, snake_Info.sEndy, 3, Snake_Erase_COLOR ); //擦除蛇尾
}
else // 蛇尾如果和上一节蛇身在同一x轴,则是往左右走
{
if( snake_list->lastNode->left->y - snake_list->lastNode->y > 0 ) // 如果蛇尾的上一节蛇身y坐标减去蛇尾y坐标大于0,则是左走下走
Snake_Draw_Erase( snake_Info.sEndx , snake_Info.sEndy + i, 2, Snake_Erase_COLOR ); //擦除蛇尾
else // 反之上走
Snake_Draw_Erase( snake_Info.sEndx, snake_Info.sEndy + SNAKE_MOVE_DISTANCE - i -1, 1, Snake_Erase_COLOR ); //擦除蛇尾
}
}
/*****************************************************************
* 函 数: Snake_Update_BodyXY
* 功 能: 更新蛇身坐标
* 参 数:
*
* 返回值: 无
*
* 备 注:蛇每移动完一个关节,更新一次蛇身坐标
******************************************************************/
static void Snake_Update_BodyXY(void)
{ // 移动前 移动后 吃到食物增加关节
// 4 3 2 1 0 | 4 3 2 1 0 5 4 3 2 1 0
// int i; // 0.1 0.2 0.3 0.4 0.5 | 0.2 0.3 0.4 0.5 0.5 0.1 0.2 0.3 0.4 0.5 0.5
// for( i=snake_Info.sLength-1; i>0; i-- ) //从尾巴开始,每一个点的位置等于它前面一个点的位置,蛇头在数组[0][]
// {
// snake_body[i][0] = snake_body[i-1][0];
// snake_body[i][1] = snake_body[i-1][1];
// }
//更新蛇头坐标,把确认可以走的下一步蛇头坐标赋值给现蛇头坐标和蛇身数组
// snake_body[0][0] = snake_Info.sHeadx = snake_Info_next.sHeadx;
// snake_body[0][1] = snake_Info.sHeady = snake_Info_next.sHeady;
// snake_Info.sEndx = snake_body[snake_Info.sLength-1][0]; //更新现蛇尾坐标
// snake_Info.sEndy = snake_body[snake_Info.sLength-1][1];
if( snake_list->size < snake_Info.sLength ) //如果吃到食物,则只增加表头
InsertNodeByHead( snake_list, snake_Info_next.sHeadx, snake_Info_next.sHeady );
else
//更新蛇头蛇尾链表坐标,消除原蛇尾坐标,把链表表尾移到表头,并赋值为新蛇头坐标
ListTailMoveToHead( snake_list, snake_Info_next.sHeadx, snake_Info_next.sHeady );
snake_Info.sHeadx = snake_Info_next.sHeadx; //更新现蛇头坐标
snake_Info.sHeady = snake_Info_next.sHeady;
snake_Info.sEndx = snake_list->lastNode->x; //更新现蛇尾坐标
snake_Info.sEndy = snake_list->lastNode->y;
}
/*****************************************************************
* 函 数: Snake_Display_Init
* 功 能: 蛇的信息及显示初始化
* 参 数:
*
* 返回值: 无
*
* 备 注:如果改蛇初始化位置及方向状态则在这改
******************************************************************/
static void Snake_Display_Init(void)
{
Snake_printf( "*****! 正在初始化蛇体信息 !*****\n" );
snake_Info.sHeadDir = 4; //初始化蛇头朝向(1上 2下 3左 4右)
snake_Info.sEndDir = 4; //初始化蛇尾朝向(1上 2下 3左 4右)
snake_Info.sLife = 1; //初始化蛇生命
snake_Info.sFraction = 0; //得分初始化
food_Info.validity = 0; //食物初始化
snake_Info.sLength = SNAKE_INIT_LENGTH + 1 ; //蛇身长度
//蛇头蛇尾坐标初始化(一开始的位置在左上角,往右走)
snake_Info.sHeadx = border_limit.yLeftLimit + SNAKE_LENGTH_PIXELS*(1+SNAKE_INIT_LENGTH) + SNAKE_LENGTH_PIXELS + 1;
snake_Info.sHeady = border_limit.xUpLimit + SNAKE_LENGTH_PIXELS + 1;
snake_Info.sEndx = snake_Info.sHeadx;
snake_Info.sEndy = snake_Info.sHeady;
snake_Info_next.sHeadx = snake_Info.sHeadx;
snake_Info_next.sHeady = snake_Info.sHeady;
// snake_body[0][0] = snake_Info.sHeadx; // 蛇头坐标,x
// snake_body[0][1] = snake_Info.sHeady; // y坐标
snake_list = createList(); // 创建双向循环链表
InsertNodeByHead( snake_list, snake_Info.sHeadx, snake_Info.sHeady ); //表头插入蛇头位置
//增加蛇头(此关节是必需的)
Snake_Draw_Erase_OneJoint( snake_Info.sHeadx, snake_Info.sHeady, SNAKE_COLOR );
//增加 SNAKE_INIT_LENGTH 个关节
for( int count=1; count <= SNAKE_INIT_LENGTH; count++ )
{
snake_Info.sEndx -= SNAKE_LENGTH_PIXELS;
// snake_body[count][0] = snake_Info.sEndx;
// snake_body[count][1] = snake_Info.sEndy;
InsertNodeByTail( snake_list, snake_Info.sEndx, snake_Info.sEndy ); //表尾循环插入蛇身数据
Snake_Draw_Erase_OneJoint( snake_Info.sEndx, snake_Info.sEndy, SNAKE_COLOR ); //添加蛇身
}
//蛇身信息
Snake_printf( "** 蛇头坐标信息:x=%d, y=%d\n", snake_Info.sHeadx, snake_Info.sHeady );
Snake_printf( "** 蛇尾坐标信息:x=%d, y=%d\n", snake_Info.sEndx, snake_Info.sEndy );
Snake_printf( "** 蛇身长度:%d节\n", snake_Info.sLength );
Snake_printf( "** 蛇头朝向:%d\n", snake_Info.sHeadDir );
//分数初始化显示
Snake_LCD_ShowNum( border_limit.yRightLimit/2, border_limit.xUpLimit-30, snake_Info.sFraction, 24, WHITE, BLACK );
Snake_printf( "*****! 蛇体信息初始化完成 !*****\n\n" );
}
/*****************************************************************
* 函 数: Border_Display_Init
* 功 能: 游戏边界初始化及显示
* 参 数:
*
* 返回值: 无
*
* 备 注:
******************************************************************/
static void Border_Display_Init(void)
{
Snake_printf("*****! 正在初始化游戏界面 !*****\n");
border_limit.xUpLimit = BORDER_LIMIT_UP;
border_limit.xDownLimit = BORDER_LIMIT_DOWN;
border_limit.yLeftLimit = BORDER_LIMIT_LEFT;
border_limit.yRightLimit = BORDER_LIMIT_RIGHT;
//游戏界面检查(界面长宽距离必须要是蛇关节长度的倍数)
if( (BORDER_LIMIT_DOWN - BORDER_LIMIT_UP - 1) % SNAKE_LENGTH_PIXELS != 0 ||
(BORDER_LIMIT_RIGHT - BORDER_LIMIT_LEFT - 1) % SNAKE_LENGTH_PIXELS != 0 )
{
Snake_printf("xxxxx! 游戏界面初始化失败 !xxxxx\n");
while(1);
}
Snake_LCD_DrawRectangle ( border_limit.yLeftLimit,
border_limit.xUpLimit,
border_limit.yRightLimit,
border_limit.xDownLimit, BORDER_WIDTH, 0, BORDER_COLOR );
Snake_printf("*****! 游戏界面初始化完成 !*****\n\n");
}
/*****************************************************************
* 函 数: Snake_HTW
* 功 能: 判断蛇下一步是否撞墙
* 参 数:
*
* 返回值: 0死亡 1正常
*
* 备 注:
******************************************************************/
static u8 Snake_HTW(void)
{
u8 row, col;
if( snake_Info_next.sHeady <= border_limit.xUpLimit ||
snake_Info_next.sHeady >= border_limit.xDownLimit ||
snake_Info_next.sHeadx <= border_limit.yLeftLimit ||
snake_Info_next.sHeadx >= border_limit.yRightLimit )
{
Snake_printf("触碰边界,死亡!\n");
//显示撞墙蛇头
Snake_Draw_Erase_OneJoint( snake_Info.sHeadx, snake_Info.sHeady, SNAKE_DIE_COLOR );
col = ( BORDER_LIMIT_RIGHT - BORDER_LIMIT_LEFT - 1 - 9*12 )/2 + 9;
row = ( BORDER_LIMIT_DOWN - BORDER_LIMIT_UP - 1 )/2 + 16;
Snake_LCD_String( col, row, "Game Over", 32, WHITE, BLACK );
//更新蛇生命状态
snake_Info.sLife = 0;
}
return snake_Info.sLife;
}
/*****************************************************************
* 函 数: Snake_TouchMyself
* 功 能: 判断蛇是否吃到自己
* 参 数:
*
* 返回值: 0死亡 1正常
*
* 备 注:
******************************************************************/
static u8 Snake_TouchMyself(void)
{
u8 snakeSta = 1, row, col;
// for( int i=0; i<snake_Info.sLength; i++ ) //遍历蛇身数组,检查蛇头是否碰到蛇身
// {
// if( snake_Info_next.sHeadx == snake_body[i][0] && snake_Info_next.sHeady == snake_body[i][1] )
// {
// snakeSta = 0;
// break;
// }
// }
//遍历链表数据,检查蛇头是否碰到蛇身,因为返回值1为有重复值,所以1-运算,赋值为0
snakeSta = 1 - listCheck( snake_list, snake_Info_next.sHeadx, snake_Info_next.sHeady );
if(snakeSta == 0)
{
Snake_printf("吃到自己,死亡!\n");
//显示吃到自己的蛇头
Snake_Draw_Erase_OneJoint( snake_Info.sHeadx, snake_Info.sHeady, SNAKE_DIE_COLOR );
col = ( BORDER_LIMIT_RIGHT - BORDER_LIMIT_LEFT - 1 - 9*12 )/2 + 9;
row = ( BORDER_LIMIT_DOWN - BORDER_LIMIT_UP - 1 )/2 + 16;
Snake_LCD_String( col, row, "Game Over", 32, WHITE, BLACK );
//更新蛇生命状态
snake_Info.sLife = 0;
}
return snake_Info.sLife;
}
/*****************************************************************
* 函 数: Snake_Move
* 功 能: 蛇身移动
* 参 数:
*
* 返回值: 0死亡 1正常
*
* 备 注:
******************************************************************/
static u8 Snake_Move(void)
{
u8 upDateSnakeEnd = 0; //更新尾巴标志位(0更新,1不更新)
// for( int i=0; i<snake_Info.sLength; i++ ) //打印全部蛇身
// {
// Snake_printf( "%d: %d , %d\n\n", i, snake_body[i][0], snake_body[i][1] );
// }
//蛇当前信息
Snake_printf( "*****! 移动信息 !*****\n");
Snake_printf( "** 蛇头坐标信息:x = %d, y = %d\n", snake_Info.sHeadx, snake_Info.sHeady );
Snake_printf( "** 蛇尾坐标信息:x = %d, y = %d\n", snake_Info.sEndx, snake_Info.sEndy );
Snake_printf( "** 蛇身长度:%d节\n", snake_Info.sLength );
Snake_printf( "** 蛇头朝向:%d\n", snake_Info.sHeadDir );
Snake_printf( "** 当前食物信息:x = %d, y = %d\n", food_Info.xFood, food_Info.yFood );
Snake_printf( "**********************\n\n");
//刷新分数界面
Snake_LCD_ShowNum( border_limit.yRightLimit/2, border_limit.xUpLimit-30, snake_Info.sFraction, 24, WHITE, BLACK );
//判断蛇下一步坐标是否会撞墙
u8 SnakeSta = Snake_HTW();
if( SnakeSta == 0 )
return SnakeSta;
//判断先导坐标是否会吃到自己
SnakeSta = Snake_TouchMyself();
if( SnakeSta == 0 )
return SnakeSta;
//判断蛇下一步坐标是否与食物一样,代表吃到食物,则此次将不删除尾巴
if( snake_Info_next.sHeadx == food_Info.xFood && snake_Info_next.sHeady == food_Info.yFood )
upDateSnakeEnd = 1;
//更新蛇的显示,为了显示流畅,选择显示一格蛇头消除一格蛇尾,循环一节蛇身次
for( int i=0; i<SNAKE_MOVE_DISTANCE; i++ )
{
Snake_Update_HeadXY( i ); //更新蛇头
if( upDateSnakeEnd == 0 ) //如果吃到食物则不更新蛇尾
Snake_Update_EndXY( i );
Delay_ms( speed ); //延时
}
Snake_LCD_DrawPoint( snake_Info_next.sHeadx, snake_Info_next.sHeady, SNAKE_HEAD_COLOR ); //在蛇头位置画点
if(upDateSnakeEnd == 1)
{
snake_Info.sLength++; //更新蛇身长度
snake_Info.sFraction++; //加分数
food_Info.validity = 0; //更新食物状态
if( snake_Info.sFraction%2 == 0 && speed>0 ) //分数每多2分速度增加
speed--;
}
//更新蛇身坐标
Snake_Update_BodyXY();
return 1;
}
/*****************************************************************
* 函 数: Create_Food
* 功 能: 生成食物
* 参 数:
*
* 返回值: 无
*
* 备 注:
******************************************************************/
void Create_Food(void)
{
uint16_t temp1=0, rowTotal, colTotal, row, col;
if( food_Info.validity == 0 ) //如果食物已经被吃掉
{
//计算食物的行列范围
colTotal = ( BORDER_LIMIT_RIGHT - BORDER_LIMIT_LEFT - 1 ) / FOOD_LENGTH_PIXELS;
rowTotal = ( BORDER_LIMIT_DOWN - BORDER_LIMIT_UP - 1 ) / FOOD_LENGTH_PIXELS;
do
{
temp1 += Snake_FOOD; //读取定时器的值
//随机生成某行某列
srand( temp1 ); //随机种子
col = rand() % colTotal;
row = rand() % rowTotal;
//根据某行某列计算出食物的实际像素坐标
food_Info.xFood = (col * FOOD_LENGTH_PIXELS) + BORDER_LIMIT_LEFT + 1;
food_Info.yFood = (row * FOOD_LENGTH_PIXELS) + BORDER_LIMIT_UP + 1;
food_Info.validity = 1; //已生成食物
Snake_printf( "xfood = %d\n", food_Info.xFood );
Snake_printf( "yfood = %d\n", food_Info.yFood );
//检查随机数是否会超出边界
if( food_Info.xFood <= BORDER_LIMIT_LEFT ||
food_Info.xFood >= BORDER_LIMIT_RIGHT ||
food_Info.yFood <= BORDER_LIMIT_UP ||
food_Info.yFood >= BORDER_LIMIT_DOWN )
{
food_Info.validity = 0;
}
else
{
// for( int i=0; i<snake_Info.sLength; i++ ) //遍历蛇身数组,检查随机数是否与蛇体重合
// {
// if( snake_body[i][0] == food_Info.xFood && snake_body[i][1] == food_Info.yFood )
// {
// food_Info.validity = 0; //随机位置与蛇体重合,重新生成食物
// continue;
// }
// }
//遍历链表数据,检查随机数是否与蛇体重合,因为返回值1为有重复值,所以1-运算,赋值为0
food_Info.validity = 1 - listCheck( snake_list, food_Info.xFood, food_Info.yFood );
}
}while( food_Info.validity == 0 );
Snake_Draw_Erase_OneJoint( food_Info.xFood, food_Info.yFood, FOOD_COLOR ); //生成新的食物
// Snake_LCD_DrawPoint( food_Info.xFood, food_Info.yFood, GREEN );
}
}
/*****************************************************************
* 函 数: Snake_Game
* 功 能: 贪吃蛇游戏主运行函数
* 参 数:
*
* 返回值: 无
*
* 备 注:
******************************************************************/
void Snake_Game(void)
{
u8 snakeSta = 1;
startGame = 0;
Border_Display_Init(); //初始化游戏边界
Snake_Display_Init(); //初始化蛇体
while( startGame == 1 ); //按下key_up键游戏开始
keyFlag = snake_Info.sHeadDir;
while(1)
{
while( snakeSta != 0 )
{
switch(keyFlag)
{
case SNAKE_UP: //向上走
if( snake_Info.sHeadDir != 2 )
{
snake_Info_next.sHeady -= SNAKE_MOVE_DISTANCE;
snake_Info.sHeadDir = 1;
} else {
keyFlag = 2;
continue;
}
break;
case SNAKE_DOWN: //向下走
if( snake_Info.sHeadDir != 1 )
{
snake_Info_next.sHeady += SNAKE_MOVE_DISTANCE;
snake_Info.sHeadDir = 2;
} else {
keyFlag = 1;
continue;
}
break;
case SNAKE_LEFT: //向左走
if( snake_Info.sHeadDir != 4 )
{
snake_Info_next.sHeadx -= SNAKE_MOVE_DISTANCE;
snake_Info.sHeadDir = 3;
} else {
keyFlag = 4;
continue;
}
break;
case SNAKE_RIGHT: //向右走
if( snake_Info.sHeadDir != 3 )
{
snake_Info_next.sHeadx += SNAKE_MOVE_DISTANCE;
snake_Info.sHeadDir = 4;
} else {
keyFlag = 3;
continue;
}
break;
}
//蛇体移动
snakeSta = Snake_Move();
//检查是否需要更新食物
Create_Food();
//移动速度
// Delay_ms( speed );
}
if( snakeSta == 0 ) //游戏结束
{
ListDestory( snake_list ); //摧毁双向链表
startGame = 0;
break;
}
}
}
2.2、snake.c
然后就是snake.h,里面就是一些宏定义,移植时大部分修改都在头文件里修改就可以了
#ifndef __SNAKE_H
#define __SNAKE_H
#include "stm32f10x.h"
#include "stdlib.h"
#include "tft2_4.h"
#include "usart1_dma.h"
#include "general_time.h"
#pragma diag_suppress 177 //屏蔽变量、函数未使用警告
#pragma diag_suppress 550
#define SNAKE_LENGTH_PIXELS 5 //蛇的其中一节的长度像素(关节是正方形的)
#define SNAKE_INIT_LENGTH 30 //除蛇头外蛇身初始化的关节数量
#define SNAKE_MOVE_DISTANCE SNAKE_LENGTH_PIXELS //蛇每走一步的长度,必须为蛇一节长度的整数倍
#define SNAKE_COLOR RED //蛇体颜色
#define Snake_Erase_COLOR TFT2_4_Info.bColor //消除蛇身的颜色
#define SNAKE_HEAD_COLOR GREEN //蛇头坐标的颜色
#define SNAKE_DIE_COLOR YELLOW //蛇头触碰边界时的颜色
#define FOOD_COLOR BLUE //食物颜色
#define SNAKE_UP 1 //蛇头方向,依次为上下左右
#define SNAKE_DOWN 2
#define SNAKE_LEFT 3
#define SNAKE_RIGHT 4
//在此可以直接更改游戏区域大小,其余参数可以自适应(其长宽距离必须要是蛇关节长度的倍数)
// 比如:(240-19-20)=201,实际的像素宽度只有200,200是蛇关节像素长度的整数倍
// 比如:(320-19-40)=261,实际的像素宽度只有260,260是蛇关节像素长度的整数倍
#define BORDER_LIMIT_UP 40 //边界上限位
#define BORDER_LIMIT_DOWN TFT2_4_Info.height - 19 //边界下限位(屏幕长-)(320)
#define BORDER_LIMIT_LEFT 20 //边界左限位
#define BORDER_LIMIT_RIGHT TFT2_4_Info.width - 19 //边界右限位(屏幕宽-)(240)
#define BORDER_WIDTH 5 //边界宽度
#define BORDER_COLOR WHITE //边界颜色
#define FOOD_LENGTH_PIXELS SNAKE_LENGTH_PIXELS //食物的宽度
//调用的外部函数,移植基本只需在这改调用到的外部函数
#define Snake_printf USART1_printf //printf函数,蛇信息输出,不需要可以把相关代码注释掉
#define Snake_LCD_ShowNum TFT2_4_ShowNum //屏幕显示数字(分数)函数,可能需要修改函数的使用
#define Snake_LCD_String TFT2_4_String //屏幕显示字符串函数
#define Snake_LCD_DrawPoint TFT2_4_DrawPoint //屏幕显示点函数
#define Snake_LCD_Fill TFT2_4_Fill //屏幕指定区域填充颜色函数
#define Snake_LCD_DrawRectangle TFT2_4_DrawRectangle //屏幕画矩形函数,此函数是可以画边有厚度的矩形,如不同可能需要修改
#define Snake_FOOD GENERAL_TIM_GetCapturex(GENERAL_TIM) //食物坐标随机种子,默认种子为:读取定时器的值
//蛇的位置信息结构体
typedef struct
{
u8 sHeadDir; //蛇头朝向(1上 2下 3左 4右)
u8 sEndDir; //蛇尾朝向(1上 2下 3左 4右)【暂时没用到】
u8 sFraction; //得分数
u8 sLength; //蛇的关节数量
u8 sLife; //蛇的生命(0死 1活)
u16 sHeadx; //蛇头的坐标值
u16 sHeady; //蛇头的坐标值
u16 sEndx; //蛇尾的坐标值
u16 sEndy; //蛇尾的坐标值
}_snake_Info;
extern _snake_Info snake_Info;
extern _snake_Info snake_Info_leader;
//游戏边界结构体
typedef struct
{
u16 xUpLimit; //上限位
u16 xDownLimit; //下限位
u16 yLeftLimit; //左限位
u16 yRightLimit; //右限位
}_border_limit;
extern _border_limit border_limit;
//食物结构体
typedef struct
{
u16 xFood;
u16 yFood;
u16 validity; //食物坐标的合法性(0不合法, 1合法)
}_food_Info;
extern _food_Info food_Info;
//static void Delay_ms( u32 ms );
//static void Snake_Draw_Erase_OneJoint( u16 sX, u16 sY, u16 color );
//static void Snake_Draw_Erase( u16 sX, u16 sY, u8 dir, u16 color );
//static void Snake_Update_HeadXY( u8 i );
//static void Snake_Update_EndXY( u8 i );
//static void Snake_Update_BodyXY(void);
//static void Snake_Display_Init(void);
//static void Border_Display_Init(void);
//static u8 Snake_HTW(void);
//static u8 Snake_TouchMyself(void);
//static u8 Snake_Move(void);
void Create_Food(void);
void Snake_Game(void);
#endif /* __SNAKE_H */
2.3、中断处理
因为程序实现是在开发板上的,开发板上只有2个按键,所以中断处理用的是一个按键中断包含按下加按下一定时间两个来判断。这样就可以两个按键控制4个方向了,就是会卡一下。
原本想用遥控,红外接收来控制的,但是不知道为什么,进入游戏的时候红外就出错了,控制不了了,一直在莫名其妙的中断触发,也是不知道到底是什么原因,好像是因为SPI屏在要显示然后spi传数据的时候会有问题,按道理说红外用的是定时器的外部触发IO口是上下沿触发,中断不会有影响的才对啊,触发中断的那个IO口也没有重用。如果有人知道可能是什么原因,希望告知一下问题方向╮(╯▽╰)╭
PS:需要在中断处理的全局变量:
- keyFlag 控制蛇的前进方向(1上 2下 3左 4右)
- startGame 控制开始游戏(0不开始 1开始)
- speed 控制蛇的前进速度,默认为50,越小越快
- stop 游戏停止与开始,这个变量还没写,没办法,我写了也用不了啊, 红外遥控又出错╮(╯▽╰)╭
void EXTI0_IRQHandler(void)
{
if( EXTI_GetITStatus( EXTI_Line0 ) != RESET ) //检测是否产生了EXTI Line中断
{
u16 KEYnum = 0;
if( GPIO_ReadInputDataBit( GPIOA, GPIO_Pin_0 ) == KEY_ON )
{
while( GPIO_ReadInputDataBit( GPIOA, GPIO_Pin_0 ) == KEY_ON )
{
Delay_1ms(50);
KEYnum++;
if( KEYnum == 10 )
goto loop;
}
}
loop: if( KEYnum == 10 )
{
keyFlag = 1;
}
else
{
keyFlag = 2;
}
EXTI_ClearITPendingBit( EXTI_Line0 ); //清除中断标志位
}
}
void EXTI15_10_IRQHandler(void)
{
if( EXTI_GetITStatus( EXTI_Line13 ) != RESET ) //检测是否产生了EXTI Line中断
{
u16 KEYnum = 0;
if( GPIO_ReadInputDataBit( GPIOC, GPIO_Pin_13 ) == KEY_ON )
{
while( GPIO_ReadInputDataBit( GPIOC, GPIO_Pin_13 ) == KEY_ON )
{
Delay_1ms(50);
KEYnum++;
if( KEYnum == 10 )
goto loop;
}
}
loop: if( KEYnum == 10 )
{
keyFlag = 4;
}
else
{
keyFlag = 3;
}
EXTI_ClearITPendingBit( EXTI_Line13 ); //清除中断标志位
}
}
2.4、startup
在调式的时候发现在吃到30个食物后会卡死,然后发现是堆区小了,在创建第31个节点(蛇身)的时候溢出了,然后程序卡死了。这个问题只需要在startup启动文件中把堆区改大一点就可以了,就是startup_stm32f10x_xx.s这个文件,改为0x6380应该够蛇走满全部屏幕通关了,对了,说到这才想起通关还没写,不会真有人通关吧
;******************** (C) COPYRIGHT 2011 STMicroelectronics ********************
;* File Name : startup_stm32f10x_hd.s
;* Author : MCD Application Team
;* Version : V3.5.0
;* Date : 11-March-2011
;* Description : STM32F10x High Density Devices vector table for MDK-ARM
;* toolchain.
;* This module performs:
;* - Set the initial SP
;* - Set the initial PC == Reset_Handler
;* - Set the vector table entries with the exceptions ISR address
;* - Configure the clock system and also configure the external
;* SRAM mounted on STM3210E-EVAL board to be used as data
;* memory (optional, to be enabled by user)
;* - Branches to __main in the C library (which eventually
;* calls main()).
;* After Reset the CortexM3 processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
;* <<< Use Configuration Wizard in Context Menu >>>
;*******************************************************************************
; THE PRESENT FIRMWARE WHICH IS FOR GUIDANCE ONLY AIMS AT PROVIDING CUSTOMERS
; WITH CODING INFORMATION REGARDING THEIR PRODUCTS IN ORDER FOR THEM TO SAVE TIME.
; AS A RESULT, STMICROELECTRONICS SHALL NOT BE HELD LIABLE FOR ANY DIRECT,
; INDIRECT OR CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE
; CONTENT OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING
; INFORMATION CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS.
;*******************************************************************************
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Heap_Size EQU 0x00006380 //这里从默认的0x00000200改为0x00006380
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
三、后言
应该最主要有可能不同的就是外部调用的那几个函数,也是我自己写的,所以可能不一样,如果功能和函数调用时的传参是一样的,那就只用在头文件那里修改一下调用的函数名就可以了。然后上面就是贪吃蛇全部的代码内容了。
PS:还没实现的功能:
- 一定概率或者时间或者吃多少个食物会出现特殊食物,比如增加一条命、吃了增加多个关节、甚至可以说可能出现debuff但是外表是正常食物的颜色,吃了减少蛇身长度等。
- 竞速模式,比如多长时间没有吃到食物就算失败。
- 无限模式,撞墙不会死,这个简单,把画地图去掉,snake_move里的撞墙判定去掉就可以了。
- 困难程度选择,特殊地形等模式
- 历史最高分记录,这个也简单,每次结束与保存的最高分做对比就行了
由于时间问题,原本还想实现下这些功能的,但是现在就只实现了最基本的功能,如果有谁用了我这个代码出现了什么bug,程序有问题,也希望顺便来告诉我下,或者说实现了上面的那些功能也希望能告诉我分享下。
唯一可能需要调整的就是食物出现的坐标,不知道是屏幕显示不同颜色的像素点有位置偏差还是什么,打印出来的看食物位置坐标点好像是没有问题的,但有时候吃食物会感觉像素位置有点偏差,可能还是需要调试一下╮(╯▽╰)╭。如果有人修改好了,也麻烦告知一下看看,嘿嘿(*^▽^*)
PS:求一波点赞和收藏,和B站视频的一键三连~,谢谢Thanks♪(・ω・)ノ
2022.6.22