项目实战 - 贪吃蛇

目录

1. 基本功能

2. 技术要点

3. 环境

4. 效果演示

5. 控制台设置

6. Win32 API介绍

6.1 Win32 API

6.2 程序台控制(Console)

6.3 控制台屏幕上的坐标(COORD)

6.4 GetStdHandle

6.5 GetConsoleCursorInfo

6.5.1 CONSOLE_CURSOR_INFO

6.6 SetConsoleCursorInfo

6.7 SetConsoleCursorPosition

6.8 GetAsyncKeyState 

7. 贪吃蛇游戏设计与分析

7.1 地图

7.1.1 本地化

7.1.2 类项 

7.1.3 setlocale函数

7.1.4 宽字符的打印

7.1.5 地图坐标

​7.2 蛇身和食物

7.3 数据结构设计

7.4 游戏流程

8. 核心逻辑实现分析

8.1 游戏主逻辑

8.2 游戏开始(GameStart)

8.2.1 打印欢迎界面

8.2.2 创建地图

8.2.3 初始化蛇身

8.2.4 创建第一个食物

8.3 游戏运行(GameRun) 

8.3.1 PrintHelpInfo

8.3.2 按键检测

8.3.3 蛇身移动(SnakeMove) 

8.3.3.1 NextIsFood

8.3.3.2 EatFood

8.3.3.3 NoFood

8.3.3.4 KillByWall

8.3.3.5 KillBySelf

8.4 游戏结束

9. 参考代码


1. 基本功能

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。

  • 贪吃蛇地图绘制
  • 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇身加速、减速
  • 暂停游戏

2. 技术要点

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。

3. 环境

Windows 10、VS2019

4. 效果演示

贪吃蛇

5. 控制台设置

如果你的电脑是Win11,并且系统的控制台窗口是这样显示的,那么就需要调整一下。

调整方式:

设置背景色以及文字颜色。

6. Win32 API介绍

本次实现贪吃蛇会使用到一些Win32 Api知识,那么我们就要了解了解。

6.1 Win32 API

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

6.2 程序台控制(Console)

平时我们运行起来的黑框程序其实就是程序台程序(cmd)。

我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列。

mode con cols=100 lines=30

参考:mode | Microsoft Learn

也可以通过命令设置控制台窗口的名字。

title 贪吃蛇

参考:title | Microsoft Learn 

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。

#include <stdio.h>
#include <stdlib.h>//system需要包含的头文件
int main()
{//设置控制台窗口的大小,30行,100列system("mode con cols=100 lines=30");//设置控制台窗口的名字system("title 贪吃蛇");//getchar();//使用getchar函数让应用程序停止下来观察//当然除了getchar这个函数,其实还有一个控制台命令system("pause");//pause - 暂停return 0;
}

6.3 控制台屏幕上的坐标(COORD)

我们写的贪吃蛇这个游戏跑起来的时候,贪吃蛇在控制台的任何地方,食物也可能在控制台的任何地方,在控制台上要找到某一个位置,屏幕上的任意一个位置其实就是坐标。

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

COORD类型的声明:

typedef struct_COORD{SHORT X;SHORT Y;
}COORD, * PCOORD;

给坐标赋值:

#include <windows.h>//COORD的头文件
int main()
{COORD pos = { 0,0 };COORD pos2 = { 10,20 };return 0;
}

COORD 结构 - Windows Console | Microsoft Learn

6.4 GetStdHandle

GetStdHandle 函数 - Windows Console | Microsoft Learn

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

意思就是要操作控制台就可以使用这个函数先获得控制台。

HANDLE GetStdHandle(DWORD nStdHandle);
//接收DWORD 类型的值,返回一个HANDLE(句柄)

如果我要运行这个贪吃蛇游戏,就是把关于贪吃蛇的所有信息显示在屏幕上,所有就需要标准输出设备,传参就传STD_OUTPUT_HANDLE。

int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;//HANDLE其实是void*类型的指针,所有一开始不知道给什么//值的话可以给空指针houtput = GetStdHandle(STD_OUTPUT_HANDLE);return 0;
}

6.5 GetConsoleCursorInfo

GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

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

 BOOL WINAPI GetConsoleCursorInfo(HANDLE               hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);//PCONSOLE_CURSOR_INFO  是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
(光标)的信息
int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO cursor_info = { 0 };//当这个函数完成任务之后就会把光标信息放到cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//打印printf("%d\n", cursor_info.bVisible);printf("%d\n", cursor_info.dwSize);system("pause");return 0;
}

6.5.1 CONSOLE_CURSOR_INFO

CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn

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

 typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL  bVisible;} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  • bVisible,游标的可见性。如果光标可见,则此成员为TRUE。

6.6 SetConsoleCursorInfo

SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

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

 BOOL WINAPI SetConsoleCursorInfo(HANDLE  hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//修改光标的占比cursor_info.dwSize = 50;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);system("pause");return 0;
}

设置后的光标信息会发现比之前高一些。

其实修改光标的可见性也是一样的。 

#include <stdbool.h>
int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//修改光标的占比//cursor_info.dwSize = 50;//修改光标的可见性cursor_info.bVisible = false;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);system("pause");return 0;
}

 

这个时候光标就看不见了。 

6.7 SetConsoleCursorPosition

SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn

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

 BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD  pos);
#include <stdbool.h>
int main()
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { 10,20 };//第一个参数句柄,第二个参数位置SetConsoleCursorPosition(houtput, pos);getchar();//用getchar暂停观察更清楚//system("pause");return 0;
}

我们可以看到,光标确实改变了位置。 

为了方便,我们还可以把设置光标位置的代码封装成一个函数。

void SetPos(short x, short y)
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { x,y };//第一个参数句柄,第二个参数位置SetConsoleCursorPosition(houtput, pos);
}
int main()
{SetPos(10, 20);printf("你好!");SetPos(5, 10);printf("hello!");getchar();//用getchar暂停观察更清楚//system("pause");return 0;
}

6.8 GetAsyncKeyState 

获取按键情况, GetAsyncKeyState  的函数原型如下:

SHORT GetAsyncKeyState(int vKey);

将按键上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

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

虚拟键值:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1,那么我们就可以写个宏来判断。

//结果是1表示按过
//结果是0表示未按过
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

例子:检测数字键

#include <stdio.h>
#include <windows.h>
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
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;
}

按键盘上的几就打印几。

7. 贪吃蛇游戏设计与分析

7.1 地图

我们最终的贪吃蛇大概就是这样的。

 

 

横向是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。

 

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符:●,打印食物使用宽字符:★,普通的字符是占一个字节的,这类宽字符是占用两个字节。

过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且采用了单字节中的低7位,最高位是没有用的,可表示为0xxxxxxx,可以看到,ASCII字符将共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就⽆法⽤ASCII码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编⼊新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字⺟,因此,哪怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel( ),在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是⼀样的,不⼀样的只是128--255的这⼀段。

⾄于亚洲国家的文字,适用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。比如,简体中⽂常见的编码方式是GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256x256=65536个符号。

后来为了使C语⾔适应国际化,C语⾔的标准中不断加入了国际化的⽀持。比如:加⼊了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序行为的函数。

7.1.1 <locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

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

  • 数字量的格式
  • 货币量的格式
  • 字符集
  • 日期和时间的表示形式
7.1.2 类项 

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

  • LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()。
  • LC_CTYPE:影响字符串处理函数的行为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响printf()的数字格式。
  • LC_TIME:影响时间格式strftime()和wcsftime()。
  • LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境。

每个类项的详细说明:%> | Microsoft Learn

7.1.3 setlocale函数
char* setlocale (int category, const char* locale);

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

setlocale的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。

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

在任意程序执行开始,都会隐藏式执行调用:

setlocale(LC_ALL, "C");

当地区设置我C时,设置为C语言默认的模式,这时库函数按正常方式执行。

当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

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

setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。

setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。

#include <locale.h>//setlocale需要包含的头文件
int main()
{//获取默认的本地信息char* ret = setlocale(LC_ALL, NULL);printf("%s\n", ret);//设置后的本地信息ret = setlocale(LC_ALL, "");printf("%s\n", ret);return 0;
}

例子:

/* setlocale example */
#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, struct tm, time, localtime, strftime */
#include <locale.h>     /* struct lconv, setlocale, localeconv */int main()
{time_t rawtime;struct tm* timeinfo;char buffer[80];struct lconv* lc;time(&rawtime);timeinfo = localtime(&rawtime);int twice = 0;do {//获取当前的模式printf("Locale is: %s\n", setlocale(LC_ALL, NULL));//获取时间和日期strftime(buffer, 80, "%c", timeinfo);printf("Date is: %s\n", buffer);//获取本地化的符号lc = localeconv();printf("Currency symbol is: %s\n-\n", lc->currency_symbol);//设置为本地环境setlocale(LC_ALL, "");} while (!twice++);return 0;
}

从运行结果看出,刚开始就是C的环境,然后下面就是C语言环境下获取的时间日期的信息, 当把环境设置为本地之后,环境就是简体中文,然后日期就和C环境下的日期不一样了,而本地化的符号也是钱的符号。

7.1.4 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?

宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理、前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc。在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。

#include <locale.h>
#include <stdio.h>
int main()
{//设置本地化setlocale(LC_ALL, "");printf("%c%c\n", 'a', 'b');wchar_t wc1 = L'你';wchar_t wc2 = L'好';wprintf(L"%lc\n", wc1);//占两个字符wprintf(L"%lc\n", wc2);//打印蛇身wprintf(L"蛇身:%lc\n", L'●');//打印食物wprintf(L"食物:%lc\n", L'★');return 0;
}

输出结果: 

普通字符和宽字符的宽度。

int main()
{system("mode con cols=30 lines=30");return 0;
}

当我指定控制台大小的时候,行30,列30,这时候就会发现行lines是30这个好长啊,但是列是30 的时候却很窄,这就是因为x坐标的30和y坐标的30不一样长。

7.1.5 地图坐标

我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙。

7.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点都是●,在固定的一个坐标处,比如(20,5)处开始出现蛇,连续5个节点。

注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。

关于食物,就是在墙体内随机生成一个坐标(生成的坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

7.3 数据结构设计

首先我们创建三个文件,main.c、snake,c、snakc.h。

main.c用来测试我们写的代码

snake.c用来存放贪吃蛇的代码

snake.h用来存放函数声明

在游戏的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行。

蛇节点结构:

//类型的声明 - 蛇身的节点类型
typedef struct SnakeNode
{//坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇。

//贪吃蛇
typedef struct Snake
{//指向舌头的指针pSnakeNode _pSnake;//指向食物节点的指针pSnakeNode _pFood;//蛇的方向enum DIRECTION _dir;//游戏状态enum GAME_STATUS _status;//一个食物的分数int _good_weight;//总成绩int _score;//休息时间,时间越短,速度越快,时间越长,速度越慢int _sleep_time;
}Snake,*pSnake;

蛇的方向,可以--列举,那么就可以使用枚举

//蛇的方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};

蛇的状态,可以--列举,也可以使用枚举。

//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{OK,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出
};

7.4 游戏流程

8. 核心逻辑实现分析

8.1 游戏主逻辑

程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。

主逻辑分为三个过程:

  • 游戏开始(GameStart)完成游戏的初始化
  • 游戏运行(GameRun)完成游戏运行逻辑的实现
  • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
//游戏的测试逻辑
void playgame()
{int ch = '\0';do{//创建贪吃蛇Snake snake = { 0 };//初始化游戏GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N)>:");ch = getchar();while(getchar()!='\n');//清理输入缓冲区} while (ch == 'Y' || ch == 'y');SetPos(0, 26);
}
int main()
{//设置适配本地环境setlocale(LC_ALL, "");//设置随机数的种子srand((unsigned int)time(NULL));playgame();return 0;
}

8.2 游戏开始(GameStart)

这个模块完成游戏的初始化任务:

  • 控制台窗口大小的设置
  • 控制台窗口名字的设置
  • 光标的隐藏
  • 打印欢迎界面
  • 创建地图
  • 初始化蛇
  • 创建第一个食物
void GameStart(pSnake ps)
{//1.设置窗口的大小和窗口名字和光标隐藏system("mode con cols=100 lines=30");system("title 贪吃蛇");HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标CONSOLE_CURSOR_INFO cursor_info = { 0 };GetConsoleCursorInfo(houtput, &cursor_info);cursor_info.bVisible = false;SetConsoleCursorInfo(houtput, &cursor_info);//2.欢迎界面和功能介绍WelcomeToGame();//3.绘制地图CreateMap();//4.创建蛇InitSnake(ps);//5.创建食物CreateFood(ps);
}
8.2.1 打印欢迎界面

在游戏开始之前,做一些提示功能。

void WelcomeToGame()
{//定位光标位置,打印提示信息SetPos(40, 14);wprintf(L"欢迎来到贪吃蛇小游戏\n");SetPos(42, 20);system("pause");//暂停system("cls");//清理屏幕SetPos(25, 14);wprintf(L"用↑.↓.←.→分别控制蛇的移动,F3为加速,F4为减速\n");SetPos(25, 15);wprintf(L"加速能够得到更高的分数");SetPos(42, 20);system("pause");system("cls");}

效果:

8.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符,所有使用wprintf函数,打印格式串前使用L,打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

坐标的计算。

上:(0,0)到(56,0)

下:(0,26)到(56,26)

左:(0,1)到(0.25)

右:(56,1)到(56,25)

创建地图函数CreateMap

void CreateMap()
{//上for (int i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (int i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}
8.2.3 初始化蛇身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。

创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。

  • 蛇的初始位置从(24,5)开始
  • 游戏状态时:ok
  • 蛇的移动速度:200毫秒
  • 蛇的默认方向:RIGHT
  • 初始成绩:0
  • 每个食物的分数:10

蛇身打印的宽字符:

#define BODY L'●'

初始化函数:InitSnake

void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;for (int i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake::malloc error!\n");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->_dir = RIGHT;//默认向右ps->_score = 0;//总成绩ps->_good_weight = 10;//一个食物10分ps->_sleep_time = 200;//单位是毫秒ps->_status = OK;//状态是OK
}

8.2.4 创建第一个食物

先随机生成食物的坐标

        x坐标必须是2的倍数

        食物的坐标得在墙体内部

        食物的坐标不能和蛇身每个节点的坐标重复

创建食物节点,打印食物

食物打印的宽字符:

#define FOOD L'★'

创建食物的函数:CreateFood

void CreateFood(pSnake ps)
{int x = 0;int y = 0;//随机生成食物begin: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 begin;}cur = cur->next;}//创建食物pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));if (food == NULL){perror("CreateFood::malloc error!\n");return;}food->x = x;food->y = y;food->next = NULL;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = food;
}

效果:

8.3 游戏运行(GameRun) 

游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64,10)。

根据游戏状态检查游戏是否继续,如果状态是OK,游戏继续,否则游戏结束。

如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

8.3.1 PrintHelpInfo
//打印提示信息
void PrintHelpInfo()
{SetPos(64, 10);wprintf(L"%ls", L"不能穿墙,不能咬到自己\n");SetPos(64, 11);wprintf(L"%ls", L"用↑.↓.←.→分别控制蛇的移动\n");SetPos(64, 12);wprintf(L"%ls", L"按F3加速,F4减速\n");SetPos(64, 13);wprintf(L"%ls", L"按esc退出游戏,按空格暂停游戏\n");SetPos(64, 15);wprintf(L"%ls", L"2024-9-4\n");
}
8.3.2 按键检测

虚拟按键的罗列:

  • 上:VK_UP
  • 下:VK_DOWN
  • 左:VK_LEFT
  • 右:VK_RIGHT
  • 空格:VK_SPACE
  • ESC:VK_ESCAPE
  • F3:VK_F3
  • F4:VK_F4

按键的状态我们写了一个宏:

#define KEY_PRESS(VK)  ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();//按键检测do{//打印总分数和食物分数SetPos(64, 7);wprintf(L"总分数: %d\n",ps->_score);SetPos(75, 7);wprintf(L"每个食物得分: %2d\n", ps->_good_weight);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)){//ESC退出ps->_status = END_NORMAL;break;}else if (KEY_PRESS(VK_F3)){//F3加速if (ps->_sleep_time>120){ps->_sleep_time -= 20;ps->_good_weight += 2;}}else if (KEY_PRESS(VK_F4)){//F4减速if (ps->_good_weight > 2){ps->_sleep_time += 20;ps->_good_weight -= 2;}}//蛇的休眠时间Sleep(ps->_sleep_time);//蛇的移动SnakeMove(ps);} while (ps->_status == OK);
}

8.3.3 蛇身移动(SnakeMove) 

先创建一个节点,根据移动方向和蛇头的坐标,蛇移动的下一个位置的坐标。

确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做食物处理(EatFood),不是食物就做前进一步的处理(NoFood)。

蛇身移动后,判断此处移动是否造成撞墙(KillByWall)或者撞上自己的蛇身(KillBySelf),从而影响游戏状态。

void SnakeMove(pSnake ps)
{//创建节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc() error!\n");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);
}
8.3.3.1 NextIsFood
//pSnakeNode pn - 下一个节点的地址
//pSnake ps - 蛇的指针
bool NextIsFood(pSnakeNode pn, pSnake ps)
{return (pn->x == ps->_pFood->x) && (pn->y == ps->_pFood->y);
}
8.3.3.2 EatFood

如果下一个节点是食物的化我们就得将申请的节点头插到我们的链表中,然后打印链表,加分,最后在释放食物节点,创建新的食物节点。

void EatFood(pSnakeNode pn, pSnake ps)
{//将下一个节点头插pn->next = ps->_pSnake;ps->_pSnake = pn;//打印蛇pSnakeNode cur = ps->_pSnake;while(cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//加分ps->_score +=  ps->_good_weight;//释放食物节点free(ps->_pFood);CreateFood(ps);
}
8.3.3.3 NoFood

将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,并且释放掉蛇身最后一个节点,这里要注意的是释放最后一个节点还得将最后一个节点的前一个节点的next指针置为空,这样保证蛇尾打印可以正常结束,不会越界访问。

void NoFood(pSnakeNode pn, pSnake ps)
{//头插pn->next = ps->_pSnake;ps->_pSnake = pn;//打印蛇pSnakeNode cur = ps->_pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//将尾节点打印空白字符SetPos(cur->next->x, cur->next->y);printf("  ");//释放最后一个节点free(cur->next);cur->next = NULL;}
8.3.3.4 KillByWall
void 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;}
}
8.3.3.5 KillBySelf

我们要从第二个节点开始遍历,头节点是否和我们蛇身的节点的坐标相等,相等的话就说明撞到自己了。

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_SELF;break;}cur = cur->next;}
}

8.4 游戏结束

当游戏状态不再是OK的时候我们就要打印游戏结束的原因,并且释放蛇身的节点。

void GameEnd(pSnake ps)
{//定位光标SetPos(24, 13);//判断状态switch (ps->_status){case KILL_BY_WALL:wprintf(L"您撞到了墙上");break;case KILL_BY_SELF:wprintf(L"您撞到了自己");break;case END_NORMAL:wprintf(L"您主动退出了游戏");break;}//释放节点pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);del = NULL;}
}

9. 参考代码

https://gitee.com/Axurea/blogboardandcode/tree/master/2024_9_2_Projecticon-default.png?t=N7T8https://gitee.com/Axurea/blogboardandcode/tree/master/2024_9_2_Project

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1524095.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Android终端如何快速接入GB28181平台实现实时音视频回传

技术背景 GB28181是由中国国家标准委员会发布的基于IP网络的安防视频监控标准。Android平台GB28181设备对接模块&#xff0c;主要涉及到视频监控领域&#xff0c;可实现不具备国标音视频能力的 Android终端&#xff0c;通过平台注册接入到现有的GB/T28181—2016服务&#xff0…

数据结构——单链表查询、逆序、排序

1、思维导图 2、查、改、删算法 //快慢排序法找中间值 int mid_link(Link_t *plink) {Link_Node_t *pfast plink->phead;Link_Node_t *pslow pfast;int m 0;while(pfast ! NULL){pfast pfast->pnext;m;if(m % 2 0){pslow pslow->pnext;}}printf("%d\n&quo…

WPF-快速构建统计表、图表并认识相关框架

一、使用ScottPlot.Wpf 官网地址&#xff1a;https://scottplot.net/quickstart/wpf/ 1、添加NuGet包&#xff1a;ScottPlot.Wpf 2、XAML映射命名空间&#xff1a; xmlns:ScottPlot"clr-namespace:ScottPlot.WPF;assemblyScottPlot.WPF" 3、简单示例&#xff1a;…

刘润《关键跃升》读书笔记6

把教练传授内容的知识含量分成五个级别&#xff1a;⽩⽔级、啤酒级、⻩酒 级、红酒级和⽩酒级&#xff08;⻅图3-4&#xff09; 第⼀个层级是⽩⽔级&#xff08;0&#xff09;。教练在传授的时候&#xff0c;什么都没有教&#xff0c;只 会训⼈。 ⼆个层级是啤酒级&#xff08…

LaTeX各符号表示方式(持续更新~)

- "\mu"&#xff1a;穆 miu - "\sigma"&#xff1a;西格玛xigema - "\lambda"&#xff1a;兰姆达或拉姆达lamuda - "\alpha"&#xff1a;阿尔法aerfa - "\beta"&#xff1a;贝塔beita - "\gamma"&#xff1a;伽马…

比特币客户端和API

1. 比特比客户端的安装 Bitcoin Core 客户端适用于从 x86 Windows 到 ARM Linux 的不同架构和平台&#xff0c;如下图所示&#xff1a; 2. Bitcoin Core客户端的类型 2.1 Bitcoind Bitcoind 末尾的字母 d 表示 daemon (守护程序&#xff09;。所谓守护程序&#xff0c;就是指…

deep-live-cam实时换中文整合包下载,双击exe直接运行

windows环境整合包下载地址&#xff1a; 点击下载 直接解压&#xff0c;双击启动.exe即可使用 硬件要求&#xff1a;有英伟达显卡&#xff0c;且要支持CUDA 硬件不符合要求也不用急&#xff0c;软件也有对应mac版本和windows非N卡版本&#xff0c;我还没做成整合包&#xff0c;…

【python因果推断库6】使用 pymc 模型的工具变量建模 (IV)1

目录 使用 pymc 模型的工具变量建模 (IV) 使用 pymc 模型的工具变量建模 (IV) 这份笔记展示了一个使用工具变量模型&#xff08;Instrumental Variable, IV&#xff09;的例子。我们将会遵循 Acemoglu, Johnson 和 Robinson (2001) 的一个案例研究&#xff0c;该研究尝试解开…

大屏可视化:阿里 DataV 大屏怎么做自适应的?

你好&#xff0c;我是沐爸&#xff0c;欢迎点赞、收藏、评论和关注。 阿里 DataV 大屏是一款功能强大的数据可视化应用搭建工具&#xff0c;由阿里云提供&#xff0c;旨在帮助用户通过图形化的界面轻松搭建专业水准的可视化应用。 下面我们一起看下 DataV 大屏 是如何做自适应…

Leetcode 第 408 场周赛题解

Leetcode 第 408 场周赛题解 Leetcode 第 408 场周赛题解题目1&#xff1a;3232. 判断是否可以赢得数字游戏思路代码复杂度分析 题目2&#xff1a;3233. 统计不是特殊数字的数字数量思路代码复杂度分析 题目3&#xff1a;3234. 统计 1 显著的字符串的数量思路代码复杂度分析 题…

矮草坪渲染尝试

本来说写unity里的&#xff0c;由于three测试方便&#xff0c;先试试three 这个图片是目标效果 可以看见草很矮&#xff0c;很密集&#xff0c;如果用instance来绘制的话&#xff0c;遭不住的 忽然发现这个效果很像绒毛效果 于是找了博客康康 https://zhuanlan.zhihu.com/p/256…

Ubuntu | 安装 Truffle 框架(安装缓慢)

目录 预备工作具体步骤Step1&#xff1a;安装 nvma. 官方方式&#xff08;可能失败&#xff09;b. 压缩包安装方式 Step2&#xff1a;安装 node.js 和 npmStep3&#xff1a;安装 Truffle 参考博客 前言&#xff1a;昨天安装 Truffle 框架&#xff0c;结果缓冲条转了一晚上都没安…

企业全球组网有哪几种常用的组网方式?

为了实现全球范围内的高效通信和数据传输&#xff0c;企业需要选择适合自身需求的组网方式。企业全球组网的有哪几种主要方式&#xff1f;一般包括传统的MPLS网络、云网络、SD-WAN技术和全球VPN&#xff0c;以帮助企业在全球范围内建立稳定、高效的网络连接。 1、传统的MPLS网络…

探索AWS EC2:云计算的强大引擎

在数字化转型的浪潮中&#xff0c;企业对计算资源的需求不断增长。亚马逊弹性计算云&#xff08;EC2&#xff09;作为AWS&#xff08;亚马逊网络服务&#xff09;的核心产品之一&#xff0c;凭借其强大的功能和灵活性&#xff0c;成为了全球企业构建和扩展应用的首选平台。无论…

数据结构(邓俊辉)学习笔记】串 10——BM_BC算法:坏字符

文章目录 1.坏字符2. 特殊情况 1.坏字符 实际上&#xff0c;刚才的实例中我们所展示的那样一个计算过程&#xff0c;就是所谓 BM 算法所采用的策略之一&#xff0c;而这一策略&#xff0c;将我们刚才所说的教训称作坏字符。 在这里&#xff0c;不妨改为基于蛮力算法的第二个版…

设置电子签名

设置点赞签名代码 export class Signature {width: number 300height: number 300canvas!: HTMLCanvasElementctx!: CanvasRenderingContext2Dprivate drawing: boolean falsepreTask: string[] []nextTask: string[] []private allTask: { x: number; y: number; color: …

Leetcode - 周赛413

目录 一&#xff0c;3274. 检查棋盘方格颜色是否相同 二&#xff0c;3275. 第 K 近障碍物查询 三&#xff0c;3276. 选择矩阵中单元格的最大得分 四&#xff0c;3277. 查询子数组最大异或值 一&#xff0c;3274. 检查棋盘方格颜色是否相同 本题就是找规律&#xff0c;假设白…

EPLAN中如何将图纸导出为PDF文件并设置页边距?

EPLAN中如何将图纸导出为PDF文件并设置页边距? 如下图所示,在项目中选中需要导出的图纸页, 如下图所示,点击上方页-----导出------PDF, 如下图所示,在弹出的窗口中设置导出文件的名称、输出目录、输出颜色,这里建议勾选“使用打印边距”, 如下图所示,继续点击下方的设…

论文速读|重新审视奖励设计与评估:用于强健人型机器人站立与行走控制的方法

论文地址&#xff1a;https://arxiv.org/pdf/2404.19173 这篇论文为类人机器人站立和行走&#xff08;SaW&#xff09;控制器的持续可衡量改进奠定了基础。通过引入一套定量实际基准测试方法&#xff0c;作者展示了现有控制器的优缺点&#xff0c;并通过基准测试指导新控制器的…

论文速读|自然语言的最优控制合成:机遇与挑战

项目地址&#xff1a;Optimal Control Synthesis from Natural Language: Opportunities and Challenges 介绍了一种从自然语言自动生成最优控制器的框架&#xff0c;该框架主要包括以下几个步骤&#xff1a;首先&#xff0c;通过人类用户提供的初始文本和系统描述&#xff0c;…