二.贪吃蛇的游戏流程分析
这是我们贪吃蛇的整个游戏流程的分析
1.游戏窗口的实现
1.界面的初始化
根据我们刚才API部分的学习,我们已经写出了Init函数
可以用来隐藏屏幕光标
然后我们可以把打印宽字符,设置窗口大小,窗口名称的代码在main函数中去写
我们在这里将控制台的大小设置为宽:35行,列:120列
所以我们就可以在main函数当中这样去写
int main() { setlocale(LC_ALL, ""); system("mode con cols=120 lines=35"); system("title 贪吃蛇"); Init(); return 0; }
这样就完成了
2.欢迎界面的打印
我们在前面已经实现了SetPos函数
然后我们就可以通过Setpos去定位光标,然后打印欢迎信息
3.窗口布局(地图坐标)
这里要实现的是CreateMap函数
我们在这里实现的是一个27行,58列的棋盘
所以我们就需要去通过SetPos定位光标,然后打印这个墙
其次,为了便于打印,我们宏定义了墙,蛇身,食物的字符
#define WALL L'□' #define BODY L'●' #define FOOD L'★'
注意:这里最后打印下面的墙是因为测试时当我们打印完成之后
程序运行结束就会打印:
C:\Users\23119\Desktop\C++_learn_code\cpp_learn_code\Snake_review\Debug\Snake_review.exe (进程 6000)已退出,代码为 0。
按任意键关闭此窗口. . .
如果我们最后打印的不是下面的墙
那样就会出现这种情况:
因为打印完下面的墙之后又打印了左边和右边的墙
导致程序结束时下面的墙被这句话覆盖了
其实我们打印的过程是这样的
我们调试看一下过程
2.蛇身结构体的创建与初始化
1.蛇身节点的结构体
2.食物节点的结构体
3.蛇身结构体的创建
因此我们就可以定义出下面的结构体
4.蛇身的初始化
定义好蛇身节点,食物节点和蛇的结构体之后
下面我们要初始化这条蛇
怎么初始化呢?
因此我们就可以写出这样的代码
这两个宏定义是Snake.h文件中的 #define INIT_X 24 #define INIT_Y 6 void InitSnake(Snake* ps) { //初始化蛇身 for (int i = 0; i < 5; i++) { SNode* newnode = (SNode*)malloc(sizeof(SNode)); if (newnode == NULL) { perror("InitSnake():: malloc fail"); exit(-1); } newnode->next = NULL; newnode->x = INIT_X + 2 * i; newnode->y = INIT_Y; if (ps->_pSnake == NULL) { ps->_pSnake = newnode; } else { newnode->next = ps->_pSnake; ps->_pSnake = newnode; } } //打印蛇身 SNode* cur = ps->_pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } //初始化其他属性 ps->_dir = RIGHT; ps->_state = OK; ps->_foodWeight = 10; ps->_score = 0; ps->_sleepTime = 200; }
后面两块是打印蛇身和初始化其他属性的注意事项
5.食物的初始化
注意:rand()%53生成的随机数的范围是:0~52
void CreateFood(Snake* ps) { //创建食物 while (1) { //保证初始化到墙内 //x:2~54 int x = rand() % 53 + 2;//0~52+2 -> 2~54 //y:1~25 int y = rand() % 25 + 1;//0~24+1 -> 1~25 //保证初始化的x必须为偶数 if (x % 2 != 0) { continue;//这里是continue while(1){...}这个循环,这次循环不再执行下面的语句,直接跳转到下一次while(1){...} } //保证初始化时不跟蛇身重合 SNode* cur = ps->_pSnake; bool flag = false; while (cur) { //跟蛇身重合,重新通过rand函数设置x和y if (cur->x == x && cur->y == y) { flag = true; break;//这里是break出while(cur){...}这个循环 } cur = cur->next; } //没有跟蛇身重合,就可以创建食物节点了 if (!flag) { SNode* newnode = (SNode*)malloc(sizeof(SNode)); if (newnode == NULL) { perror("CreateFood():: malloc fail"); exit(-1); } newnode->next = NULL; newnode->x = x; newnode->y = y; ps->_pFood = newnode; break; } } //打印食物 SetPos(ps->_pFood->x, ps->_pFood->y); wprintf(L"%lc", FOOD); }
3.GameStart部分的完整代码
1.重点说明一下main函数
2.完整代码
1.Snake.h
#pragma once #define WALL L'□' #define BODY L'●' #define FOOD L'★' #include <stdio.h> #include <Windows.h> #include <stdbool.h> #include <locale.h> #include <stdlib.h> #include <time.h> #define INIT_X 24 #define INIT_Y 6 typedef struct SnakeNode { struct SnakeNode* next; int x; int y; }SNode; enum Direction { UP, DOWN, LEFT, RIGHT }; enum GameState { OK, EXIT_NORMAL, KILL_BY_WALL, KILL_BY_SELF }; typedef struct Snake { SNode* _pSnake;//蛇头节点 SNode* _pFood;//食物 enum Direction _dir;//蛇移动的方向 enum GameState _state;//当前游戏状态 int _score;//当前得分 int _foodWeight;//每个食物的分数 int _sleepTime;//蛇的休息时间,影响加速和减速和暂停 }Snake; void Init(); void SetPos(short x, short y); void GameStart(Snake* ps); void WelcomeToGame(); void CreateMap(); void InitSnake(Snake* ps); void CreateFood(Snake* ps);
2.Snake.c
#include "Snake.h" void Init() { HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//让handle具有能够操作控制台标准输出设备的能力/权限 CONSOLE_CURSOR_INFO CursorInfo;//这个结构体就是定义光标信息的结构体 GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false;//隐藏控制台光标的操作 SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态 } void SetPos(short x, short y) { COORD pos = { x,y }; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos); } void WelcomeToGame() { SetPos(45, 12); printf("欢迎来到贪吃蛇小游戏"); SetPos(45, 18); system("pause"); system("cls"); SetPos(45, 12); printf("用↑.↓.←.→ 分别控制蛇的移动,F1为加速,F2为减速"); SetPos(45, 13); printf("加速能够得到更高的分数"); SetPos(45, 18); system("pause"); system("cls"); } void CreateMap() { //上 SetPos(0, 0); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //左 for (int i = 0; i <= 25; i++) { SetPos(0, i); wprintf(L"%lc", WALL); } //右 for (int i = 0; i <= 25; i++) { SetPos(56, i); wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for (int i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } } void InitSnake(Snake* ps) { //初始化蛇身 for (int i = 0; i < 5; i++) { SNode* newnode = (SNode*)malloc(sizeof(SNode)); if (newnode == NULL) { perror("InitSnake():: malloc fail"); exit(-1); } newnode->next = NULL; newnode->x = INIT_X + 2 * i; newnode->y = INIT_Y; if (ps->_pSnake == NULL) { ps->_pSnake = newnode; } else { newnode->next = ps->_pSnake; ps->_pSnake = newnode; } } //打印蛇身 SNode* cur = ps->_pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } //初始化其他属性 ps->_dir = RIGHT; ps->_state = OK; ps->_foodWeight = 10; ps->_score = 0; ps->_sleepTime = 200; } void CreateFood(Snake* ps) { //创建食物 while (1) { //保证初始化到墙内 //x:2~54 int x = rand() % 53 + 2;//0~52+2 -> 2~54 //y:1~25 int y = rand() % 25 + 1;//0~24+1 -> 1~25 //保证初始化的x必须为偶数 if (x % 2 != 0) { continue; } //保证初始化时不跟蛇身重合 SNode* cur = ps->_pSnake; bool flag = false; while (cur) { //跟蛇身重合 if (cur->x == x && cur->y == y) { flag = true; break; } cur = cur->next; } //没有跟蛇身重合 if (!flag) { SNode* newnode = (SNode*)malloc(sizeof(SNode)); if (newnode == NULL) { perror("CreateFood():: malloc fail"); exit(-1); } newnode->next = NULL; newnode->x = x; newnode->y = y; ps->_pFood = newnode; break; } } //打印食物 SetPos(ps->_pFood->x, ps->_pFood->y); wprintf(L"%lc", FOOD); } void GameStart(Snake* ps) { WelcomeToGame(); CreateMap(); InitSnake(ps); CreateFood(ps); }
3.test.c
#include "Snake.h" //初始化光标信息等 int main() { setlocale(LC_ALL, ""); system("mode con cols=120 lines=35"); system("title 贪吃蛇"); Init(); Snake snake = { 0 };//将snake结构体变量的内容全都初始化为0 //(这里主要是为了初始化p_Snake头节点的指针,为了防止头插法创建蛇身链表时出现野指针的非法访问问题) //p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL srand((unsigned int)time(NULL));//设置随机数种子,防止每一次运行rand生成的随机数都是一样的 GameStart(&snake); SetPos(0, 30);//这里我们要定位一下光标, //防止最后打印的那条包含:"返回值为0"的语句因为光标最后处于打印食物位置的下一行 //而导致覆盖我们的墙体 return 0; }
3.最终实现情况:
4.游戏运行
我们的蛇身结构体和食物都已经初始化好了,游戏的开始工作结束
下面开始实现游戏运行的代码了
1.GameRun函数的整体框架
在这个GameRun函数中我们要实现的整体框架是:
因此我们可以写出这样的代码框架
2.帮助信息的打印
经过了前面打印欢迎界面之后,这个帮助信息的打印对我们来说就轻而易举了
3.获取按键情况
我们之前在API中提到过这个获取按键情况的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0) • 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(); } //Esc退出 else if (KEY_PRESS(VK_ESCAPE)) { ps->_state = EXIT_NORMAL; break; } //加速 else if (KEY_PRESS(VK_F1)) { //防止一直加速导致sleepTime<0出现bug if (ps->_sleepTime >= 50) { ps->_sleepTime -= 30; ps->_foodWeight += 2;//加速时食物的分值会增加 } } //减速 else if (KEY_PRESS(VK_F2)) { //防止一直减速导致程序运行太慢出现卡顿影响用户体验 if (ps->_sleepTime < 350) { ps->_sleepTime += 30; ps->_foodWeight -= 2; //防止太慢时食物得分减为负数 if (ps->_sleepTime == 350) { ps->_foodWeight = 1; } } }
Sleep是C语言的库函数,可以让程序休息对应的时间
单位是ms
这里的pause是暂停函数:
所以我们就可以完善一下我们的GameRun函数了
void GameRun(Snake* ps) { PrintHelpInfo(); do { SetPos(65, 10); printf("得分: %d , 每个食物得分: %d ", 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_SPACE)) { pause(); } //Esc退出 else if (KEY_PRESS(VK_ESCAPE)) { ps->_state = EXIT_NORMAL; break; } //加速 else if (KEY_PRESS(VK_F1)) { //防止一直加速导致sleepTime<0出现bug if (ps->_sleepTime >= 50) { ps->_sleepTime -= 30; ps->_foodWeight += 2;//加速时食物的分值会增加 } } //减速 else if (KEY_PRESS(VK_F2)) { //防止一直减速导致程序运行太慢出现卡顿影响用户体验 if (ps->_sleepTime < 350) { ps->_sleepTime += 30; ps->_foodWeight -= 2; //防止太慢时食物得分减为负数 if (ps->_sleepTime == 350) { ps->_foodWeight = 1; } } } //蛇每次移动都要有一定的休眠时间,时间越短,蛇移动的速度就越快 Sleep(ps->_sleepTime); SnakeMove(ps); } while (ps->_state == OK); }
//暂停函数 void pause() { while (1) { Sleep(200); if (KEY_PRESS(VK_SPACE)) { break; } } }
4.蛇身的移动
1.整体框架
void SnakeMove(Snake* ps) { //1.根据蛇头的坐标和方向,计算下一个节点的坐标 int x = ps->_pSnake->x; int y = ps->_pSnake->y; switch (ps->_dir) { case UP: y--; break; case DOWN: y++; break; case LEFT: x -= 2; break; case RIGHT: x += 2; break; } //2.创建下一个节点 SNode* pNextNode = (SNode*)malloc(sizeof(SNode)); if (pNextNode == NULL) { perror("SnakeMove():: malloc fail"); exit(-1); } pNextNode->x = x; pNextNode->y = y; pNextNode->next = NULL; //3.判断下一个是不是食物 if (x == ps->_pFood->x && y == ps->_pFood->y) { //下一个位置有食物 EatFood(pNextNode, ps); } //下一个位置没有食物 else { NoFood(pNextNode, ps); } //判断是否撞墙 IfKillByWall(ps, x, y); //判断是否咬到自己 IfKillBySelf(ps, x, y); }
下面我们就要实现一下下面的这4个函数,那么SnakeMove函数就成功完成了
2.EatFood和NoFood函数
所以我们就可以写出这样的代码
3.IfKillByWall和IfKillBySelf函数
这两个函数的返回值类型可以是void
因为我们可以直接在这两个函数当中修改游戏当前状态
也就是ps->_state
因此我们可以这样写:
5.GameRun部分的完整代码
这里只写了这一部分的完整代码
需要再加上GameStart部分的完整代码才可以正常运行
1.完整代码
Snake.h
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0) void pause(); void GameRun(Snake* ps); void PrintHelpInfo(); void EatFood(SNode* pNextNode, Snake* ps); void NoFood(SNode* pNextNode, Snake* ps); void IfKillByWall(Snake* ps, int x, int y); void IfKillBySelf(Snake* ps, int x, int y); void SnakeMove(Snake* ps);
Snake.c
//暂停函数 void pause() { while (1) { Sleep(200); if (KEY_PRESS(VK_SPACE)) { break; } } } void GameRun(Snake* ps) { PrintHelpInfo(); do { SetPos(65, 10); printf("得分: %d , 每个食物得分: %d ", 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_SPACE)) { pause(); } //Esc退出 else if (KEY_PRESS(VK_ESCAPE)) { ps->_state = EXIT_NORMAL; break; } //加速 else if (KEY_PRESS(VK_F1)) { //防止一直加速导致sleepTime<0出现bug if (ps->_sleepTime >= 50) { ps->_sleepTime -= 30; ps->_foodWeight += 2;//加速时食物的分值会增加 } } //减速 else if (KEY_PRESS(VK_F2)) { //防止一直减速导致程序运行太慢出现卡顿影响用户体验 if (ps->_sleepTime < 350) { ps->_sleepTime += 30; ps->_foodWeight -= 2; //防止太慢时食物得分减为负数 if (ps->_sleepTime == 350) { ps->_foodWeight = 1; } } } //蛇每次移动都要有一定的休眠时间,时间越短,蛇移动的速度就越快 Sleep(ps->_sleepTime); SnakeMove(ps); } while (ps->_state == OK); } void PrintHelpInfo() { SetPos(65, 17); printf("不能穿墙,不能咬到自己"); SetPos(65, 18); printf("用↑.↓.←.→ 分别控制蛇的移动"); SetPos(65, 19); printf("F1为加速,F2为减速"); SetPos(65, 20); printf("Esc: 退出游戏 space:暂停游戏"); SetPos(65, 22); printf("编写者:wzs"); } void EatFood(SNode* pNextNode, Snake* ps) { pNextNode->next = ps->_pSnake; ps->_pSnake = pNextNode; //打印蛇身 SNode* cur = ps->_pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } //释放食物节点 free(ps->_pFood); ps->_pFood = NULL; //加分 ps->_score += ps->_foodWeight; //创建新食物 CreateFood(ps); } void NoFood(SNode* pNextNode, Snake* ps) { pNextNode->next = ps->_pSnake; ps->_pSnake = pNextNode; //释放最后一个节点 SNode* cur = ps->_pSnake; while (cur->next->next) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } SetPos(cur->next->x, cur->next->y); printf(" "); free(cur->next); cur->next = NULL; } //判断是否撞墙 void IfKillByWall(Snake* ps, int x, int y) { if (x == 0 || x == 56 || y == 0 || y == 26) { ps->_state = KILL_BY_WALL; } } //判断是否咬到自己 void IfKillBySelf(Snake* ps, int x, int y) { SNode* cur = ps->_pSnake->next; while (cur) { if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y) { ps->_state = KILL_BY_SELF; break; } cur = cur->next; } } void SnakeMove(Snake* ps) { //1.根据蛇头的坐标和方向,计算下一个节点的坐标 int x = ps->_pSnake->x; int y = ps->_pSnake->y; switch (ps->_dir) { case UP: y--; break; case DOWN: y++; break; case LEFT: x -= 2; break; case RIGHT: x += 2; break; } //2.创建下一个节点 SNode* pNextNode = (SNode*)malloc(sizeof(SNode)); if (pNextNode == NULL) { perror("SnakeMove():: malloc fail"); exit(-1); } pNextNode->x = x; pNextNode->y = y; pNextNode->next = NULL; //3.判断下一个是不是食物 if (x == ps->_pFood->x && y == ps->_pFood->y) { //下一个位置有食物 EatFood(pNextNode, ps); } //下一个位置没有食物 else { NoFood(pNextNode, ps); } //判断是否撞墙 IfKillByWall(ps, x, y); //判断是否咬到自己 IfKillBySelf(ps, x, y); }
test.c
int main() { setlocale(LC_ALL, ""); system("mode con cols=120 lines=35"); system("title 贪吃蛇"); Init(); Snake snake = { 0 };//将snake结构体变量的内容全都初始化为0 //(这里主要是为了初始化p_Snake头节点的指针,为了防止头插法创建蛇身链表时出现野指针的非法访问问题) //p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL srand((unsigned int)time(NULL));//设置随机数种子,防止每一次运行rand生成的随机数都是一样的 GameStart(&snake); GameRun(&snake); SetPos(0, 30);//这里我们要定位一下光标, //防止最后打印的那条包含:"返回值为0"的语句因为光标最后处于打印食物位置的下一行 //而导致覆盖我们的墙体 return 0; }
2.最终实现情况
咬到自己:
撞墙:
6.游戏结束后的处理
1.代码实现
2.Y/N 是否再来一局
int main() { setlocale(LC_ALL, ""); system("mode con cols=120 lines=35"); system("title 贪吃蛇"); Init(); char input = 0; do { Snake snake = { 0 };//p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL srand((unsigned int)time(NULL)); GameStart(&snake); GameRun(&snake); GameEnd(&snake); SetPos(65, 26); printf("要在玩一局吗?(Y/N)"); input = getchar(); getchar();//清理'\n' system("cls");//清屏 SetPos(45,12); if (input == 'n' || input == 'N') { printf("欢迎再次在玩"); } else if (input == 'Y' || input == 'y') { printf("游戏即将开始,祝您玩的开心"); SetPos(45, 14); system("pause"); system("cls");//清屏 } } while (input == 'y' || input == 'Y'); SetPos(32, 0); return 0; }
三.总结
上面就是我们贪吃蛇代码的整体分析和梳理
其实我们的整体思路就是这个图片所展现的
我们只需要先把大概的框架全部完成
具体的函数先声明出来
然后我们剩下的任务就只有去把那些函数一一实现即可
以上就是贪吃蛇代码实现与剖析(C语言)的全部内容,希望能对大家有所帮助!