当前位置:   article > 正文

【C语言】实践:贪吃蛇小游戏(附源码)_c语言贪吃蛇研究现状及分析

c语言贪吃蛇研究现状及分析

  欢迎光顾我的homepage

前言

        贪吃蛇小游戏想必大家都玩过吧,现在就要C语言代码来实现一下贪吃蛇小游戏

在实现之前,我们要对C语言结构体指针链表(单链表)有一定的基础

先来看一下预期运行效果

一、Win32 API

        这里实现贪吃蛇游戏会使用一些Win32 API的知识,这里简单学习一下

        Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、绘制图形、使用周边设备等目的,由于这些函数服务的对象是应用程序,所以便称之为Appliccation Programming Interface,简称API。WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。

1.1 控制台程序

        在我们的电脑中,windows系统使用快捷键win + R可以打开一个窗口,然后输入cmd就可以打开一个控制台程序,这个控制台可以输入一些命令来控制我们的电脑,这里输入cmd即可打开一个控制台程序窗口

        1.1.1 设置控制台程序

        本次贪吃蛇小游戏是在VS2022上来实现的,平常我们运行起来的黑框程序就是控制台层序

在VS2022上运行默认是以下情况

这里就需要先修改一个控制台

调出控制台(这里可以使用Win+R,输入cmd调出窗口),点击设置

在默认终端应用程序这里设置成Windows 控制台主机(默认是Windows 终端),点击保存

设置完成后,就是以下这种界面了

        1.1.2 设置控制台程序大小

这里我们控制台程序是默认大小,这里我们自己设置控制台程序大小,这里使用cmd控制台程序设置窗口的大小(设置大小为行33,列100)

mode con cols=100 lines=33

        1.1.3 设置控制台程序名称

我们设置控制台名称为 贪吃蛇,使用title 指令

title 贪吃蛇

        当然,这些能够在控制台窗口执行的命令,也可以通过调用C语言的system函数在中来完成

这里再补充一个指令,暂停控制台程序

system("pause");

        这个指令可以暂停程序运行,并会提示按下任意键继续...

  1. int main()
  2. {
  3. system("mode con cols=100 lines=33");
  4. system("title 贪吃蛇");
  5. system("pause");
  6. return 0;
  7. }

        1.1.4 控制台屏幕上的坐标

COORD是Windows API中自定义的一个结构体,表示一个字符在控制台屏幕缓冲区的坐标,坐标(0,0)的原点位于缓冲区的顶部左侧单元格。

COORD类型声明

  1. typedef struct _COORD {
  2. SHORT X;
  3. SHORT Y;
  4. } COORD, *PCOORD;

给坐标赋值

 COORD pos = { 10, 15 };

GetStdHandle

        GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),这个句柄可以操作设备。

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);

函数参数

函数使用

  1. HANDLE hOutput = NULL;
  2. //获取标准输出的句柄(用来标识不同设备的数值)
  3. hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorInfo

        GetConsoleCursorInfo函数检索有关指定控制台屏幕缓冲区的光标的大小和可见性的信息

函数语法

  1. BOOL WINAPI GetConsoleCursorInfo(
  2. _In_ HANDLE hConsoleOutput,
  3. _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
  4. );
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息

        CONSOLE_CURSOR_INFO结构体

        这个结构体包含了有关控制台光标的信息

  1. typedef struct _CONSOLE_CURSOR_INFO {
  2. DWORD dwSize;
  3. BOOL bVisible;
  4. } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

        dwSize 由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元格底部的水平线条

        bVisible 游标的可见性。如果光标可见,则此成员为true;如果不可见,此成员为false

函数参数

        这里就用到上面GetStdHandle函数获得的句柄了,还需要用到CONSOLE_CURSOR_INFO结构体(注意,这里第二个参数是指针)

函数使用

  1. HANDLE hOutput = NULL;
  2. //获取标准输出的句柄(⽤来标识不同设备的数值)
  3. hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  4. CONSOLE_CURSOR_INFO CursorInfo;
  5. GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

SetConsoleCursorInfo

        SetConsoleCursorInfo函数设置指定控制台屏幕缓冲区的光标的大小和可见性

函数参数

  1. BOOL WINAPI SetConsoleCursorInfo(
  2. _In_       HANDLE              hConsoleOutput,
  3. _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
  4. );

        与GetConsoleCursorInfo函数参数相同         

函数使用

  1. HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  2. //影藏光标操作
  3. CONSOLE_CURSOR_INFO CursorInfo;
  4. GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
  5. CursorInfo.bVisible = false; //隐藏控制台光标
  6. SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

SetConsoleCursorPosition

        SetConsoleCursorPosition函数设置指定控制台屏幕缓冲区的位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

  1. BOOL WINAPI SetConsoleCursorPosition(
  2. _In_ HANDLE hConsoleOutput,
  3. _In_ COORD  dwCursorPosition
  4. );

函数参数

函数使用

  1. HANDLE hOutput = NULL;
  2. //获取标准输出的句柄(用来标识不同设备的数值)
  3. hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  4. COORD pos = { 10, 5 };
  5. //设置标准输出上光标的位置为pos
  6. SetConsoleCursorPosition(hOutput, pos);

这里为了方便后面定位屏幕坐标,单独封装一个函数来实现

  1. void SetPos(short x, short y)
  2. {
  3. COORD pos = { x, y };
  4. HANDLE hOutput = NULL;
  5. //获取标准输出的句柄(用来标识不同设备的数值)
  6. hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  7. //设置标准输出上光标的位置为pos
  8. SetConsoleCursorPosition(hOutput, pos);
  9. }

GetAsyncKeyState

        GetAsyncKeyState函数获得按键情况

  1. SHORT GetAsyncKeyState(
  2. [in] int vKey
  3. );

函数参数

        这里函数参数是虚拟键码。

这里仅列出一些在游戏中可能用到的按键的虚拟键码,可以点击查看详细虚拟键码

VK_UP0x26
VK_DOWN0x28
VK_LEFT0x25
VK_RIGHT0x27
VK_F30x72F3
VK_F40x73F4
VK_ESCAPE0x1BEsc
VK_SPACE0x20空格

函数返回值

        GetAsyncKeyState 函数返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,如果最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

        在游戏中我们需要检测一个按键是否被按过,就检测 GetAsyncKeyState 函数返回值的最低值是否是1,可以写一个宏来实现:

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

二、<locale.h>本地化

在贪吃蛇游戏中,我们会涉及到墙体□ 和蛇的身体● 的打印,而在VS中我们输出出来的是?

这就是因为没有本地化设置,无法输出这些特殊字符(宽字符)。

        我们需要通过修改地区,让程序来适应不同的区域,我们就需要进行本地化设置

这里就要使用到C语言中的库函数  setlocale 函数

在C标准中,依赖地区的部分有以下几项

数字量的格式

货币量的格式

字符集

日期和时间的表示形式     

        通过修改地区,程序可以改变它的行为来适应世界的不同地域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类型进行修改,下面的一个宏就指定一个类型。 

LC_COLLATE :影响字符串表函数 strcoll strxfrm

LC_CTYPE : 影响字符处理函数的行为。

LC_MONETARY : 影响货币格式。

LC_NUMERIC : 影响 printf 的数字格式。

LC_TIME : 影响时间格式 strftimewcsftime  。

LC_ALL : 针对所有类型修改,将以上所有类别设置为给定的语言环境。

每一个类别都有详细说明,这里就不一一讲解了

        2.1 setlocale 函数

setlocale 函数用于修改当前地区,可以针对一个类项进行修改,也可以针对所有类项

函数的第一个参数可以是前面类项中的一个,也可以是LC_ALL(影响所以的类项)

函数的第二个参数

        C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地化模式)

这里我们需要进行本地化设置

 setlocale(LC_ALL, " ");//切换到本地环境

        2.2 宽字符的打印

在屏幕中,我们需要打印宽字符        

        宽字符的字面量必须加上前缀“L”,否则C语言就会把字符量当成窄字符来处理。前缀“L”子啊单引号(或者双引号)的前面,表示宽字符,对于 wprintf 的占位符是 %lc;双引号的前面,对于wprintf 的占位符是 %ls。

可以看到,这里宽字符占两个窄字符的位置。

三、游戏分析和设计

        3.1 贪吃蛇数据结构设计

        在游戏运行的过程中,蛇每吃一次食物,蛇的身体就会变长;这样我们就可以使用链表来存储蛇的信息,蛇的每一个节身体其实就是链表的一个节点。每个节点只需记录蛇身节点在地图上的坐标就可以。

        蛇身节点的结构:

  1. typedef struct Snakenode
  2. {
  3. int x;
  4. int y;
  5. struct Snakenode* next;
  6. }Snakenode, * pSnakenode;
  7. //这里也可以写 typedef Snakenode* pSnakenode

        接下来,我们还需要记录游戏过程中的相关信息

贪吃蛇,食物的位置,蛇的方向,游戏状态,当前的分数,每一个食物的分数,蛇的速度等

而这里蛇的方向和游戏状态都可以一一列举出来,这里就使用枚举变量

  1. //蛇的方向
  2. enum DIRECT
  3. {
  4. UP = 1,
  5. DOWN,
  6. LEFT,
  7. RIGHT
  8. };
  9. //蛇的状态——游戏状态
  10. //正常、撞墙、撞到自己、正常退出
  11. enum GAME_STATE
  12. {
  13. OK,
  14. KILL_WALL,
  15. KILL_SELF,
  16. NORMAL_END
  17. };
  18. //贪吃蛇的相关信息
  19. typedef struct Snake
  20. {
  21. pSnakenode psnake; //指向蛇头部的指针
  22. pSnakenode pfood; //指向食物的指针
  23. enum DIRECT dir;//蛇的方向
  24. enum GAME_STATE state;//蛇的状态
  25. int food_scores;//每个食物的分数
  26. int all_scores; //总分数
  27. int sleep_time; //休息的时间 --即蛇的速度
  28. }Snake;
  29. typedef Snake* pSnake;

这样,我们就创建了一个Snake结构体来维护游戏相关信息(维护整条贪吃蛇)

        3.2 游戏流程分析

游戏大概分析如下

四、游戏逻辑实现

        程序开始就设置程序本地化,然后就进入到游戏的主逻辑当中

根据游戏大概分析,游戏可以分为三个阶段

阶段一:游戏开始 --- 完成游戏的初始化 

阶段二:游戏运行 --- 完成游戏运行逻辑的实现

阶段三:游戏结束 --- 完成游戏结束的说明,实现资源释放

        当然,这里我们玩完一局游戏后,可以选择继续或者结束(这里就以输入Y/N来判断游戏是否继续运行)

这里我们在测试test.c文件开始就让程序本地化

  1. void test()
  2. {
  3. Snake snake = { 0 };
  4. int ch = 0;
  5. do
  6. {
  7. ch = 0;
  8. system("cls");
  9. //游戏初始化
  10. GameStart(&snake);
  11. //游戏运行
  12. GameRun(&snake);
  13. //游戏结束
  14. GameOver(&snake);
  15. KeyFun();
  16. SetPos(30, 20);
  17. wprintf(L"再来一局吗? (Y/N)");
  18. ch = getchar();
  19. while (getchar() != '\n');
  20. } while (ch == 'Y' || ch == 'y');
  21. SetPos(0, 27);
  22. }
  23. int main()
  24. {
  25. //本地化
  26. setlocale(LC_ALL, "");
  27. srand((unsigned int)time(NULL));
  28. test();
  29. //KeyFun();
  30. return 0;
  31. }

测试大概框架就是这样,接下来就分别来实现这些框架的内容

        4.1 游戏开始(GameStart)

1. 设置控制台大小和名字

这里设置控制台大小,100列,33行;设置控制台名称为:贪吃蛇

  1. //设置窗口名称大小
  2. system("title 贪吃蛇");
  3. system("mode con cols=100 lines=33");

2. 隐藏屏幕光标

隐藏屏幕光标,这里就用到了前面Win32 API的知识

  1. //隐藏光标
  2. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
  3. CONSOLE_CURSOR_INFO CursorInfo;
  4. GetConsoleCursorInfo(houtput, &CursorInfo);
  5. //获得有关指定控制台屏幕缓冲区的光标大小和可见的信息
  6. CursorInfo.bVisible = false; //隐藏控制台光标
  7. SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态

3. 打印欢迎界面

输出欢迎界面,这里分装成函数WelcomeToGame

        我们观察欢迎界面,可以发现这里并不是在坐标为(0,0)处打印的,这里就要用到设置光标位置(这里单独写一个函数,设置光标位置)

设置光标位置

  1. //设置光标位置
  2. void SetPos(int x, int y)
  3. {
  4. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
  5. COORD pos = { x,y };
  6. SetConsoleCursorPosition(houtput, pos);
  7. }

接下来就是游戏欢迎界面的打印,这里中间会用到 pause 和 cls(清理屏幕) 指令

  1. //欢迎界面打印
  2. void WelcomeToGame()
  3. {
  4. //设置光标位置
  5. SetPos(40, 15);
  6. printf("欢迎进入贪吃蛇小游戏\n");
  7. SetPos(42, 20);
  8. system("pause");
  9. system("cls");//清理屏幕
  10. SetPos(20, 11);
  11. printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇的移动,按F3加速、F4减速 ");
  12. SetPos(20, 13);
  13. printf("加速可以获得更多的分数");
  14. SetPos(20, 15);
  15. system("pause");
  16. system("cls"); //清理屏幕
  17. }

这样就可以实现预期效果图那样了,接下来就是绘制我们贪吃蛇游戏的地图了。

4. 绘制地图

这里我们使用宽字符来打印地图,先来看一下预期效果

我们把地图分为上、下、左、右这四个部分,这样我们只需依次打印这些宽字符就可以了

  1. //地图绘制
  2. void CreatMap()
  3. {
  4. //上
  5. int i = 0;
  6. for (i = 0; i < 29; i++)
  7. {
  8. wprintf(L"%lc", WALL);
  9. }
  10. //下
  11. SetPos(0, 26);
  12. for(i = 0; i < 29; i++)
  13. {
  14. wprintf(L"%lc", WALL);
  15. }
  16. //左
  17. for (i = 1; i < 26; i++)
  18. {
  19. SetPos(0, i);
  20. wprintf(L"%lc", WALL);
  21. }
  22. //右
  23. for (i = 1; i < 26; i++)
  24. {
  25. SetPos(56, i);
  26. wprintf(L"%lc", WALL);
  27. }
  28. }

5. 初始化贪吃蛇

初始化贪吃蛇,也是创建贪吃蛇,贪吃蛇身体这里其实就是一个链表,里面存放着每个节点的坐标

        初始化贪吃蛇也要给上一些初始数据

初始长度为  --  5

初始方向  --  向右(RIGHT)

初始状态  --  正常(OK)

每个食物得分  --  10

初始总分  --  0

初始速度  --  这里设定眠时间为200毫秒

初始蛇的位置  --  这里就随机生成(也可以指定)

当然初始指向食物的指针置为NULL(因为这里还未创建食物)

  1. //创建贪吃蛇
  2. void InitSnake(pSnake ps)
  3. {
  4. //创建蛇的身体
  5. pSnakenode pcur = NULL;
  6. int i = 0;
  7. int x, y;//蛇初始位置
  8. do
  9. {
  10. x = rand() % 31 + 4; //x: 4 - 34
  11. y = rand() % 20 + 2; //y: 1 - 25
  12. } while (x % 2 != 0);
  13. for (i = 0; i < 5; i++)
  14. {
  15. pcur = (pSnakenode)malloc(sizeof(Snakenode));
  16. if (pcur == NULL)
  17. {
  18. perror("InitSnake()::malloc()");
  19. return;
  20. }
  21. pcur->next = NULL;
  22. //pcur->x = SNAKE_X + i * 2;
  23. //pcur->y = SNAKE_Y;
  24. pcur->x = x + i * 2;
  25. pcur->y = y;
  26. //头插到贪吃蛇链表中
  27. if (ps->psnake == NULL) //链表为空
  28. {
  29. ps->psnake = pcur;
  30. }
  31. else
  32. {
  33. pcur->next = ps->psnake;
  34. ps->psnake = pcur;
  35. }
  36. }
  37. //输出蛇的初始位置
  38. pcur = ps->psnake;
  39. while (pcur)
  40. {
  41. SetPos(pcur->x, pcur->y);
  42. wprintf(L"%lc", SNAKENODE);
  43. pcur = pcur->next;
  44. }
  45. //初始化贪吃蛇的信息
  46. ps->dir = RIGHT; //蛇的方向
  47. ps->pfood = NULL; //指向食物 --NULL
  48. ps->state = OK; //状态
  49. ps->food_scores = 10; //每个食物的得分
  50. ps->all_scores = 0; //总分
  51. ps->sleep_time = 200;//速度,即休息时间 单位是毫秒
  52. //getchar();
  53. }

6. 创建食物

        创建完贪吃蛇,接下来就是创建食物了,其实食物和贪吃蛇身体节点一样,都存放着坐标;所以这里就创建一个结构体,再随机生成坐标
        这里需要注意:

        坐标x必须是偶数

        坐标必须在地图内

        生成食物的坐标不能与蛇的身体重复

  1. //创建食物
  2. void CreatFood(pSnake ps)
  3. {
  4. int x, y;//随机生成坐标 x , y
  5. //x 2-54
  6. //y 1-25
  7. again:
  8. do {
  9. x = rand() % 53 + 2;
  10. y = rand() % 25 + 1;
  11. } while (x % 2 != 0);
  12. //x y 不能与贪吃蛇身体重复
  13. pSnakenode pcur = ps->psnake;
  14. while (pcur)
  15. {
  16. if (x == pcur->x && y == pcur->y)
  17. {
  18. goto again;
  19. }
  20. pcur = pcur->next;
  21. }
  22. pSnakenode food = (pSnakenode)malloc(sizeof(pSnakenode));
  23. if (food == NULL)
  24. {
  25. perror("CreatFood()::malloc");
  26. return;
  27. }
  28. food->x = x;
  29. food->y = y;
  30. food->next = NULL;
  31. ps->pfood = food;
  32. SetPos(x, y);
  33. wprintf(L"%lc", FOOD);
  34. //getchar();
  35. }

这样我们的初始化就完成了

        4.2 游戏运行(GameRun)

1.输出右侧提示信息和分数详情

看预期效果图,我们在地图的右侧输出一些提示信息,并且输出当前得分详情

  1. void Printgame(pSnake ps) {
  2. SetPos(60, 15);
  3. printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇");
  4. SetPos(60, 16);
  5. printf("按F3加速、F4减速 ");
  6. SetPos(60, 18);
  7. printf("加速可以获得更高的分数 ");
  8. SetPos(60, 20);
  9. printf("ESC:退出游戏 space:暂停 ");
  10. SetPos(60, 10);
  11. printf("当前总得分:%d", ps->all_scores);
  12. SetPos(60, 12);
  13. printf("当前每个食物得分:%d", ps->food_scores);
  14. SetPos(60, 22);
  15. printf("努力学习的小廉");
  16. }

2. 获取按键情况

现在就要获取我们键盘按键的信息了,这里写一个宏来判断按键是否被按过

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

这里把获取按键信息直接写到游戏运行这个函数内,顺便看一下游戏运行都需要实现哪些东西?

  1. //游戏运行
  2. void GameRun(pSnake ps)
  3. {
  4. do
  5. {
  6. Printgame(ps);
  7. //判断按键是否被按过
  8. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
  9. {
  10. ps->dir = UP;
  11. }
  12. else if(KEY_PRESS(VK_DOWN) && ps->dir != UP){
  13. ps->dir = DOWN;
  14. }
  15. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
  16. ps->dir = LEFT;
  17. }
  18. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {
  19. ps->dir = RIGHT;
  20. }
  21. else if (KEY_PRESS(VK_SPACE)) //空格 -- 暂停
  22. {
  23. Pause();
  24. }
  25. else if (KEY_PRESS(VK_ESCAPE)) //游戏正常退出
  26. {
  27. ps->state = NORMAL_END;
  28. break;
  29. }
  30. else if (KEY_PRESS(VK_F3))
  31. {
  32. if (ps->sleep_time >= 100)
  33. {
  34. ps->sleep_time -= 50;
  35. ps->food_scores += 5;//设定食物分数最高25
  36. }
  37. }
  38. else if (KEY_PRESS(VK_F4))
  39. {
  40. if (ps->sleep_time < 300)
  41. {
  42. ps->sleep_time += 100;
  43. ps->food_scores -= 5;//⼀个⻝物分数最低是5分
  44. }
  45. }
  46. Sleep(ps->sleep_time);
  47. //贪吃蛇的移动
  48. SnakeMove(ps);
  49. //判断贪吃蛇是否撞墙
  50. KillByWall(ps);
  51. //判断贪吃蛇是否撞到自己
  52. KillBySelf(ps);
  53. } while (ps->state == OK);
  54. }

这里当游戏状态不是正常运行时,就结束了循环(即游戏结束)

3. 贪吃蛇移动

        看上述游戏运行代码,可以看到贪吃蛇的移动还有判断蛇是否撞到墙和自己,这些的实现在贪吃蛇移动当中。

        1> 蛇身的移动

        蛇身的移动,其实就是根据当前蛇的方向,找到下一个节点,再判断下一个节点是否是食物,和判断是否撞到墙和自己

        2> 判断是否吃到食物

        判断蛇的下一个节点是否是食物,就是判断下一个位置的坐标和实物的坐标是否重复

如果重复,就让蛇身变长一节,如果不是,就让蛇往前走

这里蛇移动还有一些知识,就是直接为蛇下一个位置创建一个新的节点

        再判断下一个位置是否是食物,如果是就将节点头插到蛇身链表中,不删除尾节点;如果不是就直接将节点头插到蛇身链表中,删除尾节点(这里还需在蛇的尾部输出两个空格"  ")

  1. //下一个位置是食物
  2. void IsFood(pSnakenode next, pSnake ps)
  3. {
  4. //把下一个位置的节点头插到贪吃蛇中
  5. next->next = ps->psnake;
  6. ps->psnake = next;
  7. //打印贪吃蛇
  8. pSnakenode cur = ps->psnake;
  9. while (cur)
  10. {
  11. SetPos(cur->x, cur->y);
  12. wprintf(L"%lc", SNAKENODE);
  13. cur = cur->next;
  14. }
  15. ps->all_scores += ps->food_scores;
  16. CreatFood(ps);
  17. //SetPos(ps->pfood->x, ps->pfood->y);
  18. //wprintf(L"%lc", FOOD);
  19. }
  20. //下一个位置不是食物
  21. void NoFood(pSnakenode next, pSnake ps)
  22. {
  23. //把下一个位置的节点头插到贪吃蛇中
  24. next->next = ps->psnake;
  25. ps->psnake = next;
  26. pSnakenode cur = ps->psnake;
  27. while (cur->next->next != NULL)
  28. {
  29. SetPos(cur->x, cur->y);
  30. wprintf(L"%lc", SNAKENODE);
  31. cur = cur->next;
  32. }
  33. SetPos(cur->next->x, cur->next->y);
  34. wprintf(L"%ls", L" ");
  35. free(cur->next);
  36. cur->next = NULL;
  37. }
  38. //贪吃蛇的移动
  39. void SnakeMove(pSnake ps)
  40. {
  41. pSnakenode next = (pSnakenode)malloc(sizeof(Snakenode));
  42. if (next == NULL)
  43. {
  44. perror("SnakeMove():malloc()");
  45. exit(1);
  46. }
  47. switch (ps->dir)
  48. {
  49. case UP:
  50. next->x = ps->psnake->x;
  51. next->y = ps->psnake->y - 1;
  52. break;
  53. case DOWN:
  54. next->x = ps->psnake->x;
  55. next->y = ps->psnake->y + 1;
  56. break;
  57. case LEFT:
  58. next->x = ps->psnake->x - 2;
  59. next->y = ps->psnake->y;
  60. break;
  61. case RIGHT:
  62. next->x = ps->psnake->x + 2;
  63. next->y = ps->psnake->y;
  64. break;
  65. }
  66. //判断下一个位置是不是食物
  67. if (NextIsFood(next, ps))
  68. {
  69. IsFood(next, ps);
  70. }
  71. else {
  72. NoFood(next, ps);
  73. }
  74. }
        3> 判断是否撞到墙和自己

判断蛇是否撞墙,就是判断蛇身节点的坐标是否超出地图的范围

  1. //判断贪吃蛇是否撞墙
  2. void KillByWall(pSnake ps)
  3. {
  4. if (ps->psnake->x == 0 || ps->psnake->x == 56
  5. || ps->psnake->y == 0 || ps->psnake->y == 26)
  6. ps->state = KILL_WALL;
  7. }

判断蛇是否撞到自己,就遍历链表,判断蛇身的头结点是否和身体其他节点重复

  1. //判断贪吃蛇是否撞到自己
  2. void KillBySelf(pSnake ps)
  3. {
  4. pSnakenode pcur = ps->psnake->next;
  5. while (pcur)
  6. {
  7. if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y)
  8. {
  9. ps->state = KILL_SELF;
  10. break;
  11. }
  12. pcur = pcur->next;
  13. }
  14. }

到这里,代码就已经实现的差不多了,接下来就是游戏结束后的一些善后工作

        4.3 游戏结束(GameOver)

1. 打印游戏结束的原因

        游戏结束,打印出来游戏结束的原因,是撞到墙了呢?还是撞到自己了呢?还是按Esc正常退出了呢?

2. 释放蛇身节点

        因为我们蛇身的节点是动态申请的内存,我们需要手动释放掉内存(养成好习惯!)

  1. //游戏结束
  2. void GameOver(pSnake ps)
  3. {
  4. SetPos(8, 12);
  5. switch(ps->state)
  6. {
  7. case KILL_WALL:
  8. wprintf(L"Sorry,game over because you hit the wall !\n");
  9. break;
  10. case KILL_SELF:
  11. wprintf(L"Sorry,game over because you hit youself !\n");
  12. break;
  13. case NORMAL_END:
  14. wprintf(L"Game exits normally !");
  15. break;
  16. }
  17. //释放贪吃蛇的节点内存
  18. pSnakenode pcur = ps->psnake;
  19. while (pcur)
  20. {
  21. pSnakenode del = pcur;
  22. pcur = pcur->next;
  23. free(del);
  24. }
  25. ps->psnake = NULL;
  26. }

到这里我们游戏的代码就已经全部实现了。

源代码

Snake.h

  1. #pragma once
  2. #define _CRT_SECURE_NO_WARNINGS
  3. #include<stdio.h>
  4. #include<stdlib.h>
  5. #include<locale.h>
  6. #include<windows.h>
  7. #include<stdbool.h>
  8. #include<wchar.h>
  9. #include<time.h>
  10. #include<conio.h>
  11. //初始数据
  12. #define SNAKE_X 24
  13. #define SNAKE_Y 5
  14. #define WALL L'□'
  15. #define SNAKENODE L'●'
  16. #define FOOD L'★'
  17. //蛇的节点
  18. typedef struct Snakenode
  19. {
  20. int x;
  21. int y;
  22. struct Snakenode* next;
  23. }Snakenode;
  24. typedef Snakenode* pSnakenode;
  25. //蛇的方向
  26. enum DIRECT
  27. {
  28. UP = 1,
  29. DOWN,
  30. LEFT,
  31. RIGHT
  32. };
  33. //蛇的状态——游戏状态
  34. //正常、撞墙、撞到自己、正常退出
  35. enum GAME_STATE
  36. {
  37. OK,
  38. KILL_WALL,
  39. KILL_SELF,
  40. NORMAL_END
  41. };
  42. //贪吃蛇的相关信息
  43. typedef struct Snake
  44. {
  45. pSnakenode psnake; //指向蛇头部的指针
  46. pSnakenode pfood; //指向食物的指针
  47. enum DIRECT dir;//蛇的方向
  48. enum GAME_STATE state;//蛇的状态
  49. int food_scores;//每个食物的分数
  50. int all_scores; //总分数
  51. int sleep_time; //休息的时间 --即蛇的速度
  52. }Snake;
  53. typedef Snake* pSnake;
  54. //设置光标位置
  55. void SetPos(int x, int y);
  56. //游戏初始化
  57. void GameStart(pSnake ps);
  58. //欢迎界面的输出
  59. void WelcomeToGame();
  60. //地图绘制
  61. void CreatMap();
  62. //创建贪吃蛇
  63. void InitSnake(pSnake ps);
  64. //创建食物
  65. void CreatFood(pSnake ps);
  66. //游戏运行
  67. void GameRun(pSnake ps);
  68. //贪吃蛇的移动
  69. void SnakeMove(pSnake ps);
  70. //判断下一个位置是不是食物
  71. int NextIsFood(pSnakenode next , pSnake ps);
  72. //下一个位置是食物,吃掉食物
  73. void IsFood(pSnakenode next, pSnake ps);
  74. //下一个位置不是食物
  75. void NoFood(pSnakenode next, pSnake ps);
  76. //判断贪吃蛇是否撞墙
  77. void KillByWall(pSnake ps);
  78. //判断贪吃蛇是否撞到自己
  79. void KillBySelf(pSnake ps);
  80. //游戏结束
  81. void GameOver(pSnake ps);
  82. void KeyFun();


Snake.c

  1. #include"Snake.h"
  2. //设置光标位置
  3. void SetPos(int x, int y)
  4. {
  5. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
  6. COORD pos = { x,y };
  7. SetConsoleCursorPosition(houtput, pos);
  8. }
  9. //欢迎界面打印
  10. void WelcomeToGame()
  11. {
  12. //设置光标位置
  13. SetPos(40, 15);
  14. printf("欢迎进入贪吃蛇小游戏\n");
  15. SetPos(42, 20);
  16. system("pause");
  17. system("cls");//清理屏幕
  18. SetPos(20, 11);
  19. printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇的移动,按F3加速、F4减速 ");
  20. SetPos(20, 13);
  21. printf("加速可以获得更多的分数");
  22. SetPos(20, 15);
  23. system("pause");
  24. system("cls"); //清理屏幕
  25. }
  26. //地图绘制
  27. void CreatMap()
  28. {
  29. //上
  30. int i = 0;
  31. for (i = 0; i < 29; i++)
  32. {
  33. wprintf(L"%lc", WALL);
  34. }
  35. //下
  36. SetPos(0, 26);
  37. for(i = 0; i < 29; i++)
  38. {
  39. wprintf(L"%lc", WALL);
  40. }
  41. //左
  42. for (i = 1; i < 26; i++)
  43. {
  44. SetPos(0, i);
  45. wprintf(L"%lc", WALL);
  46. }
  47. //右
  48. for (i = 1; i < 26; i++)
  49. {
  50. SetPos(56, i);
  51. wprintf(L"%lc", WALL);
  52. }
  53. //system("pause");
  54. }
  55. //创建贪吃蛇
  56. void InitSnake(pSnake ps)
  57. {
  58. //创建蛇的身体
  59. pSnakenode pcur = NULL;
  60. int i = 0;
  61. int x, y;//蛇初始位置
  62. do
  63. {
  64. x = rand() % 31 + 4; //x: 4 - 34
  65. y = rand() % 20 + 2; //y: 1 - 25
  66. } while (x % 2 != 0);
  67. for (i = 0; i < 5; i++)
  68. {
  69. pcur = (pSnakenode)malloc(sizeof(Snakenode));
  70. if (pcur == NULL)
  71. {
  72. perror("InitSnake()::malloc()");
  73. return;
  74. }
  75. pcur->next = NULL;
  76. //pcur->x = SNAKE_X + i * 2;
  77. //pcur->y = SNAKE_Y;
  78. pcur->x = x + i * 2;
  79. pcur->y = y;
  80. //头插到贪吃蛇链表中
  81. if (ps->psnake == NULL) //链表为空
  82. {
  83. ps->psnake = pcur;
  84. }
  85. else
  86. {
  87. pcur->next = ps->psnake;
  88. ps->psnake = pcur;
  89. }
  90. }
  91. //输出蛇的初始位置
  92. pcur = ps->psnake;
  93. while (pcur)
  94. {
  95. SetPos(pcur->x, pcur->y);
  96. wprintf(L"%lc", SNAKENODE);
  97. pcur = pcur->next;
  98. }
  99. //初始化贪吃蛇的信息
  100. ps->dir = RIGHT; //蛇的方向
  101. ps->pfood = NULL; //指向食物 --NULL
  102. ps->state = OK; //状态
  103. ps->food_scores = 10; //每个食物的得分
  104. ps->all_scores = 0; //总分
  105. ps->sleep_time = 200;//速度,即休息时间 单位是毫秒
  106. //getchar();
  107. }
  108. //创建食物
  109. void CreatFood(pSnake ps)
  110. {
  111. int x, y;//随机生成坐标 x , y
  112. //x 2-54
  113. //y 1-25
  114. again:
  115. do {
  116. x = rand() % 53 + 2;
  117. y = rand() % 25 + 1;
  118. } while (x % 2 != 0);
  119. //x y 不能与贪吃蛇身体重复
  120. pSnakenode pcur = ps->psnake;
  121. while (pcur)
  122. {
  123. if (x == pcur->x && y == pcur->y)
  124. {
  125. goto again;
  126. }
  127. pcur = pcur->next;
  128. }
  129. pSnakenode food = (pSnakenode)malloc(sizeof(pSnakenode));
  130. if (food == NULL)
  131. {
  132. perror("CreatFood()::malloc");
  133. return;
  134. }
  135. food->x = x;
  136. food->y = y;
  137. food->next = NULL;
  138. ps->pfood = food;
  139. SetPos(x, y);
  140. wprintf(L"%lc", FOOD);
  141. //getchar();
  142. }
  143. //游戏初始化
  144. void GameStart(pSnake ps)
  145. {
  146. //设置窗口名称大小,隐藏光标
  147. system("title 贪吃蛇");
  148. system("mode con cols=100 lines=33");
  149. //隐藏光标
  150. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
  151. CONSOLE_CURSOR_INFO CursorInfo;
  152. GetConsoleCursorInfo(houtput, &CursorInfo);//获得有关指定控制台屏幕缓冲区的光标大小和可见的信息
  153. CursorInfo.bVisible = false; //隐藏控制台光标
  154. SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
  155. //打印欢迎界面和功能介绍
  156. WelcomeToGame();
  157. //绘制地图
  158. CreatMap();
  159. //创建贪吃蛇
  160. InitSnake(ps);
  161. //创建食物
  162. CreatFood(ps);
  163. //system("pause");
  164. }
  165. #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
  166. //暂停
  167. void Pause()
  168. {
  169. while (1)
  170. {
  171. Sleep(200);
  172. if (KEY_PRESS(VK_SPACE))
  173. break;
  174. }
  175. }
  176. //判断下一个位置是不是食物
  177. int NextIsFood(pSnakenode next, pSnake ps)
  178. {
  179. return (next->x == ps->pfood->x && next->y == ps->pfood->y);
  180. }
  181. //下一个位置是食物
  182. void IsFood(pSnakenode next, pSnake ps)
  183. {
  184. //把下一个位置的节点头插到贪吃蛇中
  185. next->next = ps->psnake;
  186. ps->psnake = next;
  187. //打印贪吃蛇
  188. pSnakenode cur = ps->psnake;
  189. while (cur)
  190. {
  191. SetPos(cur->x, cur->y);
  192. wprintf(L"%lc", SNAKENODE);
  193. cur = cur->next;
  194. }
  195. ps->all_scores += ps->food_scores;
  196. CreatFood(ps);
  197. //SetPos(ps->pfood->x, ps->pfood->y);
  198. //wprintf(L"%lc", FOOD);
  199. }
  200. //下一个位置不是食物
  201. void NoFood(pSnakenode next, pSnake ps)
  202. {
  203. //把下一个位置的节点头插到贪吃蛇中
  204. next->next = ps->psnake;
  205. ps->psnake = next;
  206. pSnakenode cur = ps->psnake;
  207. while (cur->next->next != NULL)
  208. {
  209. SetPos(cur->x, cur->y);
  210. wprintf(L"%lc", SNAKENODE);
  211. cur = cur->next;
  212. }
  213. SetPos(cur->next->x, cur->next->y);
  214. wprintf(L"%ls", L" ");
  215. free(cur->next);
  216. cur->next = NULL;
  217. }
  218. //贪吃蛇的移动
  219. void SnakeMove(pSnake ps)
  220. {
  221. pSnakenode next = (pSnakenode)malloc(sizeof(Snakenode));
  222. if (next == NULL)
  223. {
  224. perror("SnakeMove():malloc()");
  225. exit(1);
  226. }
  227. switch (ps->dir)
  228. {
  229. case UP:
  230. next->x = ps->psnake->x;
  231. next->y = ps->psnake->y - 1;
  232. break;
  233. case DOWN:
  234. next->x = ps->psnake->x;
  235. next->y = ps->psnake->y + 1;
  236. break;
  237. case LEFT:
  238. next->x = ps->psnake->x - 2;
  239. next->y = ps->psnake->y;
  240. break;
  241. case RIGHT:
  242. next->x = ps->psnake->x + 2;
  243. next->y = ps->psnake->y;
  244. break;
  245. }
  246. //判断下一个位置是不是食物
  247. if (NextIsFood(next, ps))
  248. {
  249. IsFood(next, ps);
  250. }
  251. else {
  252. NoFood(next, ps);
  253. }
  254. }
  255. //
  256. void Printgame(pSnake ps) {
  257. SetPos(60, 15);
  258. printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇");
  259. SetPos(60, 16);
  260. printf("按F3加速、F4减速 ");
  261. SetPos(60, 18);
  262. printf("加速可以获得更高的分数 ");
  263. SetPos(60, 20);
  264. printf("ESC:退出游戏 space:暂停 ");
  265. SetPos(60, 10);
  266. printf("当前总得分:%d", ps->all_scores);
  267. SetPos(60, 12);
  268. printf("当前每个食物得分:%d", ps->food_scores);
  269. SetPos(60, 22);
  270. printf("努力学习的小廉");
  271. }
  272. //判断贪吃蛇是否撞墙
  273. void KillByWall(pSnake ps)
  274. {
  275. if (ps->psnake->x == 0 || ps->psnake->x == 56
  276. || ps->psnake->y == 0 || ps->psnake->y == 26)
  277. ps->state = KILL_WALL;
  278. }
  279. //判断贪吃蛇是否撞到自己
  280. void KillBySelf(pSnake ps)
  281. {
  282. pSnakenode pcur = ps->psnake->next;
  283. while (pcur)
  284. {
  285. if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y)
  286. {
  287. ps->state = KILL_SELF;
  288. break;
  289. }
  290. pcur = pcur->next;
  291. }
  292. }
  293. //游戏运行
  294. void GameRun(pSnake ps)
  295. {
  296. do
  297. {
  298. Printgame(ps);
  299. //判断按键是否被按过
  300. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)
  301. {
  302. ps->dir = UP;
  303. }
  304. else if(KEY_PRESS(VK_DOWN) && ps->dir != UP){
  305. ps->dir = DOWN;
  306. }
  307. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
  308. ps->dir = LEFT;
  309. }
  310. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {
  311. ps->dir = RIGHT;
  312. }
  313. else if (KEY_PRESS(VK_SPACE)) //空格 -- 暂停
  314. {
  315. Pause();
  316. }
  317. else if (KEY_PRESS(VK_ESCAPE)) //游戏正常退出
  318. {
  319. ps->state = NORMAL_END;
  320. break;
  321. }
  322. else if (KEY_PRESS(VK_F3))
  323. {
  324. if (ps->sleep_time >= 100)
  325. {
  326. ps->sleep_time -= 50;
  327. ps->food_scores += 5;//设定食物分数最高25
  328. }
  329. }
  330. else if (KEY_PRESS(VK_F4))
  331. {
  332. if (ps->sleep_time < 300)
  333. {
  334. ps->sleep_time += 100;
  335. ps->food_scores -= 5;//⼀个⻝物分数最低是5分
  336. }
  337. }
  338. Sleep(ps->sleep_time);
  339. //贪吃蛇的移动
  340. SnakeMove(ps);
  341. //判断贪吃蛇是否撞墙
  342. KillByWall(ps);
  343. //判断贪吃蛇是否撞到自己
  344. KillBySelf(ps);
  345. } while (ps->state == OK);
  346. }
  347. //游戏结束
  348. void GameOver(pSnake ps)
  349. {
  350. SetPos(8, 12);
  351. switch(ps->state)
  352. {
  353. case KILL_WALL:
  354. wprintf(L"Sorry,game over because you hit the wall !\n");
  355. break;
  356. case KILL_SELF:
  357. wprintf(L"Sorry,game over because you hit youself !\n");
  358. break;
  359. case NORMAL_END:
  360. wprintf(L"Game exits normally !");
  361. break;
  362. }
  363. //释放贪吃蛇的节点内存
  364. pSnakenode pcur = ps->psnake;
  365. while (pcur)
  366. {
  367. pSnakenode del = pcur;
  368. pcur = pcur->next;
  369. free(del);
  370. }
  371. ps->psnake = NULL;
  372. }
  373. void KeyFun()
  374. {
  375. while (_kbhit())
  376. {
  377. int key = _getch();
  378. }
  379. }

test.c

  1. #include"Snake.h"
  2. void test()
  3. {
  4. Snake snake = { 0 };
  5. int ch = 0;
  6. do
  7. {
  8. ch = 0;
  9. system("cls");
  10. //游戏初始化
  11. GameStart(&snake);
  12. //游戏运行
  13. GameRun(&snake);
  14. //游戏结束
  15. GameOver(&snake);
  16. KeyFun();
  17. SetPos(30, 20);
  18. wprintf(L"再来一局吗? (Y/N)");
  19. ch = getchar();
  20. while (getchar() != '\n');
  21. } while (ch == 'Y' || ch == 'y');
  22. SetPos(0, 27);
  23. }
  24. int main()
  25. {
  26. //本地化
  27. setlocale(LC_ALL, "");
  28. srand((unsigned int)time(NULL));
  29. test();
  30. //KeyFun();
  31. return 0;
  32. }

制作不易,如果本篇内容对你有帮助,可以一键三连支持一下!!!

        如有错误的地方,也请各位大佬们指出纠正

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/947917
推荐阅读
相关标签
  

闽ICP备14008679号