引言
上一章介绍了实现贪吃蛇小游戏必备的知识点。
这章,让我们一起开启手搓核弹之旅吧。
先附上贪吃蛇代码的git: 贪吃蛇小游戏_4_23 · Shown_shuai/learn_c - 码云 - 开源中国 (gitee.com)
上一章的窗口:
一、整体思维导图

二、创建贪吃蛇
在游戏开始前,先本地化,为了后面打印的需要
cpp
int main()
{
//设置本地化
setlocale(LC_ALL, "");
return 0;
}
用链表来维护贪吃蛇

在snack.h中进行类型的声明
1. 蛇身的节点
cpp
//蛇身体的节点
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* nest;
}SnakeNode,* pSnakeNode;
//pSnakeNode是指向节点的指针类型
//等价于:typedef struct SnakeNode* pSnakeNode;
2. 创建贪吃蛇
贪吃蛇有许多需要维护的信息,将贪吃蛇创建为一个结构体。
cpp
//蛇的方向
enum DIRECTION
{
UP = 1, //上
DOWN, //下
LEFT, //左
RIGHT //右
};
//蛇的状态
//游戏正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
OK, //正常
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞到自己
END_NORMAL //正常退出
};
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake; //指向蛇头的指针
pSnakeNode _pFood; //指向食物的指针
enum DIRECTION _dir; //蛇的方向
enum GAME_STATUES _status; //游戏的状态
int _food_weight; //一个食物的分数
int _score; //总分数
int _sleep_time; //休息时间,时间越长,速度越慢,越短,速度越快
}Snake, *pSnake;
3.用枚举结构体,来枚举蛇属性的状态:
cpp
//蛇的方向
enum DIRECTION
{
UP = 1, //上
DOWN, //下
LEFT, //左
RIGHT //右
};
//蛇的状态
//游戏正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
OK, //正常
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞到自己
END_NORMAL //正常退出
};
4. 定义特殊的符号,和特殊的数据
cpp
#define POS_X 24 //游戏刚开始的时候,蛇头的位置
#define POS_Y 5 //游戏刚开始的时候,蛇头的位置
#define WALL L'□'
#define BOOY L'●'
#define FOOD L'★'
三、开始游戏
1.设置游戏窗口大小和名字,隐藏光标
cpp
//1.设置窗口的大小,光标隐藏
system("mode con cols=110 lines=35");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
CONSOLE_CURSOR_INFO curInfo; //存有光标信息的结构体
//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中
GetConsoleCursorInfo(houtput, &curInfo);
curInfo.bVisible = false; //修改光标信息
//设置获得和hOutput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &curInfo);
2. 打印欢迎界面和功能介绍(上一章的知识点):
cpp
//光标定位
void SetPos(int x, int y)
{
//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎界面和功能介绍
void WelcomeToGame()
{
SetPos(45, 16);
printf("欢迎来的贪吃蛇小游戏\n");
SetPos(48, 20);
system("pause");
system("cls"); //清空屏幕
SetPos(35, 16);
wprintf(L"用↑,↓,←,→来控制小蛇的移动,按F3加速,F4减速\n");
SetPos(43, 18);
wprintf(L"加速可以获得更高的分数");
SetPos(45, 23);
system("pause");
system("cls");
}
3.绘制地图:
cpp
//3.绘制地图
void CreateMap()
{
int i = 0;
//上
for (int i = 0; i < 35; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 30);
for (i = 0; i < 35; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i < 30; i++)
{
SetPos(0, i);
wprintf(L"%lc\n", WALL);
}
//右
for (i = 1; i < 30; i++)
{
SetPos(68, i);
wprintf(L"%lc\n", WALL);
}
SetPos(80, 0);
}
4.创建蛇
申请5个节点,使用头插,将蛇链接起来
cpp
//初始化蛇身
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSanke(): malloc fail!");
return;
}
cur->nest = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else
{
cur->nest = ps->_pSnake;
ps->_pSnake = cur;
}
}
//打印:
cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BOOY);
cur = cur->nest;
}
//设置贪吃蛇的属性
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;
ps->_status = OK;
}
5.创建食物
cpp
//创建食物
void CreateFood(pSnake ps)
{
int x = 0; //取值范围:2 ~ 66 0~64
int y = 0; // 1 ~ 30 0~29
again:
do
{
x = rand() % 65 + 2;
y = rand() % 29 + 1;
} while (x % 2 != 0); //x 必须是 2 的倍数
//x 和 y 的坐标不能和蛇的坐标相同
pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->nest;
}
//创建食物的节点:
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreaterFood():malloc fail");
return;
}
pFood->x = x;
pFood->y = y;
pFood->nest = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
6.初始化游戏的所以代码:
cpp
//游戏的初始化
void GameStart(pSnake ps)
{
//0.设置窗口的大小,光标隐藏
system("mode con cols=110 lines=35");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
CONSOLE_CURSOR_INFO curInfo; //存有光标信息的结构体
//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中
GetConsoleCursorInfo(houtput, &curInfo);
curInfo.bVisible = false; //修改光标信息
//设置获得和hOutput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &curInfo);
//1.打印欢迎界面
//2.功能介绍
WelcomeToGame();
//3.绘制地图
CreateMap();
//4.创建蛇
InitSnake(ps);
//5.创建食物
CreateFood(ps);
}
四、运行游戏
1. 打印帮助信息
cpp
void PrintHelpInfo()
{
SetPos(74, 15);
wprintf(L"%s", L"不能穿墙,不能咬到自己");
SetPos(71, 16);
wprintf(L"%s", L"用↑,↓,←,→来控制小蛇的移动");
SetPos(74, 17);
wprintf(L"%s", L"按F3加速,F4减速\n");
SetPos(71, 18);
wprintf(L"%s", L"ESC:退出游戏,space:暂停游戏");
SetPos(74, 20);
wprintf(L"%s", L"制作者:@Run_Teenage");
}
2.定义一个宏,检测按键是否被按过
上一章有讲,可以去看一下
cpp
#define KEY_PRESS(kv) ((GetAsyncKeyState(kv)&1) ? 1:0)
3.游戏运行时,整体结构:
cpp
//游戏的运行:
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分数
SetPos(74,10);
printf("总分数:%d\n", ps->_score);
SetPos(74, 11);
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_DOWN) && ps->_dir != UP)//按下键时,并且向上走,不会有反应
{
ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_SPACE))
{
//暂停
Pause();
}
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;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL;
}
//贪吃蛇走
SnakeMove(ps); //蛇走一步的过程
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
4.实现暂停模块
cpp
//暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
5.实现贪吃蛇走一步的过程
cpp
//贪吃蛇走
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);
}
5.1检查下一个位置是不是食物
cpp
//检测下一个坐标是不是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y);
}
5.2下一个是食物
cpp
//下一个位置是食物,吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插
ps->_pFood->nest = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放next节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BOOY);
cur = cur->nest;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
5.3下一个位置不是食物
cpp
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插
pn->nest = ps->_pSnake;
ps->_pSnake = pn;
pSnakeNode cur = ps->_pSnake;
while (cur->nest->nest != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BOOY);
cur = cur->nest;
}
//把最后一个位置的节点打印成空格
SetPos(cur->nest->x , cur->nest->y);
printf(" ");
//释放最后一个位置的节点
free(cur->nest);
//把倒数第二的节点的地址置为NULL
cur->nest = NULL;
}
5.4检查蛇是否撞到墙
cpp
//检测蛇是否撞墙
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 68 ||
ps->_pSnake->y == 0 || ps->_pSnake->y == 29)
{
ps->_status = KILL_BY_WALL;
}
}
5.5检查蛇是否撞到自己
遍历蛇头后面的节点的坐标是否和蛇的头节点重合,重合,则说明自己撞到了自己
cpp
//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->nest;
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->nest;
}
}
五、游戏结束 (善后工作)
打印游戏是为什么结束的,释放贪吃蛇的节点
cpp
//结束游戏 ------ 善后工作(释放内存之类的)
void GameEnd(pSnake ps)
{
SetPos(25, 15);
switch (ps->_status)
{
case END_NORMAL:
printf("正常结束游戏");
break;
case KILL_BY_WALL:
printf("撞上墙了,游戏结束");
break;
case KILL_BY_SELF:
printf("咬到自己了,游戏结束");
break;
}
//释放蛇身节点
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->nest;
free(del);
}
}
六、合并游戏的全部流程,处理细节问题
cpp
#include"snack.h"
void test()
{
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1.打印欢迎界面
//2.功能介绍
//3.绘制地图
//4.创建蛇
//5.创建食物
//6.设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏 ------ 善后工作(释放内存之类的)
GameEnd(&snake);
while (_kbhit())
{
_getch(); //吸收蛇动时,按键盘的键值带来的影响,不然游戏会有小bug
}
SetPos(24, 16);
printf("再来一局吗?(Y/N)");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 32);
}
int main()
{
//设置本地化
setlocale(LC_ALL, "");
srand((unsigned int )time(NULL));
test();
return 0;
}