• 【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 )

    二、本地化

    在贪吃蛇游戏中,我们会涉及到墙体□ 和蛇的身体● 的打印,而在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
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    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. }

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

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

  • 相关阅读:
    开源分布式存储系统(HDFS、Ceph)架构分析
    LeetCode 热题100——栈与队列专题(三)
    西门子 S7-1200 与 BL200PN 通信示例
    项目进度管理
    算法竞赛进阶指南 0x21 树与图的遍历
    深入学习Redis,结合实际工作经验总结常用核心知识点
    [React 进阶系列] React Context 案例学习:使用 TS 及 HOC 封装 Context
    天猫/淘宝1688API接口大全
    git-使用操作流程
    arcgis插件 批量出图 按地块批量出图工具
  • 原文地址:https://blog.csdn.net/LH__1314/article/details/140347197