前言:
贪吃蛇从学习到真正实现花了9天实现,第一二天第一次学习,第三四五天第二次学习,第六七八天一边实现一边思考,才完成了贪吃蛇的代码。实现了贪吃蛇以后已经接近过年,我想自己再根据掌握的知识制作烟花燃烧绽放的场景。贪吃蛇的移动和烟花的移动原理是一样的,贪吃蛇的头插删尾,使我能够处理烟花消失的部分。在我花两天时间写完500行的烟花代码后,对贪吃蛇的实现原理也更加了解了。然后再写下这篇文章。
写作过程中遇到的问题:有很多,但最多的是,写着写着不知道当前要实现什么。思路不清晰。然后去看正确的代码。以及对指针的掌握不够,要使用的函数比较生疏,令人烦躁,静不下心去理解等等。之后通过各种途径一一克服了。
一、分析和规划贪吃蛇的思路
1.一个已经懂得贪吃蛇怎样写的人和一个从没写过贪吃蛇怎么写,第一次上手的思维是不一样的。但是第一部应该都需要了解自己要实现哪些功能。然后划分为不同阶段逐个完成。
2.根据想要制作的成品模样,画出草图和X-mind思维导图。
成图1,欢迎页面一
成图2,欢迎页面二
成图3,游戏页面
根据贪吃蛇游戏框,绘出墙体食物和蛇。
接下来是X-mind思维导图,写出整个程序的脉络。然后一一落实。
二、落实想法
1.建立三个文件。snake.c snake.h test.c.
2.贪吃蛇有两个结构体。
一个是蛇身,它是由一个个结点组成。
这个结点里放蛇身的坐标和下一个结点的指针,记住下一个结点的位置。放坐标是因为蛇在移动的时候要打印蛇身,蛇身的位置是依靠坐标来确定的,这样打印的时候就能找到蛇身的位置了。
这个蛇身由单向不带头不循环链表----单链表构成。
创建一个结构体,以及一个指向结构体的指针并将它们重命名。
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode * next;
}SnakeNode, *pSnakeNode;
另一个用来维护蛇。如思维导图中写的思路所示。
typedef struct Snake
{
pSnakeNode sn; //指向蛇头的指针
pSnakeNode pfood;//指向食物的指针,本质上和蛇身没什么区别,只是它是单个的,没有链。
int score; //分数,每次吃食物要涨粉
int foodweight;//当前每吃一个食物增加的分数
enum DIRECTION;//方向
enum STATUS;//状态
int sleeptime;//速度
}Snake,*psnake;//重命名
3.三个文件代码附上
snake.h
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#pragma once
#include <locale.h>
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <wchar.h>
#include <math.h>
#define WALL L'□'
#define SNAKE L'●'
#define FOOD L'★'
//监测按键
#define KEY_STATE(vkey) ((GetAsyncKeyState(vkey)&0x1)?(1):(0))
//蛇头方向
enum DIRICTION
{
UP=1,
DOWN,
LEFT,
RIGHT
};
//游戏状态
enum STATUS
{
OK=1,
KILL_BY_SELF,
KILL_BY_WALL,
ESC
};
//蛇身的结点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//蛇的维护
typedef struct Snake
{
pSnakeNode sn;//蛇身,是一个结构体指针
pSnakeNode pfood;//也是一个结构体指针,它是一个坐标
enum DIRICTION Dir;//蛇的方向
enum STATUS Status;//蛇的状态
int Score;//游戏当前得分
int Foodweight;//食物的分数
int Sleeptime;//走一步睡眠时间,和蛇速相关
}Snake,*pSnake;
//游戏开始前的准备工作
void GameStart(pSnake snake);
//玩游戏
void GameRun(pSnake snake);
//
游戏结束
//void GameEnd();
snake.c
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"
//把光标移动到想要的位置
void SetPos(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(handle, pos);
}
void Welcometogame()
{
SetPos(38, 13);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(60, 25);
system("pause");
system("cls");
//打印游戏说明
SetPos(25, 12);
printf("用↑.↓.←.→分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(25, 13);
printf("加速能得到更高的分数。\n");
SetPos(60, 25);
system("pause");
system("cls");
}
//初始化蛇身
void InitSnake(pSnake snake)
{
for (int i = 0; i < 5; i++)
{
pSnakeNode p = (pSnakeNode)malloc(sizeof(SnakeNode));
if (p == NULL)
{
perror("malloc failed!\n");
exit(1);
}
p->x = 20+2*i;
p->y = 6;
p->next = NULL;
//头插法
if (snake->sn==NULL)
{
snake->sn = p;
}
else
{
p->next = snake->sn;
snake->sn = p;
}
//打印蛇身,用循环
/*if (p)
{
SetPos(p->x, p->y);
wprintf(L"%lc", SNAKE);
}*/
while (p)
{
SetPos(p->x, p->y);
wprintf(L"%lc", SNAKE);
p = p->next;
}
}
//其他信息初始化
snake->Dir = RIGHT;
snake->Foodweight = 10;
snake->pfood = NULL;
snake->Score = 0;
snake->Sleeptime = 200;
snake->Status = OK;
}
void CreatMap()
{
int i = 0;
for (i = 0; i < 57; i += 2)
{
wprintf(L"%lc", WALL);
}
for (i = 0; i <= 26; i++)
{
SetPos(56, i);
wprintf(L"%lc\n", WALL);
}
SetPos(0, 26);
for (i = 0; i < 57; i += 2)
{
wprintf(L"%lc", WALL);
}
SetPos(0, 1);
for (i = 0; i <= 25; i++)
{
wprintf(L"%lc\n", WALL);
}
}
void CreatFood(pSnake snake)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 24 + 1;
} while (x % 2 != 0);
//不能在蛇身上
pSnakeNode cur = snake->sn;
while (cur)
{
if (cur->x != x || cur->y != y)
{
cur = cur->next;
}
else
{
goto again;
}
}
//申请食物的结点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("malloc failed!\n");
exit(1);
}
pFood->x = x;
pFood->y = y;
snake->pfood = pFood;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
//游戏开始前的准备工作
void GameStart(pSnake snake)
{
//设置一下控制台大小
system("mode con cols=100 lines=30");
//更改控制台名字
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO Cursorinfo;
GetConsoleCursorInfo(handle, &Cursorinfo);
Cursorinfo.bVisible = false;
SetConsoleCursorInfo(handle, &Cursorinfo);
//打印欢迎界面
Welcometogame();
//绘制地图
CreatMap();
//初始化蛇身
InitSnake(snake);
//打印食物
CreatFood(snake);
}
void PrintHelpInfo()
{
SetPos(61, 16);
printf("1.不能撞墙,不能咬到自己\n");
SetPos(61, 17);
printf("2.用↑.↓.←.→分别控制蛇的移动\n");
SetPos(61, 18);
printf("3.F3为加速,F4为减速\n");
SetPos(61, 19);
printf("4.加速可以获得更多分数\n");
SetPos(80, 22);
printf("制作者:真白");
}
void EatFood(pSnake snake, pSnakeNode pnext)
{
//吃食物,则头插
pnext->next = snake->sn;//食物的下一个结点连接蛇头
snake->sn = pnext;//把食物的结点给蛇头
//打印蛇身
pSnakeNode cur = snake->sn;//创建一个cur指针来循环
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", SNAKE);
cur = cur->next;
}
//分数变化
snake->Score += snake->Foodweight;
//释放旧的食物结点
free(snake->pfood);
//创建新的食物结点
CreatFood(snake);
}
void NotEatFood(pSnake snake,pSnakeNode pnext)
{
//正常走,头插,删尾
pnext->next = snake->sn;
snake->sn = pnext;
//删尾,创建一个指针循环
pSnakeNode cur = snake->sn;
while (cur->next->next)
{
SetPos(cur->x, cur->y);//先设置坐标再打印
wprintf(L"%lc", SNAKE);//打印蛇身
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);//先设置坐标再打印尾处的空白
printf(" ");
free(cur->next);//释放尾结点
cur->next = NULL;
}
void IsItFood(pSnake snake, pSnakeNode pnext)
{
if (pnext->x == snake->pfood->x && pnext->y == snake->pfood->y)
{
//是食物,吃掉
EatFood(snake,pnext);
}
else
{
//不是食物,不吃
NotEatFood(snake,pnext);
}
}
void KillByWall(pSnake snake, pSnakeNode pnext)
{
//下一个结点的位置是不是墙的坐标
if (pnext->x == 0 || pnext->y == 0 || pnext->x == 56 || pnext->y==26)
{
snake->Status = KILL_BY_WALL;
}
}
void KillBySelf(pSnake snake, pSnakeNode pnext)
{
//下一个结点是不是蛇身
pSnakeNode cur = snake->sn->next;
while (cur)
{
if (pnext->x != cur->x || pnext->y != cur->y)
{
cur = cur->next;
}
else
{
snake->Status = KILL_BY_SELF;
break;
}
}
}
void SleepTime(pSnake snake)
{
Sleep(snake->Sleeptime);
}
void Snakemove(pSnake snake)
{
//创建蛇的下一个位置的结点
pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pnext == NULL)
{
perror("Snakemove:: malloc");
return;
}
pnext->next = NULL;//只需要这个结点,不需要它下一个结点的信息
//安排pnext的坐标
switch (snake->Dir)
{
case UP: pnext->x = snake->sn->x;
pnext->y = snake->sn->y - 1;//如果按了上,下一个坐标的位置就在蛇头的上面
break;
case DOWN: pnext->x = snake->sn->x;
pnext->y = snake->sn->y + 1;//同上,下面同上
break;
case LEFT:pnext->x = snake->sn->x - 2;
pnext->y = snake->sn->y;
break;
case RIGHT:pnext->x = snake->sn->x + 2;
pnext->y = snake->sn->y;
break;
}
//判断下一个结点是否是食物
IsItFood(snake, pnext);
//判断下一个节点是否撞墙
KillByWall(snake,pnext);
//判断下一个节点是否咬到自己
KillBySelf(snake,pnext);
}
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_STATE(VK_SPACE) == 1)
{
break;
}
}
}
void F3(pSnake snake)
{
//休眠时间限制,5档 200,170,140,110,80
if (snake->Sleeptime <= 200 && snake->Sleeptime >= 110)
{
snake->Sleeptime -= 30;
snake->Foodweight += 2;
}
}
void F4(pSnake snake)
{
if (snake->Sleeptime >= 80 && snake->Sleeptime <= 170)
{
snake->Sleeptime += 30;
if (snake->Foodweight >= 4)
{
snake->Foodweight -= 2;
}
}
}
//玩游戏
void GameRun(pSnake snake)
{
//打印帮助信息
PrintHelpInfo();
do
{
//当前分数情况
SetPos(60, 10);
printf("得分:%d ", snake->Score);
printf("每个食物得分:%02d\n", snake->Foodweight);
//监测当前按键情况
if (KEY_STATE(VK_UP) == 1 && snake->Dir != DOWN)
{
snake->Dir = UP;
}
else if(KEY_STATE(VK_DOWN) == 1&& snake->Dir != UP)
{
snake->Dir = DOWN;
}
else if (KEY_STATE(VK_LEFT) == 1&& snake->Dir != RIGHT)
{
snake->Dir = LEFT;
}
else if (KEY_STATE(VK_RIGHT) == 1&& snake->Dir != LEFT)
{
snake->Dir = RIGHT;
}
else if (KEY_STATE(VK_ESCAPE) == 1)
{
snake->Status = ESC;
break;
}
else if (KEY_STATE(VK_SPACE) == 1)
{
Pause();
}
else if (KEY_STATE(VK_F3) == 1)
{
F3(snake);
}
else if (KEY_STATE(VK_F4) == 1)
{
F4(snake);
}
//蛇的移动
Snakemove(snake);
//移动一个位置,休眠一下
SleepTime(snake);
} while(snake->Status==OK);
}
void GameEnd(pSnake snake)
{
SetPos(15, 12);
switch (snake->Status)
{
case ESC:
printf("主动退出游戏,正常退出\n");
break;
case KILL_BY_WALL:
printf("很遗憾,你撞墙了,游戏结束\n");
break;
case KILL_BY_SELF:
printf("很遗憾,你咬到自己了,游戏结束\n");
break;
}
pSnakeNode cur = snake->sn;
pSnakeNode del = NULL;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
}
free(snake->pfood);
snake = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"
void test()
{
int ch = 0;
do
{
Snake s1 = { 0 };
GameStart(&s1);
GameRun(&s1);
GameEnd(&s1);
SetPos(15, 14);
printf("再玩一把吗?Y/N :");
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
}
//
int main()
{
setlocale(LC_ALL, "");//本地化,头文件<locale.h>
test();
SetPos(0, 27);
return 0;
}
三、整个贪吃蛇实现的难点
1.windows系统提供的API的一些接口和功能。
在学C语言的时候没有接触,所以要了解一下需要掌握的函数用法。
如何修改控制台的大小?
通过包含windows.h的库函数,可以使用windows命令提示符的一些命令。来达到修改控制台大小的效果。在程序结束前,都是这个大小。system("mode con cols 100 lines 30")
同样的,修改控制台的标题也是利用windows命令提示符的一些命令来修改。
如何隐藏光标?
首先是要获得控制台的句柄。句柄就相当于控制台的钥匙,获取句柄,就是获取这个控制台的信息,(这些信息使得这个控制台与其他控制台相区别)我们得到的这个控制台的句柄,只能操作这个控制台的独特信息,不能修改其他控制台的独特信息。提供的函数是GetStdHandle,它有一个参数,但可以选择填入的参数有三个,为了获取句柄,填入的是STD_OUTPUT_HANDLE。这个函数返回的类型是HANDLE。因此也要创建一个HANDLE 类型的变量来接收返回的句柄。
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO Cursorinfo; 创造一个光标的变量,前面是类型,后面是变量,这是在下一句获取光标信息要用到。提前创建一个变量。
GetConsoleCursorInfo(handle, &Cursorinfo); 传本控制台的句柄和一个光标变量的地址。传入这两个变量以后,就能获取本控制台的光标信息。本控制台的光标信息,会被复制到Cursorinfo这个变量上。
Cursorinfo.bVisible = false; 这个Cursorinfo是一个结构体变量,它有两个成员,一个是光标占一个坐标的比例(0-100)。第二个是是否可见。把bVisible设置为ture就是可见,false就是不可见。通过这一步,就把光标设置为隐藏。
SetConsoleCursorInfo(handle, &Cursorinfo);这个函数是设置光标信息。传入本控制台的句柄,再把设置好的光标信息传进去。从而改变本控制台的光标。
如何设置坐标位置?
首先,什么是坐标?
以此图为例,x坐标是横坐标,从左向右延申,y坐标是纵坐标,从上至下延申,第一个位置是原点。
如何设置坐标位置:
void SetPos(int x, int y) 只要传入坐标的位置,就可以把光标移到想要的地方。把他封装成一个函数
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); 获取本控制台的句柄,因为把光标移到指定坐标(的函数)也需要本控制台的句柄(作为它的参数)。
COORD pos = { x,y }; 这是一个坐标的结构体,COORD是一个结构体类型,里面是坐标的参数。这一步是设置pos作为一个结构体坐标变量,成员初始化为x,y。
SetConsoleCursorPosition(handle, pos);这是一个设置光标位置的函数,需要传两个参数,一个是本控制台句柄,一个是指定的坐标信息。
}
通过这些代码就可以把光标移动到指定位置,然后在指定位置打印出想要的信息。
2.绘制地图
地图的墙是宽字符,打印宽字符要先本地化。使用的函数是wprintf,w表示wide的意思。
3.初始化蛇身
初始化蛇身需要做到申请蛇的结点,每个结点的坐标关系是怎样的,在于你想要蛇一开始在地图的哪个位置出现。蛇的结点之间的连接采用头插法。把蛇的结点都创建出来以后,还要打印一遍蛇身。最后再初始化蛇的状态、速度、食物分数、分数等信息。
4.打印食物
食物的特征,本质上是一个结点。坐标要随机生成,不能生成在墙上,x坐标得是2的倍数,因为和蛇对称,和墙也要对称。坐标不能在蛇的身上。设置好相关条件就可以创建结点了。然后把它初始化为已经生成的坐标。
5.蛇的移动
蛇是走一步移动一步,它要监测是否有按上下左右,如果按了,蛇头就要转变方向。蛇的移动本质上是下一个结点的位置在哪。所以要安排下一个结点的位置。下一个结点的位置有多种可能,撞墙,撞自己,吃食物和正常进行。
如果是吃食物,就是头插。
如果是正常走,那么就是头插以后再删尾,这个过程还要打印一遍蛇身。那么尾巴部分的结点被释放以后,在尾结点的原坐标上打印两个空格来代替。这个过程在地图上显示就是蛇走了一格。
蛇移动的速度越快,休眠的时间越短。所以速度方面设置休眠时间就可以了。这个休眠时间Sleep的函数也是包含windows的库函数来实现的。
撞墙和撞自己都需要修改游戏状态。修改的游戏状态就在于停止游戏。所以外面要套个循环。游戏只在状态是OK的时候进行,其他情况都分别打印出对应的信息。
6.暂停功能如何实现?
暂停的功能可以通过死循环,一直在睡眠。只有重新按了空格键,再跳出循环继续运行。
7.最后的收尾
收尾部分主要是游戏玩了一把game over以后,因为各种原因结束而打印不同信息。打印完了要把蛇的结点依次释放。然后再把食物释放,把传来的维护蛇的指针置为空。
如果想设置再来一把的消息,可以在test.c文件里进行。
要注意两个getchar。第一个getchar用来读取信息,通过一个变量来接收,用于判断玩家到底要不要开下一把。第二个getchar用来接收读取回车字符,但是没有变量接收它,也就是它不产生实际作用。因为它的目的只是用来吸收回车,使这个回车键不至于影响到下一次的输入判定。
8.贪吃蛇的结构体维护
贪吃蛇有两个结构体,第一个结构体是蛇身,第二个是蛇的各种信息,里面也包含了蛇身。
那么就是创建一条贪吃蛇的结构体,来玩这个贪吃蛇游戏。传的参数就是这个贪吃蛇的结构体的地址,因为传地址,才能改变贪吃蛇的值。这里的值包括状态,蛇的结点的指针,蛇的方向,当前分数等等。蛇的结点的头指针是常常需要改变的,因为它要不断移动。如果传的是结点指针,那么要传二级指针。但是本次情况中蛇的结点的头指针在贪吃蛇结构体里只是一个值,这里既然传了贪吃蛇的结构体地址,那么就能随便改变蛇的结点的头指针了。这个是要注意的关于指针的细节。以便在进行与贪吃蛇相似的项目中能够复用。