1.游戏运行前的准备
1.1了解API 函数
API函数即Windows API,需要用到头文件windows.h.其中的函数服务于应用程序,所以在写贪吃蛇的时候,我们需要用到该函数来改变控制台的一些参数。
如把控制台大小改变为100列,30行,值得一提的是长度 1行=2列,打印一个字节站1行1列。
system("mode con cols=100 lines=30");
如改变控制台名称为贪吃蛇
system("title 贪吃蛇");
众所周知,打开控制台的运行结束后会显示一串,然后结尾处有一个光标。 
此时我们就要了解到句柄。句柄就像是一个开关,用来控制某个东西。
HANDLE GetStdHandle(DWORD nStdHandle);
为了关闭光标显示,就需要创建一个句柄。
HANDLE hOutput = NULL;
//获得标准输出的句柄
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
虽然我们有了句柄,但是还不知道光标怎么获取,此时需要用到一个结构体
//结构体类型名
//结构体包括有关控制台光标的信息
// DWORD dwSize;//控制光标占比
// BOOL bVisible;//控制光标是否可见
CONSOLE_CURSOR_INFO
//创建一个结构体变量
CONSOLE_CURSOR_INFO CursorInfo;
有开光和光标之后,我们该怎么讲两个东西联系起来呢,这时候又有一个函数.
//太长了分成几段
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,//句柄
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo//结构体指针
)
于是有以下操作,
//联系光标和句柄
GetConsoleCursorInfo(hOutput, &CursorInfo);
//关闭光标
CursorInfo.bVisible = false;
//更新数据
SetConsoleCursorInfo(hOutput, &CursorInfo);
有了这些之后就可以实现关闭光标显示啦!
我们再来认识一个结构体,
//(X,Y)为一个字符控制台缓冲区的坐标
//改变坐标就能改变光标的位置
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
所以我们可以封装一个函数,用于控制光标的指向位置
void SetPos(short x, short y)
{
//创建一个句柄
HANDLE hOutput = NULL;
//句柄连接上标准输出
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//创建一个结构体变量,赋值一个坐标,坐标为x,y
COORD pos = { x,y };
//应用
SetConsoleCursorPosition(hOutput, pos);
}
1.2了解locale.h
制作贪吃蛇地图的时候或许会出现□、●、◎等图形,但是我们在写c语言代码时碰见的字符基
本上都是单字节字符,而这些图形是双字节字符,所以我们不能通过简单的printf来打印这些图形,
而要通过多了一个w的printf来打印,此时就需要用到头文件locale.h中的函数.
//语言环境设置为本地语言环境
setlocale(LC_ALL, "");
//打印双字节字符
wprintf(L"%lc",L"□");
1.3设计地图
我们已经掌握了基本的打印地图的函数,那么我们正式开始吧!
游戏开始之前我们需要有一个欢迎界面,并且带有环境说明.
void InitPrint()
{
//设置窗口大小和窗口的标题
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//光标移到中间
SetPos(40, 15);
//打印欢迎标语
printf("欢迎来到贪吃蛇小游戏");
//设定一个新位置
SetPos(42, 26);
//进行一个暂停,且会出现文字说明
system("pause");
//清理屏幕
system("cls");
//此时到了下一个界面,在屏幕中间打印游戏玩法
SetPos(28, 15);
printf("用 ↑、↓、←、→控制蛇的移动, shift减速,ctrl加速\n");
SetPos(28, 16);
printf("加速可以得到更高的分数\n");
//如上操作,准备切换到地图
SetPos(42, 26);
system("pause");
system("cls");
}
效果如图(背景颜色可调,调成灰色差不多就看不清了)

接下来就是地图的绘制,
void InitGamePrint(pSnake ps)
{
//循环打印上行
for (int i = 0; i < 56; i+=2)
{
SetPos(i, 0);
wprintf(L"□");
}
//循环打印下行
for (int i = 0; i < 56; i += 2)
{
SetPos(i, 26);
wprintf(L"□");
}
//循环打印左列
for (int i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"□");
}
//循环打印右列
for (int i = 1; i < 26; i++)
{
SetPos(54, i);
wprintf(L"□");
}
//在界面左边加上提示信息
SetPos(60, 12);
printf("不能穿墙,不能吃自己\n");
SetPos(60, 13);
printf("用 ↑、↓、←、→控制蛇的移动,\n");
SetPos(60, 14);
printf(" shift减速,ctrl加速\n");
SetPos(60, 15);
printf("ESC: 退出游戏, space: 暂停退出游戏\n");
SetPos(60,17);
printf("Made in 璀璨");
}
效果如下,

1.4放置初始的蛇与食物
当我们制作好基础的界面之后,我们就该在游戏中放置蛇和食物了,那么在此之前我们需要做什么呢?
我们需要一个结构体来维护贪吃蛇这个游戏,对此,我们先来穷举一下有关蛇的元素.
typedef struct Snake
{
//蛇的方向
enum Direction _Dir;
//指向蛇头的指针
pSnakeNode _pSnake;
//指向食物的指针
pSnakeNode _pFood;
//食物的权重
int _FoodWeight;
//总分
int _Score;
//蛇的速度
int _Speed;
//蛇的状态
enum Game_Status _Status;
}Snake, * pSnake;
在蛇的方向和蛇的状态这一块由于情况的多样性,我们用到了枚举
enum Direction
{
UP=1,
DOWN,
LEFT,
RIGHT
};
enum Game_Status
{
OK,
//特指通过ESC结束游戏
END_NORMAL,
KILL_BY_WALL,
KILL_BY_SELF
};
此时就有人要问了pSnakeNode 是个什么类型,其实这是另一个结构体指针,也就是链表的指针,蛇身肯定要一个一个串起来的嘛!为什么食物也要用这个指针?因为蛇身放置在地图上和食物放置地图上实际上是一样的,只是借用了链表.
//创建一个链表,用于蛇身
typedef struct SnakeNode
{
struct SnakeNode* next;
int x;
int y;
}SnakeNode, * pSnakeNode;
好了,结构体的内容解释完了,该初始化贪吃蛇的数据了,于是有了入下函数
void InitSnake(pSnake ps)
{
//默认向右走
ps->_Dir = RIGHT;
ps->_FoodWeight = 10;
ps->_Score = 0;
//开始时状态良好
ps->_Status = OK;
ps->_Speed = 200;
}
但是咱缺的蛇头和食物这一块谁补啊!别急,下面就是
void CreateSnake(pSnake ps)
{
//给蛇头指针malloc一个空间
ps->_pSnake = (pSnakeNode)malloc(sizeof(SnakeNode));
if (ps->_pSnake == NULL)
{
perror("malloc failed");
return;
}
//设置蛇头的初始位置
ps->_pSnake->x = 30;
ps->_pSnake->y = 10;
ps->_pSnake->next = NULL;
//循环创建四个蛇节点
for (int i = 1; i < 5; i++)
{
pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (newnode == NULL)
{
perror("malloc failed");
return;
}
//因为双字节打印的缘故,x轴方向每个节点需要两个字节的空间
newnode->x = 20+2*i;
newnode->y = 10;
newnode->next = NULL;
//头插
newnode->next = ps->_pSnake->next;
ps->_pSnake->next = newnode;
}
}
蛇身都初始化好了,那么放置也就不难了,以下封装了一个函数
void PrintSnake(pSnake ps)
{
//保存蛇头指针
pSnakeNode ptail = ps->_pSnake;
//遍历链表打印
while (ptail != NULL)
{
SetPos(ptail->x, ptail->y);
//宽字符打印
wprintf(L"●");
ptail = ptail->next;
}
}
接下来食物的初始化是简单的,食物的位置就有点复杂了,
void CreateFood(pSnake ps)
{
//给食物指针开辟空间
ps->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (ps->_pFood == NULL)
{
perror("malloc failed");
return;
}
while (1)
{
again:
//通过随机数%运算的方式控制x,y的值都在边界之内
//需要用到time.h
//rand()使用时应在主函数加上srand((unsigned int)time(NULL))保证真随机
ps->_pFood->x = rand() % 51 + 2;
ps->_pFood->y = rand() % 25 + 1;
//以为宽字符的原因,必须保证食物的x坐标为偶数
if (ps->_pFood->x % 2 == 0)
{
//保存蛇头坐标
pSnakeNode ptail = ps->_pSnake;
//确保食物的创建不与蛇冲突
while (ptail != NULL)
{
if (ptail->x == ps->_pFood->x || ptail->y == ps->_pFood->y)
{
//如果冲突则回到上一个again进行循环
goto again;
}
ptail = ptail->next;
}
//如果while正常循环结束,说明x,y可取
break;
}
//%2!=0,循环继续
else continue;
}
//循环结束打印食物
SetPos(ps->_pFood->x, ps->_pFood->y);
wprintf(L"◎");
}
食物放置好后说明游戏的前期准备已经好了,先来看看目前的效果如何

2.游戏运行时的维护
2.1读取键盘按键
我们开始游戏的第一件事就是按下键盘上的按键,来控制蛇的方向,那么怎么做到呢?
此时我们需要一个能够读取键盘的函数GetAsyncKeyState(),对此有如下宏定义
//按下按键时,函数返回1,否则返回0,有&1的方式判断第一个比特位
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1)?1:0)
对此,我们就可以先设置好按下各按键产生的反映
//在右边空白处打印得分信息
SetPos(60, 10);
printf("得分: %d,每个食物得分%2d分", ps->_Score, 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_SHIFT)&&ps->_Speed>50)
{
ps->_Speed += 30;
ps->_FoodWeight -= 2;
}
else if (KEY_PRESS(VK_LCONTROL) && ps->_Speed < 350)
{
ps->_Speed -= 30;
ps->_FoodWeight += 2;
}
else if (KEY_PRESS(VK_SPACE))
{
SetPos(60, 26);
system("pause");
system("cls");
InitGamePrint(ps);
SetPos(ps->_pFood->x, ps->_pFood->y);
wprintf(L"◎");
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status =END_NORMAL;
}
当然,if执行完之后,需要清除一下缓冲区的数据(不清除会在游戏结束时产生问题),以下封装了一个函数(需要<conio.h>)
void KeyFun()
{
//检测是否有按键被按下
while (_kbhit())
{
// 使用 _getch() 获取按下的键,不阻塞程序
int key = _getch();
// 处理按键事件,可以根据需要进行相应的操作
}
}
2.2蛇的第一步
按键获取到之后,我们就需要对蛇的下一步情况进行讨论,但是再次之前,我们需要一个辅助————预知未来的蛇头,为蛇的行动提供了便利
//创建预知蛇头
pSnakeNode future = (pSnakeNode)malloc(sizeof(SnakeNode));
if (future == NULL)
{
perror("malloc failed");
return;
}
//根据按下的方向键来判断预知蛇头的位置
if (ps->_Dir == UP)
{
future->x = ps->_pSnake->x;
future->y = ps->_pSnake->y - 1;
}
else if (ps->_Dir == DOWN)
{
future->x = ps->_pSnake->x;
future->y = ps->_pSnake->y + 1;
}
else if (ps->_Dir == RIGHT)
{
future->x = ps->_pSnake->x+2;
future->y = ps->_pSnake->y ;
}
else if (ps->_Dir == LEFT)
{
future->x = ps->_pSnake->x - 2;
future->y = ps->_pSnake->y;
}
以向右走的蛇为例,
接下来要判断的有四点:下一步是食物、是墙、是自己、正常走
判断是吃食物就是要判断食物的坐标是否和预知蛇头坐标相同,
//分装一个函数,判断坐标是不是重合
int NextIsFood(pSnakeNode psn,pSnake ps)
{
return (psn->x == ps->_pFood->x && psn->y == ps->_pFood->y) ? 1 : 0;
}
如果坐标相同则吃食物,加分,释放掉当前食物,放置一个新的食物,否则继续判断
void EatFood(pSnakeNode psn,pSnake ps)
{
//吃到了食物,要长一个节点,用预知头作为节点进行头插
psn->next = ps->_pSnake;
ps->_pSnake = psn;
//吃到了食物,分数要增加
ps->_Score += ps->_FoodWeight;
//打印新蛇
PrintSnake(ps);
//释放食物
free(ps->_pFood);
//创建食物
CreateFood(ps);
}
如果撞到墙,只需要需改状态即可,因为状态将在后续作为循环条件
void NextIsWall(pSnakeNode psn, pSnake ps)
{
if (psn->x == 0 || psn->x == 54 || psn->y == 0 || psn->y == 26)
{
//改变状态
ps->_Status = KILL_BY_WALL;
}
}
如果撞到自己,则需要遍历对比
void NextIsSelf(pSnakeNode psn, pSnake ps)
{
//保护头
pSnakeNode ptail = ps->_pSnake;
//遍历链表
while (ptail)
{
if (ptail->x == psn->x && ptail->y == psn->y)
{
ps->_Status = KILL_BY_SELF;
}
ptail = ptail->next;
}
}
最后就是什么都没有碰到只是正常往前走了一步,这一步相对来说比较复杂,因为没有吃到食物,没有变长,所以需要销毁最后一个节点
void NormalWalk(pSnakeNode psn, pSnake ps)
{
//头插未来节点,作为新的蛇头
psn->next = ps->_pSnake;
ps->_pSnake = psn;
//保存蛇头
pSnakeNode ptail = psn;
//招出倒数第二个节点的指针
while (ptail->next->next)
{
ptail = ptail->next;
}
//在最后一个节点的位置放置两个空格来覆盖原来的打印
SetPos(ptail->next->x, ptail->next->y);
printf(" ");
//释放尾节点
free(ptail->next);
ptail->next = NULL;
//打印新的蛇
PrintSnake(ps);
//控制蛇的速度,在前面有按键可以修改速度
Sleep(ps->_Speed);
}
到目前为止,上述代码结合只能够走一步,那么要让代码进一步动起来需要一个do-while循环,循环条件是我们的状态OK,即ps->_Status==OK.
2.3蛇蛇死亡
蛇蛇死亡不能就这么结束,我们还需要一些亡语。
SetPos(24, 14);
//通过状态判断亡语
switch (ps->_Status)
{
case END_NORMAL:
printf("游戏结束\n");
break;
case KILL_BY_SELF:
printf("衔尾蛇\n");
break;
case KILL_BY_WALL:
printf("卡墙里了\n");
break;
}
3.游戏结束时的销毁
假设在游戏结束时按下暂停键,我们可以在控制台看到蛇的结点和食物,那么这些就是我们需要释放的对象,不释放可能会产生内存泄漏,那么我们可以如下释放
void GameEnd(pSnake ps)
{
//释放食物
free(ps->_pFood);
//释放蛇
pSnakeNode pcur = ps->_pSnake;
while (pcur)
{
pcur = ps->_pSnake->next;
free(ps->_pSnake);
ps->_pSnake = pcur;
}
}
顺带一提,如果想要做到游戏一直玩,我们可以将上述代码分装成三个函数,通过do-while循环来实现
do {
//游戏前,界面的维护
GameBefore(pS);
//游戏时,对于贪吃蛇的维护
GameRun(pS);
SetPos(20, 15);
printf("是否要再来一局?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch=='Y'||ch=='y');
GameEnd(pS);


650

被折叠的 条评论
为什么被折叠?



