目录
一、游戏背景及相关知识介绍
1.1游戏背景
贪吃蛇(也叫贪食蛇)游戏是一款休闲益智类游戏,有PC和手机等多平台版本。游戏通过控制蛇头方向吃食物,从而使蛇变得越来越长。
我们可以通过控制台程序实现简易的贪吃蛇游戏。
1.2游戏效果展示
1.3游戏涉及知识
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等
二、游戏涉及相关Win32API
2.1Win32API
windows这个多作业操作系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序,所以便称之为Application Programming Interface,简称API函数。WIN32 API 就是Microsoft Windows32位平台的应用程序编程接口。
2.2控制台程序
平常我们运行起来的黑框程序其实就是控制台程序。
我们可以使用cmd命令来设置控制台窗口的长宽(eg: mode 命令)
mode con cols=100 lines =30//设置控制台窗口的大小:30行,100列
也可以通过命令设置控制台窗口的名字(eg: title 命令)
title 贪吃蛇//将控制台命名为贪吃蛇
这些能在控制台窗口执行的命令,也可以调用C语言函数system执行
#include <stdio.h>
int main()
{
//设置控制台窗口的大小:30行,100列
systrm("mode con cols=100 lines=30");
//设置cmd窗口名称
system("title 贪吃蛇");
return 0;
}
2.3控制台屏幕上的坐标COORD
COORD是Windows API中定义的一个结构体,标识一个字符在控制台屏幕缓冲区上的坐标,坐标系的远点位于缓冲区的顶部左侧单元格,x轴正方向水平向右,y轴正方向竖直向下
COORD类型的声明:
typedef struct _COORD
{
SHORT X;
SHORT Y;
}COORD,*PCOORD;
给坐标赋值:
COORD pos = { 10, 15};
2.4GetStdHandle
用于从一个特定的标准设备(标准输入、标准输出或者标准错误)中取得一个句柄(用来标识不同设备的数据),这个句柄可以操作设备
HANDLE GetStdHandle(DWORD nStdHandle);
实例:
HANDLE houtput = NULL;
//获得标准输出设备的句柄
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
2.5GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONCOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONCOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO结构的指针,
该结构接受有关主机游标
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取控制台光标信息
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
2.5.1CONSOLE_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.6SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的贯标的大小和可见性
BOOL WINAPI SetConsoleCursorInfo
(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
2.7SetConsoleCursorPosition
设置指定控制台缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
实例:
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
我们可以封装一个设置光标位置的函数SetPos
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
2.8GetAsyncKeyState
获取按键状态,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态
SHORT GetAsyncKeyState ( int vKey);
GetAsyncKeyState函数的返回值是short类型,如果返回的16位short类型数据中最高位是1,则说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1则说明按键被按过,否则为0。
我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
键盘上的每一个按键都对应着一个虚拟键码 虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
三、游戏分析与代码实现
3.1游戏需实现功能
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏、结束游戏
3.2地图的绘制
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符:●,打印食物使用宽字符★
3.2.1<locale.h>本地化
由于C语言最开始假定字符都是单字节的,ASCII编码只能包含128个字符,对于很多非英语国家(地区)不适用,为了使C语言适应国际化,C语言标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t和宽字符的输入输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区调整程序行为的函数。
<locale.h>提供的函数用于控制C标准库中对于不同地区会产生不一样行为的部分
在标准中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
3.2.2类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能使我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的宏,包含可修改的类项
- LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()
- LC_CTYPE:影响字符处理函数的行为
- LC_MONETARY:影响货币格式
- LC_NUMERIC:影响printf()的数字格式
- LC_TIME:影响时间格式strftime()和wcsftime()
- LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境
3.2.3setlocale函数
char* setlocale(int category ,const char* locale);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项
setlocale的第一个参数可以是前说明的类项中的一个,修改一个类项,也可以是LC_ALL,影响所有的类项
C标准给第二个参数仅定义了两种可能取值:“C(正常模式)”和“(本地模式)”
在任意程序执行开始时都会隐式执行调用:
setlocale (LC_ALL, "C");
3.2.4宽字符
普通字符占一个字节,宽字符占两个字节
要在屏幕上打印宽字符,需要在字符的字面量前面加上“L”,否则C语言会将字面量当作窄字符类型来处理。前缀“L”在单引号前面,标识宽字符,对应wprintf()的占位符为"%lc",在双引号前面,表示宽字符串,对应wprintf()的占位符为"%ls"
实例:
wprintf(L"%lc", L'□');
wprintf(L"蛇");
3.2.5欢迎界面及介绍界面
首先设置一下控制台界面的大小,将控制台命名为贪吃蛇,隐藏光标
void GameStart(pSnake ps)
{
//设置窗口的大小、名称
system("mode con lines=40 cols=120");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
}
封装一个函数打印功能提醒和欢迎界面
void WelcomeToGame()
{
SetPos(50, 18);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(52, 38);
system("pause");
system("cls");
SetPos(40, 16);
wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");
SetPos(50, 18);
wprintf(L"加速能够得到更高的分数\n");
SetPos(52, 38);
system("pause");
system("cls");
}
3.2.6墙体绘制
通过CreateMap()函数创建一个地图
将光标分别定位在要打印墙体的位置,用wprintf()函数打印宽字符(注意算好坐标)
#define WALL L'□'
void CreatMap()
{
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
//wprintf(L"%lc", L'□');
}
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
}
3. 3蛇的数据结构设计
在游戏运行过程中,蛇每吃一个食物,蛇的身体就会变长一节,我们可以使用链表存储蛇的信息,蛇的每一节就是链表的一个节点。每个节点对应一个在屏幕上的坐标,通过打印节点来实现打印蛇
3.3.1蛇的节点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
3.3.2蛇的状态信息
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物的指针
enum DIRECTION _dir;//蛇的方向
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//休息时间
}Snake, * pSnake;
3.3.3蛇的方向
由于只有四个方向,可以一一列举,使用枚举类型
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
3.3.4游戏状态
同样的,游戏的状态也是可以一一列举的,使用枚举类型实现
enum GAME_STATUS
{
OK,//正常运行
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到自己
END_NORMAL//正常结束
};
3.3.5初始化蛇身
蛇身开始的长度可以自行设置,这里设置的是三节。
循环创建三个链表节点,将每个节点存放在链表中进行管理,创建完蛇身之后,将蛇的每一节打印在屏幕上。设置游戏的状态、蛇移动的速度、默认的方向、初试成绩、每个食物的分数。
- 游戏状态:OK
- 蛇的移动速度:200毫秒
- 蛇的默认方向
- 初试成绩:0
- 每个食物的分数:10
#define BODY L'●'
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 3; i++)
{
//创建蛇身节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc");
return;
}
//设置坐标
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
//打印蛇的身体
cur = ps->_pSnake;
while (cur != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒
ps->_status = OK;
}
3.4蛇吃食物的功能
3.4.1创建食物
随机生成食物的坐标,要注意,x坐标必须是2的倍数,食物的坐标不能和蛇身的节点坐标重合
#define FOOD L'★'
使用rand()函数,生成一个食物的坐标,遍历蛇身链表,确保食物的坐标不与蛇身的坐标重合,将光标定位到食物坐标处,打印食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//随机生成一个食物的坐标
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;
//保证食物的坐标不与蛇身重合
while (cur != NULL)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("malloc");
return;
}
pFood->x = x;
pFood->y = y;
//pFood->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
3.4.2蛇身移动
我们创建的蛇身链表,只需要根据移动方向和蛇头的坐标,头插一个新的节点,释放尾节点,就可以实现蛇身移动。
首先创建下一个位置,判断该位置上是否是食物,如果是就吃掉,不是就前进一步
蛇身移动后,需要判断此次移动是否会造成撞墙或者撞上自己蛇身,从而影响游戏的状态
3.4.2.1判断下一个位置是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
3.4.2.2是食物——吃掉食物
如果该节点是食物,吃掉食物之后蛇身需要增长一节,对应的得分增加,食物消失(在食物的位置上打印空格),重新生成食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法创建新的节点
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
free(pn);
pn = NULL;
//打印蛇
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建新的食物
CreateFood(ps);
}
3.4.2.3不是食物——继续移动
将下一个节点插入蛇的身体,经之前蛇身的最后一个节点打印为空格,释放掉蛇身的最后一个节点
注意将最后一个节点释放掉之后,新的尾节点的next指针要置为空
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插蛇的节点
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印蛇
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
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;
}
3.4.2.4撞墙死亡
判断蛇头的坐标是否和墙的坐标冲突
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->y == 0 ||
ps->_pSnake->x == 56 || ps->_pSnake->y == 26)
{
ps->_status = KILL_BY_WALL;
}
3.4.2.5撞到自己身体死亡
判断蛇头坐标是否和蛇身体坐标冲突
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur != NULL)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
3.4.3蛇身移动完整实现
void SnakeMove(pSnake ps)
{
//创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("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 (ChargeFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
3.5暂停游戏
当检测到按空格键后,暂停游戏
void Pause()
{
while (1)
{
if (KEY_PRESS(VK_SPACE))
{
break;
}
Sleep(200);
}
}
四、游戏模块
4.1游戏流程设计
4.2游戏主逻辑
程序开始就设置程序支持本地模式,然后进入游戏的主逻辑
主逻辑分为三个过程
- 游戏开始(GameStart)完成游戏的初始化
- 游戏运行(GameRun)完成游戏运行逻辑的实现
- 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
4.2.1GameStart
该模块完成游戏的初始化任务
- 控制台窗口大小的设置
- 控制台窗口名字的设置
- 鼠标光标的隐藏
- 打印欢迎界面
- 创建地图
- 初始化蛇
- 创建第一个食物
void GameStart(pSnake ps)
{
//设置窗口的大小、名称
system("mode con lines=40 cols=120");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
//打印环境界面和功能介绍
WelcomeToGame();
//打印地图
CreatMap();
//创建蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
//设置游戏的相关信息
}
4.2.2GameRun
游戏运行期间,右侧打印帮助信息提示玩家
根据游戏状态检查游戏是否结束,如果状态是OK,游戏继续,否则游戏结束
如果游戏继续,需要检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏
4.2.2.1需要的虚拟按键
- 上: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)
4.2.2.2打印帮助信息
在界面右侧显示游戏规则、得分情况、按键信息等
void PrintHelpInfo()
{
SetPos(64, 2);
printf("按空格键开始");
SetPos(64,4);
printf("不能穿墙,不能咬到自己");
SetPos(64, 6);
printf("用↑.↓.←.→来控制蛇的移动");
SetPos(64, 8);
printf("按F3加速,F4减速");
SetPos(64, 10);
printf("按ESC退出游戏,按空格暂停游戏");
}
4.2.2.3具体代码实现
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 15);
printf("总分数:%2d\n", ps->_score);
SetPos(64, 17);
printf("当前食物的分值:%2d\n", ps->_food_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))
{
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
4.2.3GameEnd
游戏状态不再是OK时,告知游戏结束的原因,并且释放蛇身节点
void GameEnd(pSnake ps)
{
SetPos(20, 15);
switch (ps->_status)
{
case END_NORMAL:
printf("成功结束游戏\n");
break;
case KILL_BY_WALL:
printf("您撞到了墙上,游戏结束\n");
break;
case KILL_BY_SELF:
printf("您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
五、参考代码
5.1snake.c
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
#include "snake.h"
//定位光标
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
//欢迎界面
void WelcomeToGame()
{
SetPos(50, 18);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(52, 38);
system("pause");
system("cls");
SetPos(40, 16);
wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");
SetPos(50, 18);
wprintf(L"加速能够得到更高的分数\n");
SetPos(52, 38);
system("pause");
system("cls");
}
//打印地图
void CreatMap()
{
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
//wprintf(L"%lc", L'□');
}
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
}
//初始化蛇
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 3; i++)
{
//创建蛇身节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc");
return;
}
//设置坐标
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
//打印蛇的身体
cur = ps->_pSnake;
while (cur != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒
ps->_status = OK;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//随机生成一个食物的坐标
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;
//保证食物的坐标不与蛇身重合
while (cur != NULL)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("malloc");
return;
}
pFood->x = x;
pFood->y = y;
//pFood->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(64, 2);
printf("按空格键开始");
SetPos(64,4);
printf("不能穿墙,不能咬到自己");
SetPos(64, 6);
printf("用↑.↓.←.→来控制蛇的移动");
SetPos(64, 8);
printf("按F3加速,F4减速");
SetPos(64, 10);
printf("按ESC退出游戏,按空格暂停游戏");
}
//暂停游戏
void Pause()
{
while (1)
{
if (KEY_PRESS(VK_SPACE))
{
break;
}
Sleep(200);
}
}
//判断下一个坐标是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法创建新的节点
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
free(pn);
pn = NULL;
//打印蛇
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建新的食物
CreateFood(ps);
}
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插蛇的节点
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印蛇
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
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->y == 0 ||
ps->_pSnake->x == 56 || ps->_pSnake->y == 26)
{
ps->_status = KILL_BY_WALL;
}
}
//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur != NULL)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
//初始化游戏
void GameStart(pSnake ps)
{
//设置窗口的大小、名称
system("mode con lines=40 cols=120");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
//打印环境界面和功能介绍
WelcomeToGame();
//打印地图
CreatMap();
//创建蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
//设置游戏的相关信息
}
//蛇的移动
void SnakeMove(pSnake ps)
{
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("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 (ChargeFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
//运行游戏
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 15);
printf("总分数:%2d\n", ps->_score);
SetPos(64, 17);
printf("当前食物的分值:%2d\n", ps->_food_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))
{
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
//结束游戏
void GameEnd(pSnake ps)
{
SetPos(20, 15);
switch (ps->_status)
{
case END_NORMAL:
printf("成功结束游戏\n");
break;
case KILL_BY_WALL:
printf("您撞到了墙上,游戏结束\n");
break;
case KILL_BY_SELF:
printf("您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
5.2snake.h
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include <windows.h>
#include <stdio.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
enum GAME_STATUS
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORMAL
};
//蛇身的节点类型
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 _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//休息时间
}Snake, * pSnake;
//初始化游戏
void GameStart(pSnake ps);
//定位光标
void SetPos(short x, short y);
//欢迎界面
void WelcomeToGame();
//打印地图
void CreatMap();
//创建蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//运行游戏
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//暂停游戏
void Pause();
//蛇的移动
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int ChargeFood(pSnakeNode pn, pSnake ps);
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);
//检测蛇是否撞墙
void KillByWall(pSnake ps);
//检测蛇是否撞到自己
void KillBySelf(pSnake ps);
//结束游戏
void GameEnd(pSnake ps);
5.3test.c
#include "snake.h"
void game()
{
char ch = 0;
srand((unsigned)time(NULL));
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
GameStart(&snake);
//运行游戏
GameRun(&snake);
///结束游戏
GameEnd(&snake);
SetPos(20, 13);
printf("再来一局吗?(Y/N):");
ch = getchar();
while(getchar()!='\n');
} while (ch == 'Y' || ch == 'y');
SetPos(1, 35);
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
game();
return 0;
}