贪吃蛇
文章目录
- 贪吃蛇
-
- 使用到的WIN32一些接口简单介绍
-
- 控制台窗口大小
- 隐藏光标
- 控制光标的位置
- 获取键盘的值的情况
- 字符问题
- 游戏逻辑
-
- 开始游戏
-
- 打印地图
- 初始化贪吃蛇
- 创建食物
- 运行游戏
-
- 控制蛇的移动
- 运行结束
贪吃蛇实现出来的效果如下:
贪吃蛇小游戏录屏
完整代码:
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.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)
enum DIRECTION//蛇头方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAEM_STATUS//蛇运行状态
{
RUNNING,//运行状态
EXIT_NORMAL,//正常退出状态
KILL_BY_WALL,//撞墙
KILL_BY_SELF//撞到自己
};
typedef struct SnakeNode
{
//位置坐标
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
typedef struct Snake
{
pSnakeNode _psnake;//贪吃蛇头结点
pSnakeNode _pFood;//指向食物的节点
int _score;//目前得分情况
int _FoodWeight;//一个食物的分数
int _SleepTime;//定义休眠时间
enum DIRECTION _Dir;//蛇的方向
enum GAEM_STATUS _Status;//游戏状态
}Snake, * pSnake;
int SetPos(short x, short y)//重定位光标位置
{
COORD pos = { x, y };
HANDLE output = NULL;
//获取标准输出的句柄,表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上的光标位置为pos位置
SetConsoleCursorPosition(output, pos);
return 1;
}
void StartUI()
{
//定位光标
SetPos(36, 13);
printf("Welcome to Snacks Game!\n");
SetPos(36, 25);//重定位终端提示符,把终端提示信息放在上面那句话下面
system("pause");
system("cls");
SetPos(36, 13);
printf("using ↑,↓,←,→,\
\n\t\t\tControl the direction in which the snake moves,\
\n\t\t\t\tF3 is hasten, F4 is decelerate");
SetPos(36, 25);
system("pause");
system("cls");
return;
}
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)//蛇身五个节点
{
cur = (pSnakeNode)malloc(sizeof(pSnakeNode));
if (cur == NULL)//检测空指针
{
perror("InitSnake::alloc()");
return;
}
cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2
cur->y = POS_Y;//横着的蛇身,y不用变
cur->next = NULL;//将指针域置为空
//节点头插
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->_Status = RUNNING;//游戏状态置为运行中
ps->_score = 0;//游戏分数置为0
ps->_pFood = NULL;//食物节点先设置为空
ps->_SleepTime = 200;//刷新时间
ps->_FoodWeight = 10;//食物重量设置10
ps->_Dir = RIGHT;//蛇头方向
return;
}
void CreateMap()
{
//上
SetPos(0, 0);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 0; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 0; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
return;
}
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_psnake;
while (cur)
{
if (cur->x == x || cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(pSnakeNode));
if (pFood == NULL)
{
perror("CreateFood::alloc()");
return;
}
pFood->x = x;
pFood->y = y;
ps->_pFood = pFood;
SetPos(x, y);
wprintf(L"%lc", FOOD);
return;
}
void GameStart(pSnake ps)
{
//设置终端窗口大小
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//把光标隐藏
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标具体操作(调用微软提供的C接口)
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(output, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;//将光标显示设置为false
SetConsoleCursorInfo(output, &CursorInfo);//设置控制台光标状态
//欢迎界面
StartUI();
//打印地图
CreateMap();
//初始化贪吃蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
return;
}
void PrintHelpInfo()
{
SetPos(62, 9);
printf("1、you can't hit the wall");
SetPos(62, 10);
printf("or bite yourself");
SetPos(62, 12);
printf("2、using ↑,↓,←,→");
SetPos(62, 13);
printf(",Control snake moving");
SetPos(62, 15);
printf("3、F3 is hasten, F4 is decelerate");
SetPos(62, 17);
printf("4、ESC-Exit Geme, space-time out");
return;
}
void TimeOut()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
{
return 1;
}
else
{
return 0;
}
}
void EatFood(pSnake ps, pSnakeNode pnext)
{
//头插
pnext->next = ps->_psnake;
ps->_psnake = pnext;
//打印蛇
pSnakeNode cur = ps->_psnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(ps->_pFood);
ps->_score += ps->_FoodWeight;
CreateFood(ps);
return;
}
void Space(pSnake ps, pSnakeNode pnext)//表示前面位置为空地
{
//头插
pnext->next = ps->_psnake;
ps->_psnake = pnext;
//打印蛇
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(" ");
/*pSnakeNode tmp = cur->next;
free(tmp);*/
cur->next = NULL;
return;
}
//是否撞墙
void KillByWall(pSnake ps)
{
if (ps->_psnake->x == 00 ||
ps->_psnake->x == 56 ||
ps->_psnake->y == 0 ||
ps->_psnake->y == 26)
ps->_Status = KILL_BY_WALL;
return;
}
//是否咬到自己
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;
}
cur = cur->next;
}
}
void SnakeMove(pSnake ps)
{
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode));
if (pNext == NULL)
{
perror("SnakeMove::alloc()");
return;
}
pNext->next = NULL;
switch (ps->_Dir)
{
case UP:
pNext->x = ps->_psnake->x;
pNext->y = ps->_psnake->y - 1;
break;
case DOWN:
pNext->x = ps->_psnake->x;
pNext->y = ps->_psnake->y + 1;
break;
case LEFT:
pNext->x = ps->_psnake->x - 2;
pNext->y = ps->_psnake->y;
break;
case RIGHT:
pNext->x = ps->_psnake->x + 2;
pNext->y = ps->_psnake->y;
break;
}
//判断下个位置是不是食物
if (NextIsFood(ps, pNext))
{
//吃食物
EatFood(ps, pNext);
}
else
{
//不是食物
Space(ps, pNext);
}
//是否撞墙
KillByWall(ps);
//是否咬到自己
KillBySelf(ps);
return;
}
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(64, 3);
printf("Score: %05d", ps->_score);
SetPos(64, 4);
printf("Every food's score:%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_ESCAPE))
{
ps->_Status = EXIT_NORMAL;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
TimeOut();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 80)
{
ps->_SleepTime -= 30;
ps->_FoodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime < 320)
{
ps->_SleepTime += 30;
ps->_FoodWeight -= 2;
}
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == RUNNING);//游戏运行时各个信息设置打印等
return;
}
void GameEnd(pSnake ps)
{
SetPos(20, 12);
switch (ps->_Status)
{
case EXIT_NORMAL:
printf("Exit the game\n");
break;
case KILL_BY_WALL:
printf("Kill yourself, gameover\n");
break;
case KILL_BY_SELF:
printf("Hit the wall, you are die\n");
break;
}
SetPos(0, 27);
pSnakeNode cur = ps->_psnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
//free(del);
}
ps->_psnake = NULL;
system("pause");
return;
}
void Test()
{
char ch = 0;
do
{
Snake snake = { 0 };//创建贪吃蛇对象
//开始游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//游戏结束
GameEnd(&snake);
SetPos(20, 10);
printf("Another round?(Y/N)\n");
SetPos(40, 10);
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);
return;
}
int main()
{
//设置本地中文字符
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
Test();
return 0;
}
使用到的WIN32一些接口简单介绍
实现过程使用了WIN32的一些API,这里简单介绍一下这些API的功能。
控制台窗口大小
设置控制台窗口大小,在windows界面的cmd中我们可以输入这样的指令来控制窗口的大小:
bash
mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数
打开win的终端输入该指令,就可以调整窗口的大小了,效果如下:
命令行窗口的名称也可以通过命令的方式来更改:
bash
title 贪吃蛇#更改命令行窗口的名称
同样,打开windows的cmd输入指令,效果如下:
在C语言中,我们需要使用system接口来改变终端 窗口的大小 以及 窗口名称 ,使用system接口需要包含 stdlib.h 头文件,例如下面代码:
c
#include<stdio.h>
#include<stdlib.h//使用system接口的头文件
int main()
{
system("title 贪吃蛇");//将命令行窗口的名字更改为需要的名字
system("mode con cols=100 lines=30");//设置命令行窗口的大小
//其他操作
return 0;
}
隐藏光标
通常,我们的终端也可看作坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:
我们在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD ,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:
c
#include<stdio.h>
#include<windows.h>//调用该api需要的头文件
#include<stdlib.h>
int main()
{
COORD pos = { 20, 20 };//使用第一个参数为行,第二参数为列
return 0;
}
实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄(什么是句柄可以看这个blogger的文章:戳我跳转),使用这个句柄可以操作设备。
c
HANDLE output = NULL;//HANDLE为结构体指针类型
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
要隐藏光标,我们就先要获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 CONSOLE_CORSOR_INFO 结构体对象(接收 有关主机光标信息的结构体),再调用 GetConsoleCursorInfo 函数来获得光标信息:
c
#include<stdio.h>
#include<windows.h>//调用win32 api所需要的头文件
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
return 0;
}
CONSOLE_CURSOR_INFO这个结构体包含了控制台光标信息:
c
typedef struct _CONSOLE_CURSOLE_INFO {
DWORD dwSize;
BOOL bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize 参数,由光标填充的字符单元格的百分比。值范围为1到100。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible 参数,设置光标的可见性,如果光标不可见,设置为false。
我们调用结构体的第二个参数设置为false(C语言要包含 stdbool.h 头文件才能使用布尔类型),然后再调用 SetConsoleCursorInfo 函数来设置更改的光标信息。
c
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
cursor_info.bVisible = false;
SetConsoleCursorInfo(output, &cursor_info);//设置更改信息
int ch = getchar();
putchar(ch);
return 0;
}
使用getchar putchar来输入输出信息,检测是否隐藏光标成功:
控制光标的位置
设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了COORD结构体,用来设置位置坐标。获取完坐标之后,我们可以调用 SetConsoleCorsorPosition 函数将光标位置设置到获取的坐标位置。
c
BOOL SetConsoleCorsorPosition{
HANDLE output;//句柄
COORD pos;//位置
};
有了这个接口我们就可以将光标输出的信息放在想要的位置上了:
c
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>
int main()
{
HANDLE output = NULL;
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 20, 20 };
SetConsoleCursorPosition(output, pos);
int ch = getchar();
putchar(ch);
return 0;
}
效果如下:
向上面这样写未免有些麻烦,我们可能会多次改变光标的输出位置,因此我们不妨把其封装成一个函数,使其一条语句也能完成光标定位:
cpp
void SetPos(int x, int y)
{
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 20, 20 };
SetConsoleCursorPosition(output, pos);
}
这样我们在定位光标的时候就简单多了:
c
#include<stdio.h>
#include<stdbool.h>
#include<windows.h>
void SetPos(int x, int y)
{
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 20, 20 };
SetConsoleCursorPosition(output, pos);
}
int main()
{
SetPos(20, 20);
int ch = getchar();
putchar(ch);
return 0;
}
执行结果如下:
这样就能实现控制光标输出位置了。
获取键盘的值的情况
完贪吃蛇我们一定需要用键盘来控制一些功能,我们可以使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:
c
SHORT GetAsyncKeyState(int vKey);
将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。
如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:
c
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//返回1表示按过,返回0表示没有按过
我们可以通过虚拟键码(虚拟键码:戳我查看)来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。
字符问题
我们在打印蛇身和墙体的时候,是需要特殊字符------宽字符 宽字符的长度为2字节,因为不同地区的语言不同,计算机中描述的方式也不太一样,普通的单字节字符并不适合我们的地区,因此C语言加入了宽字符(字符类型 :wchar_t 需要包含 locale.h 头文件)允许程序员针对特定地区调整程序行为函数。
类项: 通过修改地区,程序可以改变它的行为来适应世界的不同区域。但是地区改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的,所以C语言针对不同类型的类项进行修改,下面的一个宏指定一个类项:
- LC_COLLATE:影响字符串比较函数
- LC_CTYPE:影响字符处理函数行为
- LC_MONETARY:影响货币格式
- LC_NUMERIC:影响printf()函数
- LC_TIME:影响时间格式
- LC_ALL :针对所有类项修改,将以上所有类别设定为给定的语言环境
我们使用 setlocale 函数修改类项:
c
char* setlocale(int category, const char* locale);
函数的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。C标准给第二个参数定义了2种可能取值:"C"(正常模式)和""(本地模式) 在任意程序执行开始,默认隐式调用:
c
setlocale(LC_ALL, "C");
我们需要切换到本地环境输出字符,所以:
c
setlocale(LC_ALL, " ");//切换为本地环境
我们想要打印宽字符也是与普通打印不同的,宽字符字面量前必须加上L ,否则C语言就会将其当为窄字符,且占位符应当为"%lc ",和"%ls",才可正常打印宽字符。
cpp
#include<stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
wchar_t ch = L'蛇';
wprintf(L"%lc\n", ch);
return 0;
}
效果如下:
游戏逻辑
我们采用链式结构类表示贪吃蛇,所以我们需要一个结构体来描述蛇的节点以及蛇的一些属性等:
c
#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)
enum DIRECTION//蛇头方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAEM_STATUS//蛇运行状态
{
RUNNING,//运行状态
EXIT_NORMAL,//正常退出状态
KILL_BY_WALL,//撞墙
KILL_BY_SELF//撞到自己
};
typedef struct SnakeNode //蛇的节点
{
//位置坐标
int x;
int y;
struct SnakeNode* next;//指针域
}SnakeNode, *pSnakeNode;
typedef struct Snake
{
pSnakeNode _psnake;//贪吃蛇头结点
pSnakeNode _pFood;//指向食物的节点
int _score;//目前得分情况
int _FoodWeight;//一个食物的分数
int _SleepTime;//定义休眠时间
enum DIRECTION _Dir;//蛇的方向
enum GAEM_STATUS _Status;//游戏状态
}Snake, *pSnake;
描述完蛇的所有情况之后,我们可以把游戏拆分成三部分:游戏开始前,游戏运行时,游戏结束时。
c
void test()
{
Snake snake = { 0 };//创建贪吃蛇对象
//开始游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//游戏结束
GameEnd(&snake);
return;
}
开始游戏
游戏开始的时候我们隐藏光标、设置窗口大小及名称,随后我们打印欢迎界面,打印地图,初始化贪吃蛇以及创建食物等操作。
打印地图
需要注意的是,打印地图的时候,其实我们终端x轴的密度约是y轴的两倍 ,也就是说x轴两个单位约等于y轴一个单位,这里我给了一个合适的值:
c
void CreateMap()
{
//上
SetPos(0, 0);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (int i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 0; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 0; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
return;
}
地图效果:
初始化贪吃蛇
我这里初始化采用链表的头插法来进行插入,设置最初蛇的长度为5。节点创建完了之后打印出蛇身,最后再设置蛇的一系列初始状态:
c
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)//蛇身五个节点
{
cur = (pSnakeNode)malloc(sizeof(pSnakeNode));
if (cur == NULL)//检测空指针
{
perror("InitSnake::alloc()");
return;
}
cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2
cur->y = POS_Y;//横着的蛇身,y不用变
cur->next = NULL;//将指针域置为空
//节点头插
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->_Status = RUNNING;//游戏状态置为运行中
ps->_score = 0;//游戏分数置为0
ps->_pFood = NULL;//食物节点先设置为空
ps->_SleepTime = 200;//刷新时间
ps->_FoodWeight = 10;//食物重量设置10
ps->_Dir = RIGHT;//蛇头方向
return;
}
创建食物
因为蛇是采用链式结构,所以我们的食物也采用节点的方式来存储,首先贪吃蛇的食物是随机刷新的,并且在蛇吃完后才会刷新另一个。而我们地图大小是56 * 27的,食物也是一个宽字符,所以需要保证不能越界,且创建食物时,不能将食物创建在蛇身上:
c
int x = 0, y = 0;
again:
do
{
x = rand() % 53 + 2;//横坐标2-54范围刚好不会越界
y = rand() % 25 + 1;//纵坐标1-25范围也不会越界
} while (x % 2 != 0);//食物位置正确打印,保证与蛇在一条线上
pSnakeNode cur = ps->_psnake;
while (cur)//保证创建食物不在蛇身上
{
if (cur->x == x || cur->y == y)
{
goto again;
}
cur = cur->next;
}
所以这里采用goto还是很合适的,最后再创建出正确的食物节点,将节点赋值给ps的食物节点,在对应位置打印出食物:
运行游戏
游戏运行时,首先打印帮助信息,再打印食物的分数信息,然后根据按键按下的状态执行下一步的操作
这里要注意的是,如果是要控制蛇的方向,如果当前蛇头的位置朝右,那我们就不能向左走,同理,蛇头位置朝上,我们不能朝下走...
除此之外,还需要判断当前按键是不是退出、暂停、加速、减速等状态,如果对应了状态就做对用的事情,并且这些信息是需要不断刷新 的,因此,将其放在循环中在合适不过,当游戏状态为RUNNING时,就一直循环:
c
do
{
SetPos(64, 3);
printf("Score: %05d", ps->_score);
SetPos(64, 4);
printf("Every food's score:%3d", 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_ESCAPE))
{
ps->_Status = EXIT_NORMAL;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
TimeOut();
}
else if (KEY_PRESS(VK_F3))//加速
{
if (ps->_SleepTime >= 80)
{
ps->_SleepTime -= 30;//刷新时间减少
ps->_FoodWeight += 2;//食物重量增加
}
}
else if (KEY_PRESS(VK_F4))//减速
{
if (ps->_SleepTime < 320)
{
ps->_SleepTime += 30;//刷新时间增加
ps->_FoodWeight -= 2;//食物重量减少
}
}
Sleep(ps->_SleepTime);//休眠时间
SnakeMove(ps);//控制蛇的移动
} while (ps -> _Status == RUNNING);//游戏运行时各个信息设置打印等
控制蛇的移动
控制蛇的移动,可以根据按键的状态来对蛇的坐标进行定位,上面我们已经将蛇的_Dir状态置为了对应的宏,再根据这个宏来进行方向选择,将坐标变换。
如果是向右,就将向右的方向x位置+2(密度原因所以+2),y轴方向不变。同理,向下时y+1,x不变...
新位置用一个临时节点来接收,因为需要判断下面的位置是不是食物,以及蛇到底是不是撞墙或者咬到自己了,判断食物与死亡比较简单,大家可以看源码来分析,这里就不具体展示了。
c
void SnakeMove(pSnake ps)
{
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode));//新节点接收
if (pNext == NULL)
{
perror("SnakeMove::alloc()");
return;
}
pNext->next = NULL;
switch (ps->_Dir)//判断蛇头朝向,以及下一步该如何走
{
case UP:
pNext->x = ps->_psnake->x;
pNext->y = ps->_psnake->y - 1;
break;
case DOWN:
pNext->x = ps->_psnake->x;
pNext->y = ps->_psnake->y + 1;
break;
case LEFT:
pNext->x = ps->_psnake->x - 2;
pNext->y = ps->_psnake->y;
break;
case RIGHT:
pNext->x = ps->_psnake->x + 2;
pNext->y = ps->_psnake->y;
break;
}
//判断下个位置是不是食物
if (NextIsFood(ps, pNext))
{
//吃食物
EatFood(ps, pNext);
}
else
{
//不是食物,为空地
Space(ps, pNext);
}
//是否撞墙
KillByWall(ps);
//是否咬到自己
KillBySelf(ps);
return;
}
运行结束
结束的逻辑也比较简单,判断退出状态,如果为正常退出,打印一句提示信息,如果为撞墙或者咬到自己,也各自打印一句提示信息。最后游戏结束,将蛇的链式节点全部释放即可。
c
void GameEnd(pSnake ps)
{
SetPos(20, 12);//在合适的位置打印提示信息
switch (ps->_Status)
{
case EXIT_NORMAL:
printf("Exit the game\n");
break;
case KILL_BY_WALL:
printf("Hit the wall, you are die\n");
break;
case KILL_BY_SELF:
printf("Kill yourself, gameover\n");
break;
}
SetPos(0, 27);//将进程退出信息放到最下面
pSnakeNode cur = ps->_psnake;
while (cur)//将蛇的节点全部销毁
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
ps->_psnake = NULL;
system("pause");
return;
}
游戏结束后,我们可以安排是否再来一局,只需要在外层套上循环即可,输入一个提示信息,如果信息正确,则再来一局,否则退出。
c
void Test()
{
char ch = 0;
do
{
Snake snake = { 0 };//创建贪吃蛇对象
//开始游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
//游戏结束
GameEnd(&snake);
SetPos(20, 10);
printf("Another round?(Y/N)\n");
SetPos(40, 10);
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);
return;
}
用单链表的形式写贪吃蛇还是挺简单的,这也可以检测你C语言到底学的扎不扎实,如果我写的有些问题,欢迎各位佬在评论区里指出更正~~