前言
本篇博客我们来实现一个小游戏项目——贪吃蛇,相信肯定很多人都玩过,那么整个贪吃蛇是怎么实现出来的那,这个项目用到了很多方面的知识:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。我们就通过这篇博客一步一步去分析,实现贪吃蛇。
💓 个人主页:小张同学zkf
⏩ 文章专栏:数据结构 C语言
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章
目录
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 结构的指针,该结构接收有关主机游标(光标)的信息
实例
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); // 设置控制台光标状态
2.8SetConsoleCursorPosition
BOOL WINAPI SetConsoleCursorPosition (HANDLE hConsoleOutput,COORD pos);
实例:
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);
# 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.参考代码
Snake.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
enum DIRECTION//舌头方向的枚举类型的定义
{
UP = 1,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
enum GAME_STATUS
{
OK,//游戏状态正常运行
KILL_BY_WALL,//撞到墙结束
KILL_BY_SELE,//撞到自身结束
END_NOMAL//正常运行结束
};
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 WelcomeToGame();//欢迎界面的函数声明
void CreateMap();//地图函数的声明
void InitSnake(pSnake ps);//蛇初始化函数
void CreateFood(ps);//创建食物
void GameRun(pSnake ps);//运行游戏
void Pause();//暂停函数
void SnakeMove(pSnake ps);//蛇每走一步
int NextFood(pSnakeNode pNextNode, pSnake ps);//检测下一个坐标是否是食物
void EatFood(pSnakeNode pn, pSnake ps);//吃掉食物
void NoFood(pSnakeNode pn, pSnake ps);//下一个坐标不是食物的函数
//检测是否撞到墙
void KillByWall(pSnake ps);
//检测是否撞到自己
void KillBySelf(pSnake ps);
void GameEnd(pSnake ps);//游戏结束主逻辑函数
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
void SetPos(short x, short y)//创建SetPos函数用来控制光标位置
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出设备的句柄
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);//将光标位置通过句柄设置到标准输出设备中
}
void color(int c) {
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c);
}
void WelcomeToGame()//欢迎界面的函数定义
{
SetPos(40, 14);
color(4);
wprintf(L"欢迎来到贪吃蛇小游戏");
SetPos(40, 20);//让按任意键继续出现的位置好看些
system("pause");//暂停,按任意键继续
system("cls");//清空屏幕
SetPos(25, 14);
color(6);
wprintf(L"用←.→.↑.↓分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(25, 15);
color(8);
wprintf(L"●代表蛇,★代表食物,□代表墙\n");
SetPos(25, 16);
color(9);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
color(11);
system("pause");
system("cls");
}
void CreateMap()//地图函数的定义
{
color(4);
int i = 0;
//最上面横一道墙
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//最下面横一道墙
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//最左面竖一道墙
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}
//最右面竖一道墙
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", L'□');
}
}
void InitSnake(pSnake ps)//蛇初始化函数的定义
{
int i = 0;
pSnakeNode cur=NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));//创建节点空间
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL;
cur->y = POS_Y;
cur->x = POS_X + 2 * i;
//头插法插入链表
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else//链表不为空时
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
cur = ps->_pSnake;//打印蛇的身体
while (cur)
{
color(13);
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
//设置游戏属性
ps->_foodWeight = 10;
ps->_Socre = 0;
ps->_SleepTime = 200;
ps->_Dir = RIGHT;
ps->_Status = OK;
}
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//x的值为2的倍数,范围2~54
//y的范围是1~25
again:
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;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
SetPos(x, y);
color(11);
wprintf(L"%lc", FOOD);//打印食物
ps->_pFood = pFood;
}
void GameStart(pSnake ps)//游戏开始初始化函数定义
{
/*控制台窗⼝⼤⼩的设置
• 控制台窗⼝名字的设置
• ⿏标光标的隐藏
• 打印欢迎界⾯
• 创建地图
• 初始化第蛇
• 创建第⼀个⻝物*/
system("mode con cols=100 lines=30");//控制台窗口的设置
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);
}
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)//键位虚拟值判断的宏
void PrintfHelp()//提示信息函数的定义
{
color(6);
SetPos(64, 14);
wprintf(L"%ls", L"1.不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls",L"2.用 ↑. ↓ . ← . → 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls",L"3.按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls",L"4.按ESC退出游戏,按空格暂停游戏");
SetPos(64, 18);
wprintf(L"%ls",L"5.小张同学制作");
}
void Pause()//暂停函数的定义
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextFood(pSnakeNode pn, pSnake ps)//检测下一个节点坐标的函数
{
return (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y);
}
void EatFood(pSnakeNode pn, pSnake ps)//吃掉食物的函数
{
//将食物节点作为新节点头插到蛇头
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//将下一个节点释放
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
//打印吃掉后的蛇
while (cur)
{
SetPos(cur->x, cur->y);
color(13);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//分数增加
ps->_Socre += ps->_foodWeight;
//重新创建食物
CreateFood(ps);
}
void NoFood(pSnakeNode pn, pSnake ps)
{
//将下一个坐标头插到蛇
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印移动后的蛇
pSnakeNode cur = ps->_pSnake;
while (cur->next->next!=NULL)
{
SetPos(cur->x, cur->y);
color(13);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个节点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个节点
free(cur->next);
//把倒数第二个节点next置为空
cur->next = NULL;
}
//检测是否撞到墙
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26 || ps->_pSnake->x == 56)
{
ps->_Status = KILL_BY_WALL;
}
}
//检测是否撞到自己
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_Status = KILL_BY_SELE;
break;
}
cur = cur->next;
}
}
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 (NextFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测是否撞到墙
KillByWall(ps);
//检测是否撞到自己
KillBySelf(ps);
}
//运行游戏主逻辑函数的定义
void GameRun(pSnake ps)
{
PrintfHelp();//打印帮助信息
color(9);
do
{
//打印总分数与食物分数的信息
SetPos(64, 10);
printf("总分数:%d\n", ps->_Socre);
SetPos(64, 11);
color(9);
printf("当前食物的分数:%2d\n", 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_F3))//F3加速
{
if(ps->_SleepTime > 60)
{
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))//F4减速
{
if (ps->_foodWeight > 2)
{
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_ESCAPE))//Esc键表示游戏结束
{
ps->_Status = END_NOMAL;
}
SnakeMove(ps);//蛇每走一步的函数
Sleep(ps->_SleepTime);//睡眠时间
} while (ps->_Status == OK);
}
void GameEnd(pSnake ps)//游戏结束主逻辑函数
{
SetPos(24, 12);
switch (ps->_Status)
{
case END_NOMAL:
color(8);
wprintf(L"你主动退出游戏\n");
break;
case KILL_BY_WALL:
color(8);
wprintf(L"你撞到墙上,游戏结束\n");
break;
case KILL_BY_SELE:
color(8);
wprintf(L"你撞到了自己,游戏结束\n");
break;
}
//释放蛇身链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
#include <locale.h>
void test()
{
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1.打印环境界面
//2.功能介绍
//3.绘制地图
//4.创建蛇
//5.创建食物
//设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
srand((unsigned int)time(NULL));//生成随机值
setlocale(LC_ALL, "");//修改当前地区为本地模式,为了方便打印宽字符
test();
return 0;
}
结束语
贪吃蛇博客就总结完了,有什么问题,欢迎各位大佬评论,总的来说,结合了C语言知识,数据结构知识,API函数方面有关函数等,只要思路顺清楚还是比较简单的。
OK,感谢观看!!!