目录
我以过客之名,祝你前程似锦
一.地图的设计
在详细讲贪吃蛇的游戏设计之前,先说一下这里有些特别需要注意的几个点:
1.字符与坐标:
(1)控制台窗口的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。 控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓
(2)关于字符:在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节
//这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识(粗略看看就行),过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是但自己的。但是这些假定并不是在世界的任何地⽅都适⽤。C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字母上方有注⾳符号,它就⽆法⽤ASCII码表示。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符 号。如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样,如130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel(后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。比如:加⼊和宽字符的类型 wchar_t 和宽字符的输入和输出函数,加头文件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数
2.<local.h>本地化(头文件):
提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准可以中,依赖地区的部分有以下⼏项:
• 数字量的格式
• 货币量的格式
• 字符集
• 日期和时间的表示形式
3.类项:
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项:
• LC_COLLATE:影响字符串比较函数:strcoll()和strxfrm()
• LC_CTYPE:影响字符处理函数的行为
• LC_MONETARY:影响货币格式
• LC_NUMERIC:影响printf()的数字格式
• LC_TIME:影响时间格式strftime()和wcsftime()
• LC_ALL 针对所有类项修改,将以上所有类型设置为给定的语言环境
4.setlocale函数:
(1)函数原型:
char* setlocale (int category, const char* locale);
(2)使用:
setlocal就是用来修改当地区域的类项,仅仅修改一个时就使用上述对应的参数就行,但也可以一次性修改全部,也就是对应上述类项分类的最后一个LC_ALL
同时,这个函数的第二个取值有两种情况:“C“和“ ”:
而当你如果想用setlocal来查询当前地区的默认设置,将第二个参数设置为NULL就可以了:
#include<stdio.h> #include<locale.h> int main() { char* local; local = setlocale(LC_ALL, NULL); printf("默认的本地设置是:%s\n", local); local = setlocale(LC_ALL, ""); printf("设置后的本地设置是:%s\n", local); return 0; }
5.宽字符的打印:
(1)宽字符是什么:
窄字符(char):通常使用8位(1字节)来表示一个字符,主要用于表示拉丁语系的字符,如英文、西班牙语、法语等,窄字符可以表示ASCII码表中的256个字符
宽字符(wchar_t):通常使用16位或32位来表示一个字符,主要用于表示非拉丁语系的字符,如中文、日文、韩文等,宽字符可以表示更多的字符,支持国际化
(2)打印:
如果要打印宽字符的话,那宽字符的字面量必须加上前缀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"%c\n", ch1);
wprintf(L"%c\n", ch2);
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}
从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置,但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标:
6.地图的实现和打印:
(1)具体代码与预期实现效果:
//打印地图
void CreatMap()
{
//上
for (int i = 0; i < 57; i += 2)
{
wprintf(L"%lc", L'□');
}
//下
SetPos(0, 26);
for (int i = 0; i < 57; i += 2)
{
wprintf(L"%lc", L'□');
}
//左
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}
//右
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", L'□');
}
}
打印效果:
(2)解释:
这里需要注意的有三点:
一是我这里对使用宽字符的使用的解释:主要还是因为宽字符可以更好的支持多种语言的字符显示,特别是对于那些需要多个字节来表示的字符(如汉字,英文等),宽字符可以更好的保障他们的显示而且宽字符虽然会占用更大的内存,但他同时可以减少内存碎片,提高内存的使用效率
二则是对这里打印边框的方式的解释:即通过设置光标的出现位置(这一点非常重要,设置光标的函数我也附在下面,上面有些解释可以照着看看)然后加以循环实现(即SetPos函数)
//定位光标位置 void SetPos(short x, short y) { COORD pos = { x, y }; //COORD表示一种结构体类型,专门用来表示光标的存储位置,属于Win32 API的内容 HANDLE houtput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) houtput = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(houtput, pos); }
三则是横着需要设置变量范围达到58而竖着要打印28次,主要还是因为光标的形状问题,我附一下这个图应该会好理解一些:
二.游戏逻辑与大体框架
1.游戏逻辑与大体框架:
2.游戏初步实现(GameStart函数):
(1)欢迎界面与地图的打印:
在上述的介绍中,我们已经初步实现了创建地图的操作,也就是我们的CreateMap,其实关于设置游戏窗口大小和设置窗口名字的操作我已经在Win32 API详解里写过了,所以这里就简单的展示一下代码和注释,也就不再过多赘述了,同样打印欢迎界面的操作也跟上述定义地图的操作使出同门,即先设置光标的位置然后打印出自己想要的句子就行:
void GameStart(pSnake ps)
{
//0.光标隐藏,窗口大小设置
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//光标隐藏
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得键盘句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
CursorInfo.dwSize = 25;
SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
//1.打印环境界面与功能介绍
WelcomeToGame();
//2.绘制地图
CreatMap();
//3.创建蛇
InitSnake(ps);
//4.创建食物
CreatFood(ps);
}
//打印欢迎界面
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"加速可以得到更高的分数\n");//两次设置光标位置打印提示
system("pause");
SetPos(42, 20);
system("cls");
system("pause");
}
实现效果:
(2)蛇身与食物的介绍:
效果展示:
初始化状态,假设蛇的长度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现 蛇(连续5个节点),注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。 关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然 后打印★
效果展示就像这个样子(当然蛇头和食物的坐标都是随机出现的,且不能发生重叠而且必须保证范围,即必须出现在墙体之内),下面咱来仔细说一下代码:)
3.蛇身的初始化:
蛇身的初始化我大体分为两步,一个当然是各种结构体的创建,另外一个就是以链表为基础的对各个结点(即蛇身的链接)
(1)结构体的创建:
//蛇的结点
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个结点的指针
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;//对这个结构体指针进行重命名为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;
这里首先有两种结构体:一种是为了定位蛇的位置,另外一个则是包含了所有与蛇有关的相关变量,其中在第二种结构体中也设置有第一种结构体类型的变量,其他的变量的枚举,定义和相关头文件的引用我也附在下面(这些结构体的创建,枚举的创建等都是放在自己另外创建的头文件里,我这里先称呼它为Snake.h):
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<locale.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>//true和false
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的方向
enum DIRECTION
{
UP,
DOWN,
LEFT,
RIGHT,
};
//蛇的状态
//正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
OK,
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞自己
END_NORMAL,//正常退出
};
(2)蛇的初始化(主要是结点间的链接):
//初始化蛇基本信息
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("TnitSnake()failed");
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)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
//初始化贪吃蛇的其他属性
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒
ps->_status = OK;
}
4.食物的创建与初始化:
在具体介绍详细代码之前,我感觉有些代码底层的逻辑得先说明一下(也许可以帮助你的理解):首先就是关于我为什么会在蛇的结构体(Snake)里同时将食物的相关变量也放在里面并且将其一并初始化,主要就是在代码的最底层,也就是剥离掉食物和蛇的外形,仅仅将其看作一堆数据时你就会发现我们这里写的无论是食物还是蛇它们其实都是在我们界面上随机生成的两个不同的坐标而已,而我们所谓的蛇吃食物也只不过是代码之间的相互判断,因此,本质上,蛇和食物根本没有区别,只不过是运行的方式不同罢了(这也是我在下面创建食物的代码函数里穿的是pSnake指针的原因)
//创建食物
void CreatFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x是2的倍数
//x:2到54
//y:1到25
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 2;
} while (x % 2 != 0);
//x和y的坐标不能和蛇的身体出现冲突
pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
//创建食物的结点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()failed");
return;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);这里的FOOD我在头文件里定义过,其实打印的是食物对应的特殊的符号,我为了图省事就在头文件里定义了一下
ps->_pFood = pFood;
}
三.游戏运行逻辑(GameRun函数)的具体实现
1.打印右侧提示信息( PrintHelpInfo函数):
经过初始界面的讲解,这里对提示信息的打印应该也很好理解了,无非就是那两步:设置光标位置和打印需要的文字:
void PrintHelpInfo() { SetPos(64, 14); wprintf(L"%ls", L"不能穿墙,不能咬到自己"); SetPos(64, 15); wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动"); SetPos(64, 16); wprintf(L"%ls", L"按F3加速,F4减速"); SetPos(64, 17); wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏"); SetPos(64, 18); wprintf(L"%ls", L"梅茜Mercy制作"); }
实现效果:
2.游戏状态的实现:
这一步是这整个游戏最重要也是最复杂的一步,它涉及着游戏每一个瞬时状态的数据,并且需要对下一步的反应进行预测和调整,在具体实现的过程中,为了使思路更加清晰,我这里暂且将这个游戏状态的实现分为两步:
一是食物的存在与销毁的分析
二则是对蛇走一步的过程的具体分析
(1)食物的分析:
仔细考虑我们会发现在与食物有关的问题中存在以下几种情况:
食物的创建(CreatFood)在上面我们已经实现过,
蛇下一步吃到食物(EatFood),
蛇下一步没吃到食物(NoFood),
而在以上行为存在的前提还有一个就是对下一步是否是食物的判断(NextIsFood),
这样我们实现与食物有关的函数的思路就清晰多了,下面是具体代码和一些注意点:
//判断下一个坐标是否是食物 int NextIsFood(pSnakeNode pn, pSnake ps) { if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y) return 1; else return 0; } //下一个位置是食物,就吃掉食物 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; //重新创建食物 CreatFood(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); //把倒数第二个节点的地址置为NULL cur->next = NULL; }
(2)蛇每走一步的分析:
这里对蛇的分析为主要分为两点,
一是对蛇下一步走向的控制:
这里就运用到了Win32 API里对按键状态的判断和使用了,具体可以参照Win32 API详解里的内容
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)//按键状态的宏 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; } }
二则是对蛇下一个结点的属性判断:
蛇本质上作为一个动态的链表,每一次的移动都意味着头结点的创建和尾结点的销毁(蛇没吃到食物时的状态),同时伴随着对下一个结点是否是食物或自身或墙的判断
//蛇走一步的过程 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 { NoFood(pNextNode, ps); } //检测蛇是否撞墙 KillByWall(ps); //检测蛇是否撞到自己 KillBySelf(ps); } //检测蛇是否撞到自己 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; } } //检测蛇是否撞墙 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; } }
(3)汇总:
这里其实有一点我没有着重强调就是我过程中对分数的记录:加速加分数,减速减分数,吃到食物加分数,这些依据个人喜好来看,实现方法也很容易,通过->访问对应的结构体成员就行
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 10);
printf("当前食物的分数:%d\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);
}
这里还要补充的一点就是对sleep()函数的使用:
#include <windows.h> #include <stdio.h> int main() { printf("Starting...\n"); // 休眠5000毫秒(5秒) Sleep(5000); printf("Ended after 5 seconds.\n"); return 0; } //在Windows系统上,使用Sleep函数,其接受的参数是毫秒数(1秒=1000毫秒)
四.游戏收尾(GameEnd函数)
这一块的代码相比就要轻松不少,一共也分为两部分:
结束提示和释放空间:
(但这里的结束也要分成几种情况讨论)
//游戏善后的工作 void GameEnd(pSnake ps) { SetPos(24, 12); switch (ps->_status) { case END_NORMAL: wprintf(L"您主动结束游戏\n"); break; case KILL_BY_WALL: wprintf(L"您撞到墙上,游戏结束\n"); break; case KILL_BY_SELF: wprintf(L"您撞到了自己,游戏结束\n"); break; //释放蛇身的链表 pSnakeNode cur = ps->_pSnake; while (cur) { pSnakeNode del = cur; cur = cur->next; free(del); } }
这样咱的贪吃蛇游戏也就到此为止,很高兴你可以一直阅读到这里,接下来我把源码放在这儿,要的话直接那就行,希望可以对你有些帮助:
text.c
#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"
//完成游戏的测试逻辑
void test()
{
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1. 打印环境界面
//2. 功能介绍
//3. 绘制地图
//4. 创建蛇
//5. 创建食物
//6. 设置游戏的相关信息
GameStart(&snake);
//运行游戏
//GameRun();
//结束游戏 - 善后工作
//GameEend();
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1. 打印环境界面
//2. 功能介绍
//3. 绘制地图
//4. 创建蛇
//5. 创建食物
//6. 设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏 - 善后工作
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
Snake.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<locale.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>//true和false
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的方向
enum DIRECTION
{
UP,
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;//对这个结构体指针进行重命名为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 WelcomeToGame();
//初始化蛇
void InitSnake(pSnake ps);
//绘制地图
void CreatMap(ps);
//创建食物
void CreatFood(ps);
//运行游戏
void GameRun(ps);
//蛇走一步的过程
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int NextIsFood(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);
Snake.c
#include"snake.h"
//定位光标位置
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
//创建食物
void CreatFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x是2的倍数
//x:2到54
//y:1到25
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 2;
} while (x % 2 != 0);
//x和y的坐标不能和蛇的身体出现冲突
pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
//创建食物的结点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()failed");
return;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
//打印欢迎界面
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"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause");
system("cls");
}
//打印地图
void CreatMap()
{
//上
for (int i = 0; i < 57; i += 2)
{
wprintf(L"%lc", L'□');
}
//下
SetPos(0, 26);
for (int i = 0; i < 57; i += 2)
{
wprintf(L"%lc", L'□');
}
//左
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}
//右
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", L'□');
}
}
//初始化蛇基本信息
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("TnitSnake()failed");
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)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", L'●');
cur = cur->next;
}
//初始化贪吃蛇的其他属性
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒
ps->_status = OK;
}
//游戏善后的工作
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status)
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
//检测蛇是否撞到自己
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;
}
}
//检测蛇是否撞墙
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 GameStart(pSnake ps)
{
//0.光标隐藏,窗口大小设置
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//光标隐藏
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得键盘句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
CursorInfo.dwSize = 25;
SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
//1.打印环境界面与功能介绍
WelcomeToGame();
//2.绘制地图
CreatMap();
//3.创建蛇
InitSnake(ps);
//4.创建食物
CreatFood(ps);
}
void PrintHelpInfo()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
SetPos(64, 18);
wprintf(L"%ls", L"梅茜Mercy制作");
}
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)//按键状态的宏
//下一个位置不是食物
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);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
return 1;
else
return 0;
}
//下一个位置是食物,就吃掉食物
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;
//重新创建食物
CreatFood(ps);
}
//蛇走一步的过程
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
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
//暂停游戏
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 10);
printf("当前食物的分数:%d\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);
}
全文终