前言
本篇博客我们来实现一个小游戏项目——贪吃蛇,相信肯定很多人都玩过,那么整个贪吃蛇是怎么实现出来的那,这个项目用到了很多方面的知识:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。我们就通过这篇博客一步一步去分析,实现贪吃蛇。
💓 个人主页:小张同学zkf
⏩ 文章专栏:数据结构 C语言
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章
目录
1.游戏背景
2.Win32 API介绍
2.1Win32 API
2.2控制台程序
2.3控制台屏幕上的坐标COORD
2.4GetStdHandle
2.5GetConsoleCursorInfo
2.6CONSOLE_CURSOR_INFO
2.7SetConsoleCursorInfo
2.8SetConsoleCursorPosition
2.9GetAsyncKeyState
3.贪吃蛇游戏思路
3.1游戏窗口
3.1.1本地化
3.1.2类项
3.1.3setlocale函数
3.1.4宽字符的打印
3.1.5地图坐标
3.2蛇身和食物
3.3数据结构设计
3.4游戏流程设计
4.核心逻辑分析
4.1游戏主逻辑
4.2 游戏开始(GameStart)
4.2.1打印欢迎界面
4.2.2创建地图
4.2.3初始化蛇身
4.2.4创建第一个食物
4.3游戏运行(GameRun)
4.3.1KEY_PRESS
4.3.2PrintHelpInfo
4.3.3蛇身移动(SnakeMove)
4.3.3.1NextIsFood
4.3.3.2 EatFood
4.3.3.3NoFood
4.3.3.4KillByWall
4.3.3.5KillBySelf
4.4游戏结束
5.参考代码
1.游戏背景
贪吃蛇是一款休闲益智类游戏,有PC和手机等多平台版本。既简单又耐玩。该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
2.Win32 API介绍
2.1Win32 API
2.2控制台程序
mode con cols= 100 lines= 30
通过这个命令可以把屏幕控制在100列的长度,30行宽度
参考:mode命令
title 贪吃蛇
参考:title命令
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
# include <stdio.h>int main (){// 设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩, 30 ⾏, 100 列system( "mode con cols=100 lines=30" );// 设置 cmd 窗⼝名称system( "title 贪吃蛇 " );return 0 ;}
2.3控制台屏幕上的坐标COORD
COORD类型的声明:
typedef struct _ COORD {SHORT X;SHORT Y;} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10 , 15 };
2.4GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个 句柄 (用来标识不同设备的数值),使用这个句柄可以操作设备。
参考:GetStdHandle
HANDLE GetStdHandle (DWORD nStdHandle);
实例:
HANDLE hOutput = NULL ;// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2.5GetConsoleCursorInfo
这个也是API函数,作用是检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo (HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息
参考:GetConsoleCursorInfo
实例
HANDLE hOutput = NULL ;// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )hOutput = GetStdHandle (STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息
2.6CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _ CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
CursorInfo.bVisible = false ; // 隐藏控制台光标
2.7SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo (HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE);// 影藏光标操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息CursorInfo.bVisible = false ; // 隐藏控制台光标SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态
参考:SetConsoleCursorInfo
2.8SetConsoleCursorPosition
BOOL WINAPI SetConsoleCursorPosition (HANDLE hConsoleOutput,COORD pos);
参考:SetConsoleCursorPosition
实例:
COORD pos = { 10 , 5 };HANDLE hOutput = NULL ;// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )hOutput = GetStdHandle (STD_OUTPUT_HANDLE);// 设置标准输出上光标的位置为 posSetConsoleCursorPosition (hOutput, pos);
SetPos: 封装一个设置光标位置的函数
/ 设置光标的坐标void SetPos ( short x, short y){COORD pos = { x, y };HANDLE hOutput = NULL ;// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )hOutput = GetStdHandle (STD_OUTPUT_HANDLE);// 设置标准输出上光标的位置为 posSetConsoleCursorPosition (hOutput, pos);}
2.9GetAsyncKeyState
SHORT GetAsyncKeyState (int vKey);
参考:GetAsyncKeyState
# define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
我们可以定一个宏专门判断最低位是否是1
参考:虚拟键码(Winuser.h) - Win32 apps
# include <stdio.h># include <windows.h>int main (){while ( 1 ){if (KEY_PRESS( 0x30 )){printf ( "0\n" );}else if (KEY_PRESS( 0x31 )){printf ( "1\n" );}else if (KEY_PRESS( 0x32 )){printf ( "2\n" );}else if (KEY_PRESS( 0x33 )){printf ( "3\n" );}else if (KEY_PRESS( 0x34 )){printf ( "4\n" );}else if (KEY_PRESS( 0x35 )){printf ( "5\n" );}else if (KEY_PRESS( 0x36 )){printf ( "6\n" );}else if (KEY_PRESS( 0x37 )){printf ( "7\n" );}else if (KEY_PRESS( 0x38 )){printf ( "8\n" );}else if (KEY_PRESS( 0x39 )){printf ( "9\n" );}}return 0 ;}
3.贪吃蛇游戏思路
3.1游戏窗口
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (), 在俄语编码中又会代表另一个符号。但是不管怎样,所有这 些编码方式中,0--127表⽰的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号, 肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。
3.1.1<locale.h>本地化
• 数字量的格式• 货币量的格式• 字符集• 日 期和时间的表示形式
3.1.2类项
• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。• LC_CTYPE:影响字符处理函数的行为。• LC_MONETARY:影响货币格式。• LC_NUMERIC:影响 printf() 的数字格式。• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境
详细介绍:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170
3.1.3setlocale函数
char * setlocale ( int category, const char * locale);
setlocale (LC_ALL, "C" );
setlocale (LC_ALL, " " ); // 切换到本地环境
3.1.4宽字符的打印
# include <stdio.h># include <locale.h>int main () {setlocale (LC_ALL, "" );wchar_t ch1 = L' ● ' ;wchar_t ch2 = L' ★ ' ;printf ( "%c%c\n" , 'a' , 'b' );wprintf ( L"%lc\n" , ch1);wprintf ( L"%lc\n" , ch2);return 0 ;}
这样一些好看的图案就可以在屏幕上打印了,宽字符占两个字节
3.1.5地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
这样我们根据坐标就可以将墙给表示出来了
3.2蛇身和食物
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐!!!
3.3数据结构设计
typedef struct SnakeNode{int x;int y;struct SnakeNode * next;}SnakeNode, * pSnakeNode;
typedef struct Snake{pSnakeNode _pSnake; // 维护整条蛇的指针pSnakeNode _pFood; // 维护⻝物的指针enum DIRECTION _Dir; // 蛇头的⽅向 , 默认是向右enum GAME_STATUS _Status; // 游戏状态int _Socre; // 游戏当前获得分数int _foodWeight; // 默认每个⻝物 10 分int _SleepTime; // 每⾛⼀步休眠时间}Snake, * pSnake;
蛇的方向,分为上,下,左,右可以列举,使用枚举
// ⽅向enum DIRECTION{UP = 1 ,DOWN,LEFT,RIGHT};
游戏状态,分为正常运行,撞墙,咬到自己,正常结束可以列举,使用枚举
// 游戏状态enum GAME_STATUS{OK, // 正常运⾏KILL_BY_WALL, // 撞墙KILL_BY_SELF, // 咬到⾃⼰END_NOMAL // 正常结束};
3.4游戏流程设计
4.核心逻辑分析
4.1游戏主逻辑
• 游戏开始(GameStart)完成游戏的初始化• 游戏运行(GameRun)完成游戏运行逻辑的实现• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
# include <locale.h>void test (){int ch = 0 ;srand (( unsigned int ) time ( NULL ));do{Snake snake = { 0 };GameStart (&snake);GameRun (&snake);GameEnd (&snake);SetPos ( 20 , 15 );printf ( " 再来⼀局吗? (Y/N):" );ch = getchar ();getchar (); // 清 理 \n} while (ch == 'Y' );SetPos ( 0 , 27 );}int main (){// 修改当前地区为本地模式,为了⽀持中⽂宽字符的打印setlocale (LC_ALL, "" );// 测试逻辑test ();return 0 ;}
注意:getchar()是获取字符信息,若此刻你没有输出任何字符,相当于运行上的一个暂停。
4.2 游戏开始(GameStart)
void GameStart (pSnake ps){// 设置控制台窗⼝的⼤⼩, 30 ⾏, 100 列//mode 为 DOS 命令system ( "mode con cols=100 lines=30" );// 设置 cmd 窗⼝名称system ( "title 贪吃蛇 " );// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE);// 影藏光标操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息CursorInfo.bVisible = false ; // 隐藏控制台光标SetConsoleCursorInfo (hOutput, &CursorInfo); // 设置控制台光标状态// 打印欢迎界⾯WelcomeToGame ();// 打印地图CreateMap ();// 初始化蛇InitSnake (ps);// 创造第⼀个⻝物CreateFood (ps);}
4.2.1打印欢迎界面
在游戏正式开始之前,做一些功能提醒
void WelcomeToGame (){SetPos ( 40 , 15 );printf ( " 欢迎来到贪吃蛇⼩游戏 " );SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点system ( "pause" );system ( "cls" );SetPos ( 25 , 12 );printf ( " ⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3 为加速, F4 为减速 \n" );SetPos ( 25 , 13 );printf ( " 加速将能得到更⾼的分数。 \n" );SetPos ( 40 , 25 ); // 让按任意键继续的出现的位置好看点system ( "pause" );system ( "cls" );}
system("pause")这个就是按任意键继续的功能与system("cls")(清空屏幕的功能)一连用就达到了切换页面的效果。
4.2.2创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L
# define WALL L' □ '
易错点:就是坐标的计算
上:(0,0)到(56,0)下:(0,26)到(56,26)左:(0,1)到(0,25)右:(56,1)到(56,25)
创建地图函数CreateMap
void CreateMap (){int i = 0 ;// 上 (0,0)-(56, 0)SetPos ( 0 , 0 );for (i = 0 ; i < 58 ; i += 2 ){wprintf ( L"%c" , WALL);}// 下 (0,26)-(56, 26)SetPos ( 0 , 26 );for (i = 0 ; i < 58 ; i += 2 ){wprintf ( L"%c" , WALL);}// 左//x 是 0 , y 从 1 开始增⻓for (i = 1 ; i < 26 ; i++){SetPos ( 0 , i);wprintf ( L"%c" , WALL);}//x 是 56 , y 从 1 开始增⻓for (i = 1 ; i < 26 ; i++){SetPos ( 56 , i);wprintf ( L"%c" , WALL);}}
4.2.3初始化蛇身
• 蛇的初始位置从 (24,5) 开始。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。• 游戏状态是:OK• 蛇的移动速度:200毫秒• 蛇的默认方向:RIGHT• 初始成绩:0• 每个食物的分数:10
# define BODY L' ●
void InitSnake (pSnake ps){pSnakeNode cur = NULL ;int i = 0 ;// 创建蛇⾝节点,并初始化坐标// 头插法for (i = 0 ; i < 5 ; i++){// 创建蛇⾝的节点cur = (pSnakeNode) malloc ( sizeof (SnakeNode));if (cur == NULL ){perror ( "InitSnake()::malloc()" );return ;}// 设置坐标cur->next = NULL ;cur->x = POS_X + i * 2 ;cur->y = POS_Y;// 头插法if (ps->_pSnake == NULL ){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}// 打印蛇的⾝体cur = ps->_pSnake;while (cur){SetPos (cur->x, cur->y);wprintf ( L"%lc" , BODY);cur = cur->next;}// 初始化贪吃蛇数据ps->_SleepTime = 200 ;ps->_Socre = 0 ;ps->_Status = OK;ps->_Dir = RIGHT;ps->_foodWeight = 10 ;}
4.2.4创建第一个食物
# define FOOD L' ★ '
void CreateFood (pSnake ps){int x = 0 ;int y = 0 ;again:// 产⽣的 x 坐标应该是 2 的倍数,这样才可能和蛇头坐标对⻬。do{x = rand () % 53 + 2 ;y = rand () % 25 + 1 ;} while (x % 2 != 0 );pSnakeNode cur = ps->_pSnake; // 获取指向蛇头的指针// ⻝物不能和蛇⾝冲突while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}pSnakeNode pFood = (pSnakeNode) malloc ( sizeof (SnakeNode)); // 创建⻝物if (pFood == NULL ){perror ( "CreateFood::malloc()" );return ;}else{pFood->x = x;pFood->y = y;SetPos (pFood->x, pFood->y);wprintf ( L"%c" , FOOD);ps->_pFood = pFood;}}
生成食物的函数,用rand随机生成但要注意不要越过墙的坐标范围,而且不能随机生成到蛇身上,也就是说随机坐标要有这俩判断条件,我们这个代码,中间生成的随机值与蛇身重合,就可以用goto语句来重新来一遍循环,将食物节点下一个next值置为空,别忘了,将食物节点,储存到蛇的结构体中
4.3游戏运行(GameRun)
• 上:VK_UP• 下:VK_DOWN• 左:VK_LEFT• 右:VK_RIGHT• 空格:VK_SPACE• ESC:VK_ESCAPE• F3:VK_F3• F4:VK_F4
确定了蛇的方向和速度,蛇就可以移动了。
void GameRun (pSnake ps){// 打印右侧帮助信息PrintHelpInfo ();do{SetPos ( 64 , 10 );printf ( " 得分: %d " , ps->_Socre);printf ( " 每个⻝物得分: %d 分 " , ps->_foodWeight);if ( KEY_PRESS (VK_UP) && ps->_Dir != DOWN){ps->_Dir = UP;}else if ( KEY_PRESS (VK_DOWN) && ps->_Dir != UP){ps->_Dir = DOWN;}else if ( KEY_PRESS (VK_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if ( KEY_PRESS (VK_RIGHT) && ps->_Dir != LEFT){ps->_Dir = RIGHT;}else if ( KEY_PRESS (VK_SPACE)){pause ();}else if ( KEY_PRESS (VK_ESCAPE)){ps->_Status = END_NOMAL;break ;}else if ( KEY_PRESS (VK_F3)){if (ps->_SleepTime >= 80 ){ps->_SleepTime -= 30 ;ps->_foodWeight += 2 ; // ⼀个⻝物分数最⾼是 20 分}}else if ( KEY_PRESS (VK_F4)){if (ps->_SleepTime < 320 ){ps->_SleepTime += 30 ;ps->_foodWeight -= 2 ; // ⼀个⻝物分数最低是 2 分}}// 蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快Sleep (ps->_SleepTime);SnakeMove (ps);} while (ps->_Status == OK);}
这个函数我们根据虚拟键位值返回的值判断方向,但我们在玩贪吃蛇时,假如蛇方向在上,你不能按下的键,与它方向相反的键你按了不管用,改变不了蛇的状态,在加速减速中,我们通过控制睡眠时间长短,来控制蛇的速度,此外我们还需要确定蛇移动函数,注意:这些信息一定是在游戏正常运行时才能出现的
4.3.1KEY_PRESS
检测按键状态,我们封装了⼀个宏
# define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
4.3.2PrintHelpInfo
我们可以在游戏运行时,右边放些提示信息,来方便我们玩游戏
void PrintHelpInfo (){// 打印提⽰信息SetPos ( 64 , 15 );printf ( " 不能穿墙,不能咬到⾃⼰ \n" );SetPos ( 64 , 16 );printf ( " ⽤ ↑ . ↓ . ← . → 分别控制蛇的移动 ." );SetPos ( 64 , 17 );printf ( "F3 为加速, F4 为减速 \n" );SetPos ( 64 , 18 );printf ( "ESC :退出游戏 .space :暂停游戏 ." );}
4.3.3蛇身移动(SnakeMove)
上面游戏运行过程中我们不是需要定义一个蛇移动的函数嘛
void SnakeMove (pSnake ps){// 创建下⼀个节点pSnakeNode pNextNode = (pSnakeNode) malloc ( sizeof (SnakeNode));if (pNextNode == NULL ){perror ( "SnakeMove()::malloc()" );return ;}// 确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定switch (ps->_Dir){case UP:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1 ;}break ;case DOWN:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1 ;}break ;case LEFT:{pNextNode->x = ps->_pSnake->x - 2 ;pNextNode->y = ps->_pSnake->y;}break ;case RIGHT:{pNextNode->x = ps->_pSnake->x + 2 ;pNextNode->y = ps->_pSnake->y;}break ;}// 如果下⼀个位置就是⻝物if ( NextIsFood (pNextNode, ps)){EatFood (pNextNode, ps);}else // 如果没有⻝物{NoFood (pNextNode, ps);}KillByWall (ps);KillBySelf (ps);}
4.3.3.1NextIsFood
假如下一个坐标是食物,我们需要返回1或0,1就代表是食物,0就代表不是食物,方便If判断
//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针int NextIsFood (pSnakeNode psn, pSnake ps){return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);}
4.3.3.2 EatFood
这个就是吃掉事物的函数,假如下一个坐标是食物,我们吃掉它,我们蛇身需要增长,我们想一下,我们可以直接用食物的节点头插到我们蛇身,成为我们新蛇头节点,这样就可以让蛇身增长了
吃完之后,得需要再打印一遍蛇,吃掉的话,总分就会加食物分,我们不要忘记
//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针void EatFood (pSnakeNode psn, pSnake ps){// 头插法psn->next = ps->_pSnake;ps->_pSnake = psn;// 打印蛇pSnakeNode cur = ps->_pSnake;while (cur){SetPos (cur->x, cur->y);wprintf ( L"%c" , BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;// 释放⻝物节点free (ps->_pFood);// 创建新的⻝物CreateFood (ps);}
4.3.3.3NoFood
//pSnakeNode psn 是下⼀个节点的地址//pSnake ps 维护蛇的指针void NoFood (pSnakeNode psn, pSnake ps){// 头插法psn->next = ps->_pSnake;ps->_pSnake = psn;// 打印蛇pSnakeNode cur = ps->_pSnake;{SetPos (cur->x, cur->y);wprintf ( L"%c" , BODY);cur = cur->next;}// 最后⼀个位置打印空格,然后释放节点SetPos (cur->next->x, cur->next->y);printf ( " " );free (cur->next);cur->next = NULL ;}
4.3.3.4KillByWall
判断蛇头的坐标是否和墙的坐标冲突函数
这个函数目的就在于若蛇头坐标与墙坐标重叠就将游戏状态变为因撞墙而结束
//pSnake ps 维护蛇的指针int KillByWall (pSnake ps){if ((ps->_pSnake->x == 0 )|| (ps->_pSnake->x == 56 )|| (ps->_pSnake->y == 0 )|| (ps->_pSnake->y == 26 )){ps->_Status = KILL_BY_WALL;return 1 ;}return 0 ;}
4.3.3.5KillBySelf
判断蛇头的坐标是否和蛇身体的坐标冲突,若冲突,就将游戏状态变为因撞到蛇身而结束
/pSnake ps 维护蛇的指针int KillBySelf (pSnake ps){pSnakeNode cur = ps->_pSnake->next;while (cur){if ((ps->_pSnake->x == cur->x)&& (ps->_pSnake->y == cur->y))ps->_Status = KILL_BY_SELF;return 1 ;}cur = cur->next;}return 0 ;}
4.4游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
void GameEnd (pSnake ps){pSnakeNode cur = ps->_pSnake;SetPos ( 24 , 12 );switch (ps->_Status){case END_NOMAL:printf ( " 您主动退出游戏 \n" );break ;case KILL_BY_SELF:printf ( " 您撞上⾃⼰了 , 游戏结束 !\n" );break ;case KILL_BY_WALL:printf ( " 您撞墙了 , 游戏结束 !\n" );break ;}// 释放蛇⾝的节点while (cur){pSnakeNode del = cur;cur = cur->next;free (del);}}
5.参考代码
test.cpp
#include "Snake.h"#include <locale.h>void test(){int ch = 0;srand((unsigned int)time(NULL));do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("再来⼀局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y' || ch == 'y');SetPos(0, 27);}
int main()
{//修改当前地区为本地模式,为了⽀持中⽂宽字符的打印setlocale(LC_ALL, "");//测试逻辑test();return 0;
}
#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//⽅向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};
//游戏状态
enum GAME_STATUS
{OK,//正常运⾏KILL_BY_WALL,//撞墙KILL_BY_SELF,//咬到⾃⼰END_NOMAL//正常结束
};
#define WALL L'□'
#define BODY L'●' //★○●◇◆□■
#define FOOD L'★' //★○●◇◆□■
//蛇的初始位置
#define POS_X 24
#define POS_Y 5
//蛇⾝节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
typedef struct Snake
{pSnakeNode _pSnake;//维护整条蛇的指针pSnakeNode _pFood;//维护⻝物的指针enum DIRECTION _Dir;//蛇头的⽅向默认是向右enum GAME_STATUS _Status;//游戏状态int _Socre;//当前获得分数int _foodWeight;//默认每个⻝物10分int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;
//游戏开始前的初始化
void GameStart(pSnake ps);
//游戏运⾏过程
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
//设置光标的坐标
void SetPos(short x, short y);
//欢迎界⾯
void WelcomeToGame();
//打印帮助信息
void PrintHelpInfo();
//创建地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建⻝物
void CreateFood(pSnake ps);
//暂停响应
void pause();
//下⼀个节点是⻝物
int NextIsFood(pSnakeNode psn, pSnake ps);
//吃⻝物
void EatFood(pSnakeNode psn, pSnake ps);
//不吃⻝物
void NoFood(pSnakeNode psn, pSnake ps);
//撞墙检测
int KillByWall(pSnake ps);
//撞⾃⾝检测
int KillBySelf(pSnake ps);
//蛇的移动
void SnakeMove(pSnake ps);
//游戏初始化
void GameStart(pSnake ps);
//游戏运⾏
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
#include "Snake.h"
//设置光标的坐标
void SetPos(short x, short y)
{COORD pos = { x, y };HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}
void WelcomeToGame()
{SetPos(40, 15);printf("欢迎来到贪吃蛇⼩游戏");SetPos(40, 25);//让按任意键继续的出现的位置好看点system("pause");system("cls");SetPos(25, 12);printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(25, 13);printf("加速将能得到更⾼的分数。\n");SetPos(40, 25);//让按任意键继续的出现的位置好看点system("pause");system("cls");
}
void CreateMap()
{int i = 0;//上(0,0)-(56, 0)SetPos(0, 0);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//左//x是0,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}//x是56,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(56, i);
wprintf(L"%c", WALL);}}void InitSnake(pSnake ps){pSnakeNode cur = NULL;int i = 0;//创建蛇⾝节点,并初始化坐标//头插法for (i = 0; i < 5; i++){//创建蛇⾝的节点cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}//设置坐标cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//头插法if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇的⾝体cur = ps->_pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
//初始化贪吃蛇数据ps->_SleepTime = 200;ps->_Socre = 0;ps->_Status = OK;ps->_Dir = RIGHT;ps->_foodWeight = 10;}void CreateFood(pSnake ps){int x = 0;int y = 0;again://产⽣的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬。do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针//⻝物不能和蛇⾝冲突while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建⻝物if (pFood == NULL){perror("CreateFood::malloc()");return;}else{pFood->x = x;pFood->y = y;SetPos(pFood->x, pFood->y);wprintf(L"%c", FOOD);ps->_pFood = pFood;
}
}
void PrintHelpInfo()
{//打印提⽰信息SetPos(64, 15);printf("不能穿墙,不能咬到⾃⼰\n");SetPos(64, 16);printf("⽤↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3 为加速,F4 为减速\n");SetPos(64, 18);printf("ESC :退出游戏.space:暂停游戏.");
}
void pause()//暂停
{while (1){Sleep(300);if (KEY_PRESS(VK_SPACE)){break;}}
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode psn, pSnake ps)
{return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur)
{SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;free(ps->_pFood);CreateFood(ps);
}
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{//头插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}//最后⼀个位置打印空格,然后释放节点SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next);cur->next = NULL;
}
//pSnake ps 维护蛇的指针
int KillByWall(pSnake ps)
{if ((ps->_pSnake->x == 0)|| (ps->_pSnake->x == 56)|| (ps->_pSnake->y == 0)|| (ps->_pSnake->y == 26)){ps->_Status = KILL_BY_WALL;return 1;}return 0;
}
//pSnake ps 维护蛇的指针
int KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){if ((ps->_pSnake->x == cur->x)&& (ps->_pSnake->y == cur->y)){ps->_Status = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;
}
void SnakeMove(pSnake ps)
{//创建下⼀个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}//确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定switch (ps->_Dir){case UP:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;}break;case DOWN:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;}break;case LEFT:{pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;
}break;case RIGHT:{pNextNode->x = ps->_pSnake->x + 2;pNextNode->y = ps->_pSnake->y;}break;}//如果下⼀个位置就是⻝物if (NextIsFood(pNextNode, ps)){EatFood(pNextNode, ps);}else//如果没有⻝物{NoFood(pNextNode, ps);}KillByWall(ps);KillBySelf(ps);
}
void GameStart(pSnake ps)
{//设置控制台窗⼝的⼤⼩,30⾏,100列//mode 为DOS命令system("mode con cols=100 lines=30");//设置cmd窗⼝名称system("title 贪吃蛇"); //获取标准输出的句柄(⽤来标识不同设备的数值)HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//影藏光标操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态//打印欢迎界⾯WelcomeToGame();//打印地图CreateMap();
//初始化蛇InitSnake(ps);//创造第⼀个⻝物CreateFood(ps);
}
void GameRun(pSnake ps)
{//打印右侧帮助信息PrintHelpInfo();do{SetPos(64, 10);printf("得分:%d ", ps->_Socre);printf("每个⻝物得分:%d分", ps->_foodWeight);if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN){ps->_Dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP){ps->_Dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT){ps->_Dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT){ps->_Dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)){pause();}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status = END_NOMAL;break;}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 50){ps->_SleepTime -= 30;ps->_foodWeight += 2;
}}else if (KEY_PRESS(VK_F4)){if (ps->_SleepTime < 350){ps->_SleepTime += 30;ps->_foodWeight -= 2;if (ps->_SleepTime == 350){ps->_foodWeight = 1;}}}//蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);
}
void GameEnd(pSnake ps)
{pSnakeNode cur = ps->_pSnake;SetPos(24, 12);switch (ps->_Status){case END_NOMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("您撞上⾃⼰了 ,游戏结束!\n");break;case KILL_BY_WALL:printf("您撞墙了,游戏结束!\n");break;}//释放蛇⾝的节点while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}
结束语
贪吃蛇博客就总结完了,有什么问题,欢迎各位大佬评论,总的来说,结合了C语言知识,数据结构知识,API函数方面有关函数等,只要思路顺清楚还是比较简单的。
OK,感谢观看!!!