贪吃蛇游戏运行画面-CSDN直播
目录
5.3.1 判断移动过程中是否遇到食物(NextIsFood)
1. 实验目标
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
-
蛇撞墙死亡
-
蛇撞自身死亡
-
计算得分
-
蛇身加速、减速
-
暂停游戏
2. Win32 API介绍
2.1 Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API 也就是 Microsoft Windows 32 位平台的应用程序编程接口。
其实说人话就是:如果你要基于Windows操作系统来编写一些程序,则Windows会提供各种接口,便于你完成一些功能
2.2 控制台程序(Console)
mode con cols=100 lines=30
也可以通过命令设置控制台窗口的名字:
title 贪吃蛇
#include <stdlib.h>
int main()
{
//设置控制台窗口的长度:设置控制台窗口的大小,30行,100列
system("mode con cols=100 lines=30");
//设置cmd窗口名称
system("title 贪吃蛇");
return 0;
}
运行效果图
执行完后我们会发现窗口大小调制好了,但窗口名却没有,这是因为程序已经结束了。
解决方法:
- getchar(); 执行到这行会停下来,等待接收一个字符
- system("pause") 执行到这行命令程序会暂停
2.3 控制台屏幕上的坐标COORD
注意:使用COORD需要包含头文件<windows.h>
COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };
2.4 GetStdHandle
使用需要包含头文件<windows.h>
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。HANDLE GetStdHandle(DWORD nStdHandle);
说人话:要操作特定的控制台程序就要获得它的操作权限,能要识别你在操作谁
2.5 GetConsoleCursorlnfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息总结:使用GetConsoleCursorlnfo传入的第一个参数是句柄,为何需要句柄?原因是如要隐藏光标首先需要获得当前控制台对应的光标信息。第二个参数是结构体指针
HANDLE hOutput = NULL;
//获取标准输出设备的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义结构体变量
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
//调用完这个函数后就能把hOutput对应的光标信息填充到这个结构体变量中去
2.5.1 CONSOLE_CURSOR_INFO
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
-
dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
-
bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
CursorInfo.bVisible = false; //隐藏控制台光标
2.6 SetConsoleCursorlnfo
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获得控制台光标信息
CursorInfo.bVisible = false;//隐藏控制台光标,false需包含头文件stdbool.h
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
return 0;
}
调试:
2.7 SetConsoleCursorPosition
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
第一个参数传入的是句柄,第二个参数传入的是坐标信息,也就是COORD类型的结构体变量。
实例:
#include <stdio.h>
#include <windows.h>
int main()
{
COORD pos = { 10,5 };
HANDLE hOutput = NULL;
//获得标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
printf("haha\n");
return 0;
}
运行结果:
如果我们不去设置指定光标位置,那么haha就会在这里被输出
由于日后我们可能会多次使用设置指定光标位置,所以我们不妨封装一个设置光标位置的函数Setpos
//设置光标位置
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
//获得标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
2.8 GetAsyncKeyState
SHORT GetAsyncKeyState(
int vKey
);
这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过,函数通过返回值来分辨按键的状态。(返回值类似是short)
如果返回的这个数据的二进制位的最高位为1,则代表按键状态是按下
如果返回的这个数据的二进制位的最高位为0,则代表按键状态是抬起
如果返回的这个数据的二进制位的最低位为1,则代表该键被按过
如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
举个例子
虚拟键值表如下
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
3. 贪吃蛇准备阶段
3.1 地图
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符普通的字符是占一个字节的,这类宽字符是占用2个字节。这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel, 在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表 256 x 256 = 65536 个符号。后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入和<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.1 <locale.h>本地化
- 数字量的格式
-
货币量的格式
-
字符集
-
日期和时间的表示形式
3.1.2 类型
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
- LC_COLLATE:影响字符串比较函数 strco1l()和 strxfrm()。
- LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响printf()的数字格式。
- LC_TIME:影响时间格式strftime()和wcsftime()。
- LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。
3.1.3 setlocale函数
使用setlocale函数需要包含头文件<locale.h>
char* setlocale (int category, const char* locale);
setlocale(LC_ALL, "C");
setlocale(LC_ALL, "");//切换本地环境
扩展:
- setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。
- setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。
#include <locale.h>
int main()
{
char* loc;
loc = setlocale(LC_ALL, NULL);
printf("默认的本地信息:%s\n", loc);
loc = setlocale(LC_ALL, "");
printf("设置后的本地信息:%s\n", loc);
return 0;
}
运行结果:
3.14 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls
#include <stdio.h>
#include <locale.h>
int main()
{
setlocale(LC_ALL, "");
wchar_t ch1 = L'中';
wchar_t ch2 = L'国';
wchar_t ch3 = L'□';
wchar_t ch4 = L'';
printf("%c%c\n", 'a', 'b');
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}
运行结果:
3.1.5 地图坐标
- 列:最好是2的倍数,因为宽字符占2位
- 我们可以看一个正常字符占的大小,我们不难发现,一个字符占的大小的宽度是比较窄的,但是高度是比较长的
3.2 蛇身和食物
3.3 链表定义蛇身
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
3.4 结构体维护贪吃蛇游戏
typedef struct Snake //定义贪吃蛇
{
pSnakeNode pSnake;//维护整条蛇的指针
pSnakeNode pFood;//维护⻝物的指针
enum DIRECTION Dir;//蛇头的⽅向默认是向右
enum GAME_STATUS Status;//游戏状态
int Socre;//当前获得分数
int foodWeight;//默认每个⻝物10分
int SleepTime;//每⾛⼀步休眠时间(蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢)
}Snake, * pSnake;
3.5 枚举定义蛇的方向和游戏状态
蛇的方向,可以一一列举,使用枚举
//方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
游戏状态,可以一一列举,使用枚举
//游戏状态
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
ESC//强制退出游戏
};
3.6 确定游戏流程设计
4. 游戏开始(GameStart)
首先我们先创建3个文件
snake.h ----> 贪吃蛇游戏中类型的声明,函数的声明
snake.c ----> 函数的实现
test.c ----> 贪吃蛇游戏的测试
游戏主逻辑(test.c)
#include "snake.h"
void test()
{
srand((unsigned int)time(NULL));
int ch = 0;
do
{
Snake ps = { 0 };
//游戏开始前的初始化
GameStart(&ps);
//游戏玩的过程
GameRun(&ps);
//游戏结束
GameEnd(&ps);
SetPos(16,13);
printf("再来一局吗?(Y/N):");
scanf(" %c", &ch);
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//适配本地中文环境
setlocale(LC_ALL, "");
test();
SetPos(0, 27);
return 0;
}
4.1 设置游戏窗口大小和名字以及隐藏光标
//游戏开始
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
}
首先我们要让窗口大小100行,30列,有人是不是会疑惑为什么不是58行,27列,我们不是之前给的图片就是这样的嘛?
那是因为这只是地图的大小,地图大小外还有提示的信息,就比如下图一样,所以我们给的大小就会比地图大小还要大一些
4.2 打印欢迎界面
效果图:
要实现这个效果图,首先肯定要用到COORD和SetConsoleCursorPosition来设置光标位置
//定位光标位置
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//在屏幕上设置指定光标位置
SetConsoleCursorPosition(hOutput, pos);
}
接下来就是打印欢迎信息
//打印欢迎信息
void WelcomeToGame()
{
//欢迎信息
SetPos(35,10);
printf("欢迎来带贪吃蛇小游戏\n");
SetPos(38, 20);
//暂停
system("pause");
//清屏
system("cls");
//功能介绍
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速将得到更高的分数。");
SetPos(38, 20);
system("pause");
system("cls");
}
4.3 绘制地图
#define WALL L'□'
打印墙体代码:
//绘制地图
void CreateMap()
{
SetPos(0, 0);
int i = 0;
//一个宽字符占2位,所以i加的是2
//i到56就行了,因为输出WALL就能把56个57空间给占了
//上框框
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下框框
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左框框
//这里i只要加1的原因是宽字符和正常字符的高度是一样的
//只有宽字符和正常字符的宽度有差2倍
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右框框
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
效果图:
4.4 初始化蛇身
我们可以按照下面这张图来进行初始化蛇身与食物
#define BODY L'●'
1.打印蛇身
//初始化蛇身
//ps是维护整条蛇的地址
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
我把行和列封装成一个宏,这样的好处是方便后续修改
#define POS_X 24
#define POS_Y 5
头插法的解析
2.创建好后打印蛇身
//创建好后就打印蛇的身体
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
3.初始化贪吃蛇的其他数据
//初始化贪吃蛇的数据
ps->SleepTime = 200;
ps->Socre = 0;
ps->Status = OK;
ps->Dir = RIGHT;
ps->foodWeight = 10;
ps->pFood = NULL;
4.初始化蛇身总代码
//初始化蛇身
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
//创建好后就打印蛇的身体
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;
ps->pFood = NULL;
}
4.5 创建食物
- 食物是随机出现的,所以坐标就是随机的(但是生成的坐标x必须是2的倍数)
- 生成的坐标必须在墙内
- 生成的坐标不能在蛇的身上
由于打印食物宽字符L''后续可能会多次用到,所以我们把它封装成一个宏
#define FOOD L''
1.创建食物坐标
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//判断坐标是否在蛇的身上
pSnakeNode cur = ps->pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto agin;
}
cur = cur->next;
}
x = rand() % 53 + 2;
rand() % 53生成范围是0~52,后面加2,所以x的范围则是2~54
y = rand() % 25 + 1;rand() % 25 + 1;生成范围是0~24,后面加1,所以y的范围则是1到25
首先我们已经保证生成的坐标是在墙内
然后我们还要判断x是否是2的倍数,所以我们用了个do-while循环来进行判断
后续我们又用了while循环来判断生成的食物节点是否与蛇身的某个节点重合,如果一样,我们就用goto语句来进行跳转,使坐标重新生成
使用rand(),我们需要包含头文件<stdlib.h> 为了防止生成的随机数与后续再次执行的程序一样,我用了srand((unsigned int)time(NULL));来修改种子,如果有不懂srand和time的同学可以看我之前写的博客(猜数字游戏),然后使用srand函数需要包含头文件<stdlib.h>;使用time需要包含头文件<time.h>
走到这一步我们生成的x与y坐标就已经符合要求了,接下来就是生成一个节点(pFood),让x与y赋值给pFood中的x与y,最后让维护蛇指针内的pFood指针来指向这个节点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = pFood;
创建食物总代码
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//判断坐标是否在蛇的身上
pSnakeNode cur = ps->pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto agin;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = pFood;
}
5. 游戏运行(GameRun)
首先我们再次看一遍游戏流程设计
- 游戏运行期间,右侧打印帮助信息,提示玩家
- 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
- 如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
- 确定了蛇的方向和速度,蛇就可以移动了。
5.1 打印帮助信息(PrintHelpInfo)
首先我们可以看到程序运行起来后右侧的帮助信息
因为我们设置游戏窗口的时候设置的是100列,30行,所以设置帮助信息我是一开始是把光标定位到62列17行,大家可以按自己的想法来,不一定要和我一样
//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 17);
printf("1.不能穿墙,不能咬到自己");
SetPos(62, 18);
printf("2.用↑.↓.←.→分别控制蛇的移动。");
SetPos(62, 19);
printf("3.F3是加速,F4是减速");
SetPos(62, 20);
printf("ESC:退出游戏 SPACE:暂停游戏");
}
5.2 按键判断与打印得分
首先,我们需要知道每个键的虚拟键值,如下:
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
然后我整理出了本程序需要用到的虚拟键值
然后我们就要判断我们是否有按过这些键,就可以用到前面GetAsyncKeyState中定义的宏
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
游戏玩的过程总代码
//游戏玩的过程
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印当前得分
SetPos(62, 10);
printf("得分:%d", ps->Socre);
SetPos(62, 11);
printf("每个食物得分:%-2d", 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))
{
//加速
if (ps->SleepTime >= 50)
{
ps->SleepTime -= 30;
ps->foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->foodWeight > 2)
{
ps->SleepTime += 30;
ps->foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
//按ESC键退出
ps->Status = ESC;
break;
}
//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
Sleep(ps->SleepTime);
SnakeMove(ps);
//判断是否撞墙了
KillByWall(ps);
//判断是否撞到自己了
KillBySelf(ps);
} while (ps->Status == OK);
}
当我们按下了上下左右的时候,我们还要与当前蛇行走的方向进行判断
- 当我们按上的时候,蛇当前运动方向是不能向下的
- 当我们按下的时候,蛇当前运动方向是不能向上的
- 当我们按左的时候,蛇当前运动方向是不能向右的
- 当我们按右的时候,蛇当前运动方向是不能向左的
- 当我们按ESC键的时候,我们就把当前游戏状态进行修改,然后直接退出循环
- 当我们按下F3就会加速,休眠时间变短,但时间不可能减成负数,索性我们就规定当休眠时间大于等于50的时候,我们才能加速,当然休眠时间变短一次吃食物分数就会变多
- 当我们按下F4就会减速,休眠时变长,一次吃的食物分数变少,但是我们不能把分数减到0吧,不能吃一个食物一分都不得,所以我们规定只有食物分数大于2时我们按了F4才有效果
注意事项(打印每个食物得分):
打印每个食物的得分有个小细节就是要用%2d(或者%-2d,%3d,%-4d都可以),就是不要用%d来打印,因为一开始默认每个食物的得分是10分,后续我们是可以用F3和F4来控制蛇移动的速度,速度决定每吃一个食物所得分数。如果我们速度慢下来了,食物所得分数就会变少,由原来的10分变成8分或者更低,但因为一开始我们默认打印了10分,然后减了一次速度按理来说是要打印8分,但用%d打印的结果是80,原因是10后面的0没有被覆盖掉
5.3 蛇身移动(SnakeMove)
当然我们在蛇身移动前我们可以让蛇先休眠一下
蛇身移动的主要思想:
-
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
- 确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
(EatFood),如果不是食物则做前进一步的处理(NoFood)。
- 蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
移动过程:
向上和向下
- 向上走:首先创建的新节点是在蛇头上面,由图可以看出新节点y坐标就是蛇头坐标y减1,而新节点x坐标则是和蛇头x坐标一样
- 向下走:首先创建的新节点是在蛇头下面,由图可以看出新节点y坐标就是蛇头坐标y加1,而新节点x坐标则是和蛇头x坐标一样
- 向左走:首先创建的新节点是在蛇头左边,由图可以看出新节点x坐标是蛇头节点x坐标减2(减2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
- 向右走:首先创建的新节点是在蛇头右边,由图可以看出新节点x坐标是蛇头节点x坐标加2(加2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
//蛇移动过程
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
{
//没遇到食物
NotEatFood(pNextNode, ps);
}
}
5.3.1 判断移动过程中是否遇到食物(NextIsFood)
当然在蛇移动的过程中,可能移动的下一个节点就是食物,所以我们还要判断是否遇到了食物
//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}
相等返回1,不相等返回0
5.3.1.1 吃食物(EatFood)
如果返回值是1,则我们遇到了食物,那么我们就把新节点头插到贪吃蛇身上,然后打印蛇身
同时,我们之前定义食物的时候还动态malloc了一块空间,我们既然用新节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)
吃掉了食之后,我们原先在地图上的食物就被覆盖了,那么此时我们就应该再创建一个食物
同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
pSnakeNode cur = ps->pSnake;
ps->Socre += ps->foodWeight;
//打印蛇身
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(ps->pFood);
CreateFood(ps);
}
首先我不知道看到这里的小伙伴会不会有这个疑问:
我们知道食物和新节点是不同的地址(两次malloc开辟出来的),但是他们两个空间重合了(食物和下一个节点在地图上存放空间重合),所以会不会不理解释放食物为什么不会把下一个节点空间也释放了?
原因:
创建出来的下一个节点和食物这个节点只是x和y坐标一样,节点是完全不同的两个节点,所以释放食物对创建出来的下一个节点没有影响
5.3.1.2 不吃食物(NoFood)
当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面
同时我们需要知道,本来贪吃蛇是已经被打印出来了的(初始化蛇身的时候),所以我们只需要将新的头节点打印出来,同时将尾节点打印成空格并释放,我们就能在视觉上达到贪吃蛇走一步的效果
至于找到尾结点对我们来说可是很轻松的,用while循环就能办到
注意:SetPos到尾结点位置之后要打印两个空格,注意,是两个空格,因为尾结点宽字符占2位
//pSnakeNode pNextNode 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
//下一步要走的位置处不是食物,就不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
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;
}
我不知道有没有小伙伴没看我前面介绍有这一个疑问,反正当时我是有这个疑问的:
原因是:这里就是不进行打印的,使用的是上一次打印留下的图形,走到cur->next->next是为了释放下一个节点,所以这个函数没有进行清屏,而是用printf(" ");来把最后一个图形覆盖掉
5.3.2 撞到墙游戏结束(KillByWall)
判断蛇是否撞到墙,我们只需要判断贪吃蛇的头节点是否在墙壁所圈定的范围之内,如果不在这个范围内,那就证明蛇已经撞到墙了
接着,我们需要将游戏的状态更改为 KILL_BY_WALL
当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束
//判断是否撞墙了
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;
}
5.3.3 咬到自身游戏结束(KillBySelf)
我们要判断蛇是否会撞到自己,我们只需要将头节点和蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己了
接着,我们需要将游戏的状态更改为 KILL_BY_SELF
当蛇向后走了一步时,判断到状态不为 OK,就会跳出循环,游戏结束
//检测是否撞自己
void 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;
}
cur = cur->next;
}
}
6. 游戏结束(GameEnd)
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点与食物节点。
至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点
这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针cur指向 NULL 的时候,循环停止
在释放完之后,不忘释放食物的空间
/游戏结束
void GameEnd(pSnake ps)
{
SetPos(18, 10);
switch (ps->Status)
{
case KILL_BY_WALL:
printf("很遗憾撞墙了,游戏结束");
break;
case KILL_BY_SELF:
printf("很遗憾撞到自身了,游戏结束");
break;
case ESC:
printf("按了ESC键,正常退出");
break;
}
//释放蛇身节点和食物节点
pSnakeNode cur = ps->pSnake;
pSnakeNode pNextNode = NULL;
while (cur)
{
pNextNode = cur;
cur = cur->next;
free(pNextNode);
}
free(ps->pFood);
ps = NULL;
}
6.1 总代码
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void test()
{
srand((unsigned int)time(NULL));
int ch = 0;
do
{
Snake ps = { 0 };
//游戏开始前的初始化
GameStart(&ps);
//游戏玩的过程
GameRun(&ps);
//游戏结束
GameEnd(&ps);
SetPos(16,13);
printf("再来一局吗?(Y/N):");
scanf(" %c", &ch);
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//适配本地中文环境
setlocale(LC_ALL, "");
test();
SetPos(0, 27);
return 0;
}
snake.h
#pragma once
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>//随机数
#include <time.h>
#define WALL L'□'
#define BODY L'●'
#define FOOD L''
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//定义蛇身节点
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,//咬到自己
ESC, //按了ESC键退出,正常退出
};
//游戏开始前的准备
void GameStart(pSnake ps);
//打印欢迎信息
void WelcomeToGame();
//定位控制台光标位置
void SetPos(int x, int y);
//绘制地图
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇移动过程
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);
//游戏结束,处理善后工作
void GameEnd(pSnake ps);
snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
//定位光标位置
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//在屏幕上设置指定光标位置
SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎信息
void WelcomeToGame()
{
//欢迎信息
SetPos(35,10);
printf("欢迎来带贪吃蛇小游戏\n");
SetPos(38, 20);
//暂停
system("pause");
//清屏
system("cls");
//功能介绍
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速将得到更高的分数。");
SetPos(38, 20);
system("pause");
system("cls");
}
//绘制地图
void CreateMap()
{
SetPos(0, 0);
int i = 0;
//一个宽字符占2位,所以i加的是2
//i到56就行了,因为输出WALL就能把56个57空间给占了
//上框框
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下框框
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左框框
//这里i只要加1的原因是宽字符和正常字符的高度是一样的
//只有宽字符和正常字符的宽度有差2倍
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右框框
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
//初始化蛇身
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
//创建好后就打印蛇的身体
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;
ps->pFood = NULL;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//判断坐标是否在蛇的身上
pSnakeNode cur = ps->pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto agin;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = pFood;
}
//游戏开始
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
//绘制地图
CreateMap();
//初始化蛇身
InitSnake(ps);
//创建食物
CreateFood(ps);
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 17);
printf("1.不能穿墙,不能咬到自己");
SetPos(62, 18);
printf("2.用↑.↓.←.→分别控制蛇的移动。");
SetPos(62, 19);
printf("3.F3是加速,F4是减速");
SetPos(62, 20);
printf("ESC:退出游戏 SPACE:暂停游戏");
}
//暂停过程
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
pSnakeNode cur = ps->pSnake;
ps->Socre += ps->foodWeight;
//打印蛇身
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(ps->pFood);
CreateFood(ps);
}
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
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;
}
//判断是否撞墙了
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;
}
//检测是否撞自己
void 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;
}
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 (NextIsFood(pNextNode, ps))
{
//遇到食物
EatFood(pNextNode, ps);
}
else
{
//没遇到食物
NotEatFood(pNextNode, ps);
}
}
//游戏玩的过程
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印当前得分
SetPos(62, 10);
printf("得分:%d", ps->Socre);
SetPos(62, 11);
printf("每个食物得分:%-2d", 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))
{
//加速
if (ps->SleepTime >= 50)
{
ps->SleepTime -= 30;
ps->foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->foodWeight > 2)
{
ps->SleepTime += 30;
ps->foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
//按ESC键退出
ps->Status = ESC;
break;
}
//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
Sleep(ps->SleepTime);
SnakeMove(ps);
//判断是否撞墙了
KillByWall(ps);
//判断是否撞到自己了
KillBySelf(ps);
} while (ps->Status == OK);
}
//游戏结束
void GameEnd(pSnake ps)
{
SetPos(18, 10);
switch (ps->Status)
{
case KILL_BY_WALL:
printf("很遗憾撞墙了,游戏结束");
break;
case KILL_BY_SELF:
printf("很遗憾撞到自身了,游戏结束");
break;
case ESC:
printf("按了ESC键,正常退出");
break;
}
//释放蛇身节点和食物节点
pSnakeNode cur = ps->pSnake;
pSnakeNode pNextNode = NULL;
while (cur)
{
pNextNode = cur;
cur = cur->next;
free(pNextNode);
}
free(ps->pFood);
ps = NULL;
}
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 贪吃蛇(C语言详解)
发表评论 取消回复