游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。 在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。
游戏效果演示
实现基本的功能
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
技术要点
C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等
Win32 API介绍
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便 称之为ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows 32位平台的应⽤程序编程接⼝。
由于Win32API内容过多,本篇在此不过多介绍,有兴趣的读者可之后自行去查找,本篇只介绍贪吃蛇需要用到的部分函数。
控制台屏幕上的坐标COORD
COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
控制台屏幕坐标如下图所示:
COORD的作用通俗来说就是改变在控制台屏幕上的光标位置。
使用方法
COORD pos = { x,y };//设置光标位置
但要成功改变光标的位置,我们首先要获取控制台屏幕的句柄。
获取句柄GetStdHandle
句柄的类型名为HANDLE,使用方法为
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台
总而言之,获取句柄就能改变控制台屏幕的信息。
改变光标位置SetConsoleCursorPosition
当使用COORD设置完光标信息后,必须使用SetConsoleCursorPosition函数将COORD的信息存放进句柄中才能正确生效。
SetConsoleCursorPosition(put, pos);//改变光标位置
获取光标信息函数CONSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO cursur = { 0 };
创建一个包含光标信息的结构体变量。
改变光标信息GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息
光标信息生效函数SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
在改变完光标信息之后,必须调用此函数才可生效。
获取按键信息函数GetAsyncKeyState
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。
此时我们可以设定一个宏来方便检测按键信息。
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
正篇
此时我们就可以正式进入代码环节
头文件
#include<stdio.h>
#include<locale.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>
贪吃蛇所需的头文件。
图形定义
这里我们为了方便使用,用宏来简化一些符号
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
蛇身状态和蛇身结点结构体
typedef struct snakenode//蛇身结点
{
//蛇身坐标
int x;
int y;
//指向下一个结点的指针
struct snakenode* next;
}snake;
typedef struct SNAKE//贪吃蛇
{
snake* phead;//指向蛇头的指针
snake* food;//指向食物的指针
enum direction dir;//蛇的方向
enum STATUS status;//蛇的状态
int onescore;//单个食物的得分
int score;//总分
int sleeptime;//休息时间,时间越短,速度越快,时间越长,速度越慢
}sna;
我们创建分别创建两个关于蛇身结点和蛇身状态的结构体,方便进行链表的插入和修改。
设定光标函数
void GB(short x, short y)//改变光标位置函数
{
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台
//改变光标位置,改完光标位置调用函数才能生效
COORD pos = { x,y };//设置光标位置
SetConsoleCursorPosition(put, pos);//改变光标位置
}
我们通过获取句柄来操作控制台屏幕,然后创建一个包含光标信息的结构体,将x和y传入进结构体中,最后再通过SetConsoleCursorPosition函数将pos结构体光标的信息传入句柄之中从而改变光标的位置。
打印
void menuone()//初始界面
{
GB(40, 25);
wprintf(L"欢迎来到贪吃蛇小游戏");
GB(40, 28);
system("pause");//暂停程序
system("cls");//清理屏幕
GB(30, 25);
printf("用箭头来控制上下左右,按F3加速,按F4减速");
GB(40, 28);
printf("加速能拿到更高分");
GB(40, 32);
system("pause");//暂停程序
system("cls");
-
显示欢迎信息:首先,使用
GB(40, 25);
将光标移动到指定位置(第25行,第40列),然后用wprintf(L"欢迎来到贪吃蛇小游戏");
显示欢迎语句。 -
程序暂停:
system("pause");
执行后,程序将会等待用户按任意键继续。 -
清屏:
system("cls");
命令被执行后,控制台屏幕上的现有内容将会被清除。 -
显示游戏说明:然后,函数再次通过
GB
函数移动光标,使用printf
在不同的行上打印出游戏的操作说明,如使用箭头键控制蛇的移动,以及按F3加速、按F4减速的信息,和提醒玩家加速可以获得更高的分数。 -
再次暂停与清屏:之后,使用另一个
system("pause");
让玩家有机会阅读这些说明。等待玩家按任意键后,最后使用system("cls");
再次清屏,为游戏的开始做准备。
void printgame()//打印游戏中的提示
{
GB(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
GB(64, 15);
wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
GB(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
GB(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
GB(64, 18);
wprintf(L"%ls", L"制作");
}
在游戏进行时可在控制台的右侧显示操作信息
初始化
void gamestart()//初始化
{
system("mode con cols=100 lines=50");//设置行和列
system("title 贪吃蛇");//命名
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄,可修改控制台信息
CONSOLE_CURSOR_INFO cursur = { 0 };//创建变量,创建一个包含光标信息的结构体变量、
GetConsoleCursorInfo(put, &cursur);//将光标信息放进cursur,获取和put相关的控制台光标信息,存放进cursur
//隐藏光标操作
cursur.bVisible = false;//将光标可见度设为0
SetConsoleCursorInfo(put, &cursur);//设置光标信息,改完光标信息调用函数才能生效
menuone();
map();
}
-
设置控制台大小:通过
system("mode con cols=100 lines=50");
命令来设置控制台窗口的列数和行数,这里设置的是宽度100字符,高度50行。 -
设置控制台标题:使用
system("title 贪吃蛇");
命令将控制台窗口的标题命名为“贪吃蛇”。 -
获取控制台句柄:
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);
这行代码获取标准输出设备的句柄,以便后续修改控制台信息。 -
初始化控制台光标信息:定义
CONSOLE_CURSOR_INFO
结构体cursur
变量,并通过GetConsoleCursorInfo(put, &cursur);
函数获取当前控制台光标的信息。 -
隐藏控制台光标:将
cursur.bVisible
设置为false
,这样光标在控制台就不可见了。然后通过SetConsoleCursorInfo(put, &cursur);
应用这个设置。 -
显示初始菜单:调用
menuone();
函数向玩家展示游戏的欢迎界面和操作说明。 -
绘制游戏地图:最终调用
map();
函数来绘制游戏中使用的地图。
地图
#define WALL L'□'
void map()//地图
{
int i = 0;
for (i = 0; i < 29; i++)//上
{
wprintf(L"%lc", WALL);
}
GB(0, 26);
for (i = 0; i < 29; i++)//下
{
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)//左
{
GB(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)//右
{
GB(56, i);
wprintf(L"%lc", WALL);
}
}
我们打印墙体是用到的是宽字符,这样我们的地图形状设计才能更加美观,由于普通的字符占用的都是一个字节,打印出的墙体为长方形,此时我们就要用到宽字符打印。而在打印右边的下面的墙体时,我们通过GB函数改变光标的位置,随后再打印墙体。
普通字符和宽字符打印出宽度的展⽰如下:
蛇身
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24,5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中, 另外⼀般在墙外的现象,坐标不好对⻬。
void ini(sna* pa)//初始化贪吃蛇
{
int i = 0;
snake* new = NULL;
for (i = 0; i < 5; i++)//创建蛇身
{
new = (snake*)malloc(sizeof(snake));//申请空间
if (new == NULL)
{
perror("huang");
return;
}
new->next = NULL;
new->x = 24 + 2 * i;
new->y = 5;
//头插法
if (pa->phead == NULL)//空链表
{
pa->phead = new;
}
else//非空链表
{
new->next = pa->phead;
pa->phead = new;
}
}
-
创建蛇身:在一个
for
循环中,函数连续创建蛇的身体部分。循环变量i
从 0 开始,它决定了每个蛇身部分在水平方向上的位置。蛇的初始长度被设定为 5 个部分。 -
申请空间:对于蛇的每个部分,函数通过调用
malloc
为snake
结构分配内存。如果内存分配失败,函数会打印错误消息并返回。 -
头插法:新创建的蛇身节点通过头插法插入到链表中。头插法是指新的节点始终插入链表的开头,这样新节点就成了新的链表头(
pa->phead
)。对于第一个节点,由于链表是空的,新节点直接成为链表头。对于后续的节点,它们被插入链表的前端。这样,最后创建的节点成为链表(即蛇的身体)的第一个节点,从而确定了蛇头的位置。 -
设置初始坐标:每个新创建的蛇身节点的
x
坐标依次增加,y
坐标保持不变。这确保了蛇初始时呈直线排列。 -
绘制蛇身:创建和插入完所有蛇身节点后,函数遍历链表并使用
GB
函数和wprintf
为每个蛇身节点在游戏界面上绘制对应的符号。 -
设置蛇的初始状态:蛇的初始移动方向被设定为向右(
RIGHT
)。onescore
(吃到食物后增加的得分)、score
(总分)、sleeptime
(控制游戏速度的参数)、status
(蛇的状态,比如正常、碰撞等)等都被初始化为特定的值。
食物
void food(sna* pa)//生成食物
{
int x = 0;
int y = 0;
again:
do//生成食物,x是2的倍数,如果x不是2的倍数,食物有可能卡住
{
x = rand() % 53 + 2;//2-54
y = rand() % 25 + 1;//1-25
} while (x % 2 != 0);
snake* new = pa->phead;
while (new)
{
if (x == new->x && y == new->y)//如果食物与蛇身重合
{
goto again;
}
new = new->next;
}
snake* food = (snake*)malloc(sizeof(snake));
if (food == NULL)
{
perror("food malloc");
return;
}
food->x = x;
food->y = y;
food->next = NULL;
GB(x, y);
wprintf(L"%lc", FOOD);
pa->food = food;
}
- 初始化两个整数
x
和y
用来表示食物在游戏界面上的位置。 - 通过循环生成食物的坐标位置,
x
值在2到54之间,y
值在1到25之间。这里特意限制x
为2的倍数否则食物可能会“卡住”。循环确保x
是2的倍数。 - 使用一个
while
循环遍历蛇的每个节点以检查食物是否生成在蛇的身体上。如果发现食物的位置与蛇身的任何一部分重合,就跳回到标签again
,重新生成食物位置。 - 一旦确定了食物的位置不与蛇身重合,就分配一个新的
snake
节点用来表示食物,并将其位置设置为前面生成的x
和y
。 - 如果
malloc
调用失败,函数会打印错误信息并返回。 - 使用函数
GB(x, y)
可能会对游戏界面进行更新(该函数没有在代码片段中定义,但通常用来设置指定位置的属性或输出)。然后通过wprintf
在对应的位置输出一个字符,这个字符由FOOD
宏定义表示,可能是代表食物的特定图案或符号。 - 最后,将刚分配的食物节点放入
sna
结构中,表示食物已被生成并放置在游戏中。
按键识别
这里我们先用一个宏定义来方便检测返回值
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
然后我们使用枚举来列出蛇身方向的状态
enum STATUS//蛇身状态
{
OK,//正常
KILLWALL,//撞到墙
KILLI,//撞到自己
EXIT,//正常退出
};
判断蛇身位置
判断下一个结点是否是食物
int nextfood(snake* pa, sna* pb)//判断下一个坐标是不是食物
{
if (pb->food->x == pa->x && pb->food->y == pa->y)
{
return 1;
}
else
{
return 0;
}
}
- 函数首先比较
pb->food->x
(食物的x坐标)和pa->x
(蛇头的x坐标),以及pb->food->y
(食物的y坐标)和pa->y
(蛇头的y坐标)是否相同。 - 如果这些坐标相同,说明蛇的下一个坐标就是食物的位置,函数返回1,表示下一个坐标是食物。
- 如果这些坐标不同,函数返回0,表示下一个坐标不是食物。
下一个位置是食物的情况
void eatfood(snake* pa, sna* pb)//下一个位置是食物
{
//头插
pb->food->next = pb->phead;
pb->phead = pb->food;
//释放下一个位置的结点
free(pa);
pa = NULL;
//打印蛇
snake* new = pb->phead;
while (new)
{
GB(new->x, new->y);
wprintf(L"%lc", BODY);
new = new->next;
}
pb->score += pb->onescore;
//重新创建食物
food(pb);
}
- 将食物变成蛇的新头部(头插法),表示蛇吃掉了这个食物,并因此增长。
- 更新游戏得分,每次吃掉食物时,蛇的得分会增加一个固定的分值(
pb->onescore
)。 - 在蛇吃掉食物后,重新生成食物的位置,保持游戏持续进行。
下一个结点不是食物的情况
void nofood(snake* pa, sna* pb)//下一个结点不是食物
{
//头插
pa->next = pb->phead;
pb->phead = pa;
snake* new = pb->phead;
while (new->next->next != NULL)//打印到蛇身倒数第二个结点
{
GB(new->x, new->y);
wprintf(L"%lc", BODY);
new = new->next;
}
GB(new->next->x, new->next->y);//此时蛇身已经移动,将蛇身最后一个结点打印成空格
printf(" ");
free(new->next);
new->next = NULL;
}
-
头插法添加新节点:首先,函数通过将新位置
pa
(蛇头将要移动到的位置)通过头插法添加到蛇身的开头,以此表示蛇向该方向移动了一个单位。此时,pa
成为了蛇的新头部(pb->phead
)。 -
遍历蛇身体:接着,函数遍历蛇身链表直到倒数第二个节点。在这个过程中,函数调用
GB
函数,并用wprintf
在每个节点的位置上打印蛇身的字符(由宏BODY
定义)以在游戏界面上显示蛇的移动。 -
更新蛇尾部:在蛇向前移动之后,原本作为蛇尾的节点(即现在链表中的最后一个节点)需要被更新。首先,函数在这个节点的位置上打印空格字符(或者相当于清除该位置的显示),使得该位置在游戏界面上不再显示为蛇身的一部分。
-
移除蛇尾节点:最后,函数释放了原蛇尾的节点(即现在的最后一个节点),并将相应的指针设置为
NULL
,从而减少蛇身的长度。
蛇身撞墙的情况
void killwall(sna* pa)//蛇身撞墙
{
if (pa->phead->x <= 0 || pa->phead->x >= 56 || pa->phead->y <= 0 || pa->phead->y >= 26)
{
pa->status = KILLWALL;
}
}
- 检查蛇头的
x
坐标(水平位置)和y
坐标(垂直位置)。 - 若蛇头的
x
坐标小于等于0或者大于等于56,或者蛇头的y
坐标小于等于0或者大于等于26,说明蛇头已经触碰到了游戏边界,即蛇头撞墙。 - 如果检测到蛇头撞墙,函数将
pa->status
更新为KILLWALL
,表示蛇已经死亡。
蛇撞到自己的情况
void killmy(sna* pa)//蛇身撞到自己
{
snake* new = pa->phead->next;//指向蛇头的第二个位置,如果是第一个位置就会直接死亡
while (new)
{
if (new->x == pa->phead->x && new->y == pa->phead->y)
{
pa->status = KILLI;
break;
}
new = new->next;
}
}
-
初始化:函数开始通过设置一个指针
new
,该指针指向蛇头 (pa->phead
) 的下一个节点,也就是蛇身的第二个部分。注意:蛇头是蛇身的第一个部分,因此它的下一个节点是蛇身第二个部分。 -
循环检测:函数进入一个
while
循环,循环的条件是new
指针非空。在这个循环中,函数检查当前new
指向的蛇身部分的坐标 (x
,y
) 是否与蛇头的坐标相同。 -
撞击检测:如果蛇头的坐标和任何其他蛇身部分的坐标相同,那么就意味着蛇头撞到了自己的身体。此时,设置
pa->status
为KILLI
,也就是游戏中的"自杀"状态。 -
游戏状态更新:之后,函数跳出
while
循环。这时,游戏逻辑会检查pa->status
,如果是KILLI
,就可以处理游戏结束的逻辑。
判断蛇身的运动状态
void snakemove(sna* pa)//蛇的运动状态
{
snake* new = (snake*)malloc(sizeof(snake));//蛇的下一个位置
if (new == NULL)
{
perror("snakemove");
return;
}
switch (pa->dir)//推算蛇头的下一个位置
{
case UP://x,y-1
new->x = pa->phead->x;
new->y = pa->phead->y - 1;
break;
case DOWN://x,y+1
new->x = pa->phead->x;
new->y = pa->phead->y + 1;
break;
case LEFT://x-2,y
new->x = pa->phead->x - 2;
new->y = pa->phead->y;
break;
case RIGHT://x+2,y
new->x = pa->phead->x + 2;
new->y = pa->phead->y;
break;
}
if (nextfood(new, pa))//检查下一个坐标是否是食物
{
eatfood(new, pa);//下一个结点是食物
}
else
{
nofood(new, pa);//下一个结点不是食物
}
killwall(pa);
killmy(pa);
}
-
函数首先尝试分配一个新的
snake
类型的节点,用于蛇头的下一个位置。 -
如果内存分配失败,函数会打印出一个错误信息,并直接返回。
-
使用
switch
语句按照蛇当前的移动方向(存储在pa->dir
中)来计算蛇头的下一个位置。根据蛇的方向(上、下、左、右),更新新节点new
的x
和y
坐标。- 如果向上移动(
UP
),y
坐标减1。 - 如果向下移动(
DOWN
),y
坐标加1。 - 如果向左移动(
LEFT
),x
坐标减2(这里的减2可能是因为在游戏的显示界面中,蛇的每次移动可能是两个字符单位的宽度)。 - 如果向右移动(
RIGHT
),x
坐标加2。
- 如果向上移动(
-
调用
nextfood
函数来检查蛇的下一个位置是否是食物。如果是食物,调用eatfood
函数处理蛇吃食物的动作;如果不是食物,则调用nofood
函数让蛇继续移动而不增长。 -
函数还调用了
killwall
函数,用于检查蛇是否撞墙,以及调用killmy
函数检查蛇是否咬到自己。
暂停
void stope()//暂停
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))//再按一次空格取消暂停
{
break;
}
}
}
-
开始一个无限循环:
while(1)
使得函数会一直循环运行,直到明确的中断(即break
)语句出现。 -
休眠:
Sleep(200)
使得当前线程暂停 200 毫秒,这个延时操作让代码短暂休息,避免了持续无间断的运行造成的不必要的计算机资源消耗。 -
检测按键输入:如果玩家按下空格键
KEY_PRESS(VK_SPACE)
,在循环运行的过程中就会break
退出循环,从而结束函数的运行。换句话说,暂停状态会一直保持,直到玩家再次按下空格
游戏运行问题
void gamerun(sna* pa)//游戏运行问题
{
printgame();
do
{
GB(64, 10);
printf("总分数:%d\n", pa->score);
GB(64, 11);
printf("当前食物的分数:%2d\n", pa->onescore);//用2d保证食物的分数不会打印错误
if (KEY_PRESS(VK_UP) && pa->dir != DOWN)
{
pa->dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && pa->dir != UP)
{
pa->dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && pa->dir != RIGHT)
{
pa->dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && pa->dir != LEFT)
{
pa->dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
stope();
}
else if (KEY_PRESS(VK_ESCAPE))
{
pa->status = EXIT;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (pa->sleeptime > 30)//休眠时间大于80才能继续加速
{
pa->sleeptime = pa->sleeptime - 30;
pa->onescore += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (pa->onescore > 2)//分数大于2是才能继续减速
{
pa->sleeptime += 30;
pa->onescore -= 2;
}
}
snakemove(pa);//蛇运动的状态
Sleep(pa->sleeptime);//设置休眠时间
} while (pa->status == OK);//在状态为OK下运行
}
-
打印游戏界面:调用
printgame()
函数来显示游戏界面。 -
运行一个无限循环:使用
do...while
语句使游戏循环运行,直到游戏状态不再是 OK。 -
打印分数和当前食物分数:在指定位置显示总分数和当前食物的得分。
-
检测按键输入:
- 如果玩家按下上键且蛇当前不是向下移动,则改变蛇的方向为上。
- 如果玩家按下下键且蛇当前不是向上移动,则改变蛇的方向为下。
- 如果玩家按下左键且蛇当前不是向右移动,则改变蛇的方向为左。
- 如果玩家按下右键且蛇当前不是向左移动,则改变蛇的方向为右。
- 如果玩家按下空格键,则调用
stope()
函数,该函数用于暂停游戏。 - 如果玩家按下 escape 键,则将游戏状态设置为 EXIT,表示退出游戏。
- 如果玩家按下 F3 键,则游戏加速,休眠时间减少,食物分数增加。
- 如果玩家按下 F4 键,则游戏减速,休眠时间增加,食物分数减少。
-
移动蛇:调用
snakemove(pa)
函数来根据蛇的当前方向移动蛇。 -
延时:使用
Sleep()
函数根据pa->sleeptime
设置的休眠时间暂停游戏,这是为了控制游戏的速度。
源码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<locale.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
enum direction//枚举方向
{
UP = 1,
DOWN,
LEFT,
RIGHT,
};
void GB(short x, short y)//改变光标位置函数
{
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//标准输出设备(屏幕缓冲区),获得句柄,总之获得句柄就可以操作控制台
//改变光标位置,改完光标位置调用函数才能生效
COORD pos = { x,y };//设置光标位置
SetConsoleCursorPosition(put, pos);//改变光标位置
}
enum STATUS//蛇身状态
{
OK,//正常
KILLWALL,//撞到墙
KILLI,//撞到自己
EXIT,//正常退出
};
typedef struct snakenode//蛇身结点
{
//蛇身坐标
int x;
int y;
//指向下一个结点的指针
struct snakenode* next;
}snake;
typedef struct SNAKE//贪吃蛇
{
snake* phead;//指向蛇头的指针
snake* food;//指向食物的指针
enum direction dir;//蛇的方向
enum STATUS status;//蛇的状态
int onescore;//单个食物的得分
int score;//总分
int sleeptime;//休息时间,时间越短,速度越快,时间越长,速度越慢
}sna;
void ini(sna* pa)//初始化贪吃蛇
{
int i = 0;
snake* new = NULL;
for (i = 0; i < 5; i++)//创建蛇身
{
new = (snake*)malloc(sizeof(snake));//申请空间
if (new == NULL)
{
perror("huang");
return;
}
new->next = NULL;
new->x = 24 + 2 * i;
new->y = 5;
//头插法
if (pa->phead == NULL)//空链表
{
pa->phead = new;
}
else//非空链表
{
new->next = pa->phead;
pa->phead = new;
}
}
while (new)
{
GB(new->x, new->y);
wprintf(L"%lc", BODY);
new = new->next;
}
//蛇身的初始化
pa->dir = RIGHT;
pa->onescore = 4;
pa->score = 0;
pa->sleeptime = 200;
pa->status = OK;
}
void food(sna* pa)//生成食物
{
int x = 0;
int y = 0;
again:
do//生成食物,x是2的倍数,如果x不是2的倍数,食物有可能卡住
{
x = rand() % 53 + 2;//2-54
y = rand() % 25 + 1;//1-25
} while (x % 2 != 0);
snake* new = pa->phead;
while (new)
{
if (x == new->x && y == new->y)//如果食物与蛇身重合
{
goto again;
}
new = new->next;
}
snake* food = (snake*)malloc(sizeof(snake));
if (food == NULL)
{
perror("food malloc");
return;
}
food->x = x;
food->y = y;
food->next = NULL;
GB(x, y);
wprintf(L"%lc", FOOD);
pa->food = food;
}
void menuone()//初始界面
{
GB(40, 25);
wprintf(L"欢迎来到贪吃蛇小游戏");
GB(40, 28);
system("pause");//暂停程序
system("cls");//清理屏幕
GB(30, 25);
printf("用箭头来控制上下左右,按F3加速,按F4减速");
GB(40, 28);
printf("加速能拿到更高分");
GB(40, 32);
system("pause");//暂停程序
system("cls");
}
void map()//地图
{
int i = 0;
for (i = 0; i < 29; i++)//上
{
//printf("1");
//wprintf(L"%lc", L'▢');
wprintf(L"%lc", WALL);
}
GB(0, 26);
for (i = 0; i < 29; i++)//下
{
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)//左
{
GB(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i <= 25; i++)//右
{
GB(56, i);
wprintf(L"%lc", WALL);
}
}
void gamestart()//初始化
{
system("mode con cols=100 lines=50");//设置行和列
system("title 贪吃蛇");//命名
HANDLE put = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄,可修改控制台信息
CONSOLE_CURSOR_INFO cursur = { 0 };//创建变量,创建一个包含光标信息的结构体变量、
GetConsoleCursorInfo(put, &cursur);//将光标信息放进cursur,获取和put相关的控制台光标信息,存放进cursur
//隐藏光标操作
cursur.bVisible = false;//将光标可见度设为0
SetConsoleCursorInfo(put, &cursur);//设置光标信息,改完光标信息调用函数才能生效
menuone();
map();
}
void printgame()//打印游戏中的提示
{
GB(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
GB(64, 15);
wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
GB(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
GB(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
GB(64, 18);
wprintf(L"%ls", L"制作");
}
void stope()//暂停
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))//再按一次空格取消暂停
{
break;
}
}
}
int nextfood(snake* pa, sna* pb)//判断下一个坐标是不是食物
{
if (pb->food->x == pa->x && pb->food->y == pa->y)
{
return 1;
}
else
{
return 0;
}
}
void eatfood(snake* pa, sna* pb)//下一个位置是食物
{
//头插
pb->food->next = pb->phead;
pb->phead = pb->food;
//释放下一个位置的结点
free(pa);
pa = NULL;
//打印蛇
snake* new = pb->phead;
while (new)
{
GB(new->x, new->y);
wprintf(L"%lc", BODY);
new = new->next;
}
pb->score += pb->onescore;
//重新创建食物
food(pb);
}
void nofood(snake* pa, sna* pb)//下一个结点不是食物
{
//头插
pa->next = pb->phead;
pb->phead = pa;
snake* new = pb->phead;
while (new->next->next != NULL)//打印到蛇身倒数第二个结点
{
GB(new->x, new->y);
wprintf(L"%lc", BODY);
new = new->next;
}
GB(new->next->x, new->next->y);//此时蛇身已经移动,将蛇身最后一个结点打印成空格
printf(" ");
free(new->next);
new->next = NULL;
}
void killwall(sna* pa)//蛇身撞墙
{
if (pa->phead->x <= 0 || pa->phead->x >= 56 || pa->phead->y <= 0 || pa->phead->y >= 26)
{
pa->status = KILLWALL;
}
}
void killmy(sna* pa)//蛇身撞到自己
{
snake* new = pa->phead->next;//指向蛇头的第二个位置,如果是第一个位置就会直接死亡
while (new)
{
if (new->x == pa->phead->x && new->y == pa->phead->y)
{
pa->status = KILLI;
break;
}
new = new->next;
}
}
void snakemove(sna* pa)//蛇的运动状态
{
snake* new = (snake*)malloc(sizeof(snake));//蛇的下一个位置
if (new == NULL)
{
perror("snakemove");
return;
}
switch (pa->dir)//推算蛇头的下一个位置
{
case UP://x,y-1
new->x = pa->phead->x;
new->y = pa->phead->y - 1;
break;
case DOWN://x,y+1
new->x = pa->phead->x;
new->y = pa->phead->y + 1;
break;
case LEFT://x-2,y
new->x = pa->phead->x - 2;
new->y = pa->phead->y;
break;
case RIGHT://x+2,y
new->x = pa->phead->x + 2;
new->y = pa->phead->y;
break;
}
if (nextfood(new, pa))//检查下一个坐标是否是食物
{
eatfood(new, pa);//下一个结点是食物
}
else
{
nofood(new, pa);//下一个结点不是食物
}
killwall(pa);
killmy(pa);
}
void gamerun(sna* pa)//游戏运行问题
{
printgame();
do
{
GB(64, 10);
printf("总分数:%d\n", pa->score);
GB(64, 11);
printf("当前食物的分数:%2d\n", pa->onescore);//用2d保证食物的分数不会打印错误
if (KEY_PRESS(VK_UP) && pa->dir != DOWN)
{
pa->dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && pa->dir != UP)
{
pa->dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && pa->dir != RIGHT)
{
pa->dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && pa->dir != LEFT)
{
pa->dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
stope();
}
else if (KEY_PRESS(VK_ESCAPE))
{
pa->status = EXIT;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (pa->sleeptime > 30)//休眠时间大于80才能继续加速
{
pa->sleeptime = pa->sleeptime - 30;
pa->onescore += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (pa->onescore > 2)//分数大于2是才能继续减速
{
pa->sleeptime += 30;
pa->onescore -= 2;
}
}
snakemove(pa);//蛇运动的状态
Sleep(pa->sleeptime);//设置休眠时间
} while (pa->status == OK);//在状态为OK下运行
}
void gameend(sna* pa)//游戏结束工作
{
GB(30, 35);
switch (pa->status)
{
case EXIT:
printf("游戏正常结束");
break;
case KILLWALL:
printf("你撞到墙了,你死了");
break;
case KILLI:
printf("你撞到你自己了,你死了");
break;
}
//释放链表
snake* new = pa->phead;
while (new)
{
snake* del = new->next;
free(new);
new = del;
}
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");//打印宽字符
srand((unsigned int)time(NULL));
gamestart();
sna tou = { 0 };
ini(&tou);
food(&tou);
gamerun(&tou);
GB(50, 40);
system("pause");
gameend(&tou);
return 0;
}