赞
踩
由于篇幅限制 和 使知识模块化,
若想了解 使用到的 Win32API 的知识:请点击跳转:【Win32API】贪吃蛇会使用到的 Win32API
目录
准备工作:创建三个文件
我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?
1.1.1 控制台窗口的坐标知识
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。
1.1.2 宽字符:
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符 ● ,打印食物使用宽字符★或图中的那个
普通的字符是占一个字节的,这类宽字符是占用 2 个字节。
宽字符的来源
过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入和 <locale.h> 头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
(1) <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
(2) setlocale函数
因为各个地区的代码语言环境不同:需要转换到 本地环境, 才能支持宽字符(如汉字)的输出
用 "" 作为第 2 个参数,(注意:就是一对双引号,中间没有空格) ,调用 setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
#include<locale.h> // 包含头文件 setlocale(LC_ALL, "" );//切换到本地环境:中文环境(3)宽字符 的 打印格式
宽字符要 在单引号前面加上 L,表示宽字符,宽字符的打印用wprintf,对应wprintf()的占位符为%lc;假设 A 为一个窄字符,B 为 宽字符
窄字符:printf("%c", A) 宽字符:wprintf(L"%lc", B);
下图,边框上的墙体就是 一种宽字符:□ ,宽度占 2 个字符位
1个坐标可以放1个正常字符,即窄字符
2个坐标可以存放1个宽字符
- void CreateMap()
- {
- // 上:29个宽格子
- SetPos(0, 0);// 从 (0, 0)开始
- for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
- {
- wprintf(L"%c", WALL);
- }
- //下
- SetPos(0, 26);
- for (int i = 0; i <= 56; i += 2)
- {
- wprintf(L"%c", WALL);
- }
- // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的
- //左
- for (int i = 1; i <= 25; i++)
- {
- SetPos(0, i);
- wprintf(L"%c", WALL);
- }
- //右
- for (int i = 1; i <= 25; i++)
- {
- SetPos(56, i);
- wprintf(L"%c", WALL);
- }
- }
初始化状态,假设蛇的长度是 5 ,蛇身的每个节点是●,在固定的一个坐标处,比如 (24,5) 处开始出现蛇,连续 5 个节点。 注意: 蛇的每个节点的 x 坐标必须是 2 的倍数, 即偶数,否则撞墙时可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是 2 的倍数),坐标不能和蛇的身体重合,然后打印★。
void InitSnake(pSnake ps) { // 创建5个节点: pSnakeNode cur = NULL; for (int i = 0; i < 5; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake:malloc"); return; } cur->x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; // 头插法: // 没有节点下 if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针 { ps->pSnake = cur; } // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头 else { cur->next = ps->pSnake; ps->pSnake = cur; } } //打印蛇的身体 // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行 // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思 cur = ps->pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } // 初始化贪吃蛇的其他信息 ps->dir = RIGHT; // 初始默认向右 ps->FoodWeight = 10; ps->pFood = NULL; ps->Score = 0; ps->SleepTime = 200; // 休眠 200 ms ps->state = OK; }
在游戏运行的过程中,蛇每次吃一个⻝物,蛇的身体就会变⻓一节,如果我们使用 链表 存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,
所以蛇节点结构如下:
// 蛇身节点 // 蛇身用链表来维护:一个蛇身节点就是一个链表节点 // 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode; // 创建一个指向蛇身节点的结构体指针
要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇:
//贪吃蛇 // 整条蛇 // 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇: typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头 pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样 enum DIRECTION dir;//蛇头的方向默认是向右 enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型) int Score;//当前获得分数 int FoodWeight;//默认每个食物 10 分 int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度) }Snake, * pSnake; // pSnake 是 指向贪吃蛇的指针: 下面就用上了
蛇的方向:可以一一列举,使用枚举
//蛇走的方向 enum DIRECTION { UP = 1, // 上 DOWN, // 下 LEFT, // 左 RIGHT // 右 };
游戏状态:可以一一列举,使用枚举
/蛇的状态: 维护游戏状态的 枚举类型 enum GAME_STATE { OK,//正常运行 ESC, // 按 ESC 键退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF,//咬到自己 //END_NOMAL//正常结束 };
GameStart(&snake); 游戏开始
GameRun(&snake); 游戏过程
GameEnd(&snake); 游戏结束
- #include"snake.h"
-
- void test()
- {
- int ch = 0;
- do
- {
- // 创建一条贪吃蛇
- Snake snake = { 0 };
- // 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
- GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
- GameRun(&snake); // 游戏过程
- GameEnd(&snake);//善后工作
- SetPos(20, 15);
- printf("再来一局?(y/n)");
- ch = getchar();
- getchar();
- } while (ch == 'Y' || ch == 'y');
-
- }
- int main()
- {
- //适配本地中文环境
- setlocale(LC_ALL, "");
-
- //贪吃蛇游戏的设置
- test();
- return 0;
- }
一、打印欢迎信息
WelcomeToGame();
二、绘制地图
CreateMap();
三、初始化蛇
InitSnake(ps);
四、创建食物
CreateFood(ps);
- // 游戏前准备
- void GameStart(pSnake ps)
- {
- // 一、设置控制台的信息:窗口大小、窗口名
- // 设置控制台窗口的长宽:设置控制台窗口的大小
- // 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆 列为 100, 行 30
- system("mode con cols=100 lines=30");
- system("title 贪吃蛇");// 设置cmd窗口名称
-
- // 二、隐藏光标:不想光标在一直闪
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
- //隐藏光标操作: 6.5.1节 那里有示例
- CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
- GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
- //先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
- cursorinfo.bVisible = false; //隐藏控制台光标
- SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
-
- //一、打印欢迎信息
- WelcomeToGame();
- //二、绘制地图
- CreateMap();
- //三、初始化蛇
- InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
- //四、创建食物
- CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
- }
游戏前准备 GameStart(pSnake ps) 的 四个子函数
先封装 设置坐标函数
//封装一个设置光标位置的函数 SetPos: void SetPos(int x, int y) { //获取标准输出设备的句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos COORD pos = { x,y }; SetConsoleCursorPosition(handle, pos); }
一、打印欢迎信息
void WelcomeToGame() { //一、 欢迎页面 // 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30 SetPos(38, 13); // 坐标的设置 看感觉来吧 printf("欢迎来到贪吃蛇小游戏\n"); // 打印完上面这句后:会有一句:请按任意键继续. . . 位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标 SetPos(38, 16); // 请按任意键继续. . . 的位置 system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . . 然后打印另一张 // 清空屏幕信息:要打印下一张页面 system("cls"); //二、功能介绍页面 SetPos(29, 8); printf("1、用 ↑ ↓ ← → 来控制蛇的移动 \n"); SetPos(29, 10); printf("2、F3 是 加速,F4 是 减速 O(∩_∩)O\n"); SetPos(29, 12); printf("3、加速可以获得更高的分数 (づ ̄ 3 ̄)づ\n"); SetPos(38, 16); // 请按任意键继续. . . 的位置 system("pause"); //一张页面打印完成后 暂停一下 // 清空屏幕信息:要打印下一张页面 system("cls"); }
二、绘制地图
void CreateMap() { // 上:29个宽格子 SetPos(0, 0);// 从 (0, 0)开始 for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符 { wprintf(L"%c", WALL); } //下 SetPos(0, 26); for (int i = 0; i <= 56; i += 2) { wprintf(L"%c", WALL); } // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的 //左 for (int i = 1; i <= 25; i++) { SetPos(0, i); wprintf(L"%c", WALL); } //右 for (int i = 1; i <= 25; i++) { SetPos(56, i); wprintf(L"%c", WALL); } }
三、初始化 蛇
// 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要 // 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。 // 创建5个节点,然后将每个节点存放在链表中进⾏管理。 void InitSnake(pSnake ps) { // 创建5个节点: pSnakeNode cur = NULL; for (int i = 0; i < 5; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake:malloc"); return; } cur->x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; // 头插法: // 没有节点下 if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针 { ps->pSnake = cur; } // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头 else { cur->next = ps->pSnake; ps->pSnake = cur; } } //打印蛇的身体 // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行 // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思 cur = ps->pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } // 初始化贪吃蛇的其他信息 ps->dir = RIGHT; // 初始默认向右 ps->FoodWeight = 10; ps->pFood = NULL; ps->Score = 0; ps->SleepTime = 200; // 休眠 200 ms ps->state = OK; }
四、创建食物
// 思路: // 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模 // 2、x 的范围:2 ~ 54 ---> 生成 0 ~ 52 + 2 ---> rand()%53 + 2 因为 x 是占 2 个窄字符的, x 的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖 // 3、y 的范围:1 ~ 25 ---> 生成 0 ~ 24 + 2 ---> rand()%25 + 1 // 创建食物注意事项 // 1.食物是随机出现的,坐标就是随机的 // 2.坐标必须在墙内 // 3.坐标不能在蛇的身体上 void CreateFood(pSnake ps) { // 1.食物是随机出现的,坐标就是随机的 // 2.坐标必须在墙内 int x = 0; int y = 0; again: do { x = rand() % 53 + 2; y = rand() % 24 + 1; } while (x % 2 != 0); // 这一步啥意思呀:42min 左右 // 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比 //判断食物是否在蛇身上 pSnakeNode cur = ps->pSnake; while (cur) { if (x == cur->x && y == cur->y) { goto again; } cur = cur->next; } //创建食物: 一个新节点 pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));// (pSnakeNode) 和 (SnakeNode) 千万别混淆!!! if (pFood == NULL) { perror("CreateFood():malloc()"); return; } pFood->x = x; pFood->y = y; ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点 // 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动 //打印食物 SetPos(x, y); wprintf(L"%c", FOOD); //getchar(); // 注意 :这个别忘了关闭 }
确定了蛇的方向和速度,蛇就可以移动了。
- // 游戏运行过程
- void GameRun(pSnake ps)
- {
- // 一、打印界面右侧的帮助提示信息
- PrintHelpInfo();
- // 游戏一般 是 do while循环:总之都需要先让游戏先运行一次
-
- do
- {
- //当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
- SetPos(65, 4);
- //[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
- // SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | BACKGROUND_INTENSITY);
- CONSOLE_FONT_INFOEX cfi;
- cfi.cbSize = sizeof cfi;
- cfi.dwFontSize.X = 0; //字宽
- cfi.dwFontSize.Y = 20;//字高
- cfi.FontWeight = FW_NORMAL;//粗细
- SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
-
- printf("总分:%5d", ps->Score);
- SetPos(65, 6 );
- printf("食物的分值:%02d", ps->FoodWeight);
-
- // 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
- // 上下左右,ESC,空格,F3,F4
- // 不能同时 方向 等于 相反方向
- if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于 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_ESCAPE))
- {
- ps->state = ESC; // 将状态该成 退出状态
- break;
- }
- else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
- {
- //游戏要暂定
- // 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
- Pause();//暂定和回复暂定
- }
- else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
- {
- if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
- {
- ps->SleepTime -= 30;
- ps->FoodWeight += 2;
- }
- }
- else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
- {
- if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
- {
- ps->SleepTime += 30;
- ps->FoodWeight -= 2;
- }
- }
- // 三 四 步 别写反
- // 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
- // 蛇身移动较为复杂,封装成一个函数便于分析
- SnakeMove(ps);
- // 四、睡眠一下
- Sleep(ps->SleepTime);
-
- } while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束
-
- }
游戏运行过程函数:GameRun(pSnake ps) 的 几个子函数
一、PrintHelpInfo():打印界面右侧的帮助提示信息
二、Pause():游戏暂停键
三、SnakeMove(ps):蛇身移动(较为复杂)
一、PrintHelpInfo():打印界面右侧的帮助提示信息
void PrintHelpInfo() { // 设置位置 SetPos(65, 8); printf("温馨提示\n"); SetPos(65, 10); printf("1、不能穿墙\n"); SetPos(65, 11); printf("2、不能咬到自己\n"); SetPos(65, 12); printf("3、按 ESC 退出游戏\n"); SetPos(65, 13); printf("4、按 空格 暂停游戏\n"); SetPos(65, 15); printf("操作回顾\n"); SetPos(65, 17); printf("5、用 ↑↓ ← → 来控制蛇的移动 \n"); SetPos(65, 18); printf("6、F3 加速,F4 减速\n"); SetPos(65, 19); printf("7、加速可以获得更高的分数\n"); SetPos(65, 22); printf("版权 @时差\n"); // getchar(); // 注意 :这个别忘了关闭 }
二、Pause():游戏暂停键
void Pause()//游戏暂停 { // tip : 死循环的 sleep , 再循环中 判断 再次点击空格键 即可回复 while (1) { Sleep(200); if (KEY_PRESS(VK_SPACE)) { break; } } }
三、SnakeMove(ps):蛇身移动(较为复杂)
分成几个子函数:
- // 蛇身 移动
- // 移动的本质: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
- void SnakeMove(pSnake ps)
- {
- // 根据按键:调整移动方向
- // 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min
- // 取名:方向节点
- pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pNext == NULL)
- {
- perror("SnakeMove():malloc:");
- return;
- }
- pNext->next = NULL;// 指向空就好
-
- // 利用 方向节点,
- // pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头
- // 通过 改变 方向节点 的坐标位置,引导蛇头的移动
- switch (ps->dir)
- {
- case UP:
- pNext->x = ps->pSnake->x;
- pNext->y = ps->pSnake->y - 1;
- break;
- case DOWN:
- pNext->x = ps->pSnake->x;
- pNext->y = ps->pSnake->y + 1;
- break;
- case LEFT:
- pNext->x = ps->pSnake->x - 2;
- pNext->y = ps->pSnake->y;
- break;
- case RIGHT:
- pNext->x = ps->pSnake->x + 2;
- pNext->y = ps->pSnake->y;
- break;
- }
- //下一个坐标处是否是食物
- if (NextIsFood(ps, pNext))
- {
- //是食物就吃掉:将 方向节点 链接到 蛇身上,将 食物指针 free 释放掉
- EatFood(ps, pNext);
- }
- else
- {
- //不是食物就正常一步: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
- NotEatFood(ps, pNext);
- }
- // getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了
-
- //检测撞墙
- KillByWall(ps);
- //是否撞到自己
- KillBySelf(ps);
- }
1、NextIsFood(ps, pNext):判断蛇头 下一步处 是否是 食物
// 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物 int NextIsFood(pSnake ps, pSnakeNode pNext) { // 两者坐标重合 就是了 if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物 else return 0; }
2、EatFood(ps, pNext); 是食物就吃掉
//下一步处 是食物就吃掉:将 方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉 void EatFood(pSnake ps, pSnakeNode pNext) { pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点 ps->pSnake = pNext; // 更新头指针 // 打印蛇身:每次更新一轮,就打印一次 // PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本 //打印蛇 pSnakeNode cur = ps->pSnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } // 分数变化: ps->Score += ps->FoodWeight; // 释放 旧食物节点:吃掉了 就不存在了 free(ps->pFood); //创建新食物; CreateFood(ps); }
3、 NotEatFood(ps, pNext); 不是食物就正常走一步
void NotEatFood(pSnake ps, pSnakeNode pNext) { // 头插法:链接方向指针,相当于移动了一步 pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点 ps->pSnake = pNext;// 更新头指针 // 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法 pSnakeNode 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;//易错 // 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点 // 打印蛇身:每次更新一轮,就打印一次 // PrintSnake(ps); }
4、 KillByWall(ps);// 检测撞墙
void KillByWall(pSnake ps) { if (ps->pSnake->x == 0 || ps->pSnake->x == 56 || ps->pSnake->y == 0 || ps->pSnake->y == 26) { ps->state = KILL_BY_WALL; } }
5、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->state = KILL_BY_SELF; return; } cur = cur->next; } }
- void GameEnd(pSnake ps)
- {
- SetPos(15, 12);
- switch (ps->state)
- {
- case ESC:
- printf("主动退出游戏\n");
- break;
- case KILL_BY_WALL:
- printf("很遗憾,撞墙了,游戏结束\n");
- break;
- case KILL_BY_SELF:
- printf("很遗憾,撞到自己,游戏结束\n");
- break;
- }
-
- //释放
- pSnakeNode cur = ps->pSnake;
- pSnakeNode del = NULL;
-
- while (cur)
- {
- del = cur;
- cur = cur->next;
- free(del);
- }
-
- free(ps->pFood);
- ps = NULL;
-
- }
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include"snake.h"
-
- //封装一个设置光标位置的函数 SetPos:
- void SetPos(int x, int y)
- {
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
- COORD pos = { x,y };
- SetConsoleCursorPosition(handle, pos);
- }
-
- // 打印欢迎信息
- void WelcomeToGame()
- {
- //一、 欢迎页面
- // 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30
- SetPos(38, 13); // 坐标的设置 看感觉来吧
- printf("欢迎来到贪吃蛇小游戏\n");
- // 打印完上面这句后:会有一句:请按任意键继续. . . 位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标
- SetPos(38, 16); // 请按任意键继续. . . 的位置
-
- system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . . 然后打印另一张
- // 清空屏幕信息:要打印下一张页面
- system("cls");
-
- //二、功能介绍页面
- SetPos(29, 8);
- printf("1、用 ↑ ↓ ← → 来控制蛇的移动 \n");
- SetPos(29, 10);
- printf("2、F3 是 加速,F4 是 减速 O(∩_∩)O\n");
- SetPos(29, 12);
- printf("3、加速可以获得更高的分数 (づ ̄ 3 ̄)づ\n");
- SetPos(38, 16); // 请按任意键继续. . . 的位置
- system("pause"); //一张页面打印完成后 暂停一下
- // 清空屏幕信息:要打印下一张页面
- system("cls");
- }
-
-
- // 绘制地图
- void CreateMap()
- {
- // 上:29个宽格子
- SetPos(0, 0);// 从 (0, 0)开始
- for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
- {
- wprintf(L"%c", WALL);
- }
- //下
- SetPos(0, 26);
- for (int i = 0; i <= 56; i += 2)
- {
- wprintf(L"%c", WALL);
- }
- // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的
- //左
- for (int i = 1; i <= 25; i++)
- {
- SetPos(0, i);
- wprintf(L"%c", WALL);
- }
- //右
- for (int i = 1; i <= 25; i++)
- {
- SetPos(56, i);
- wprintf(L"%c", WALL);
- }
- }
-
- // 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要
- // 初始化 蛇
- // 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
- // 创建5个节点,然后将每个节点存放在链表中进⾏管理。
- void InitSnake(pSnake ps)
- {
- // 创建5个节点:
- pSnakeNode cur = NULL;
- for (int i = 0; i < 5; i++)
- {
- cur = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (cur == NULL)
- {
- perror("InitSnake:malloc");
- return;
- }
- cur->x = POS_X + 2 * i;
- cur->y = POS_Y;
- cur->next = NULL;
-
- // 头插法:
- // 没有节点下
- if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
- {
- ps->pSnake = cur;
- }
- // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
- else
- {
- cur->next = ps->pSnake;
- ps->pSnake = cur;
- }
- }
- //打印蛇的身体
- // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
- // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
- cur = ps->pSnake;
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%lc", BODY);
- cur = cur->next;
- }
-
- // 初始化贪吃蛇的其他信息
- ps->dir = RIGHT; // 初始默认向右
- ps->FoodWeight = 10;
- ps->pFood = NULL;
- ps->Score = 0;
- ps->SleepTime = 200; // 休眠 200 ms
- ps->state = OK;
- }
-
- // 创建食物
- // 思路:
- // 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模
- // 2、x 的范围:2 ~ 54 ---> 生成 0 ~ 52 + 2 ---> rand()%53 + 2 因为 x 是占 2 个窄字符的, x 的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖
- // 3、y 的范围:1 ~ 25 ---> 生成 0 ~ 24 + 2 ---> rand()%25 + 1
- // 创建食物注意事项
- // 1.食物是随机出现的,坐标就是随机的
- // 2.坐标必须在墙内
- // 3.坐标不能在蛇的身体上
- void CreateFood(pSnake ps)
- {
- // 1.食物是随机出现的,坐标就是随机的
- // 2.坐标必须在墙内
- int x = 0;
- int y = 0;
-
- again:
- do
- {
- x = rand() % 53 + 2;
- y = rand() % 24 + 1;
- } while (x % 2 != 0); // 这一步啥意思呀:42min 左右
-
- // 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比
- //判断食物是否在蛇身上
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- if (x == cur->x && y == cur->y)
- {
- goto again;
- }
- cur = cur->next;
- }
-
- //创建食物: 一个新节点
- pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));// (pSnakeNode) 和 (SnakeNode) 千万别混淆!!!
- if (pFood == NULL)
- {
- perror("CreateFood():malloc()");
- return;
- }
- pFood->x = x;
- pFood->y = y;
- ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点
- // 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动
-
- //打印食物
- SetPos(x, y);
- wprintf(L"%c", FOOD);
- //getchar(); // 注意 :这个别忘了关闭
- }
-
-
-
- // 游戏前准备
- void GameStart(pSnake ps)
- {
- // 一、设置控制台的信息:窗口大小、窗口名
- // 设置控制台窗口的长宽:设置控制台窗口的大小
- // 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆 列为 100, 行 30
- system("mode con cols=100 lines=30");
- system("title 贪吃蛇");// 设置cmd窗口名称
-
- // 二、隐藏光标:不想光标在一直闪
- HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
- //隐藏光标操作: 6.5.1节 那里有示例
- CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
- GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
- //先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
- cursorinfo.bVisible = false; //隐藏控制台光标
- SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
-
- //打印欢迎信息
- WelcomeToGame();
- //绘制地图
- CreateMap();
- //初始化蛇
- InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
- //创建食物
- CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
- }
-
-
- void PrintHelpInfo()
- {
- // 设置位置
- SetPos(65, 8);
- printf("温馨提示\n");
- SetPos(65, 10);
- printf("1、不能穿墙\n");
- SetPos(65, 11);
- printf("2、不能咬到自己\n");
- SetPos(65, 12);
- printf("3、按 ESC 退出游戏\n");
- SetPos(65, 13);
- printf("4、按 空格 暂停游戏\n");
- SetPos(65, 15);
- printf("操作回顾\n");
- SetPos(65, 17);
- printf("5、用 ↑↓ ← → 来控制蛇的移动 \n");
- SetPos(65, 18);
- printf("6、F3 加速,F4 减速\n");
- SetPos(65, 19);
- printf("7、加速可以获得更高的分数\n");
- SetPos(65, 22);
- printf("版权 @时差\n");
- // getchar(); // 注意 :这个别忘了关闭
- }
-
- // 按 空格 :暂定和回复暂定
- void Pause()//游戏暂停
- {
- // tip : 死循环的 sleep , 再循环中 判断 再次点击空格键 即可回复
- while (1)
- {
- Sleep(200);
- if (KEY_PRESS(VK_SPACE))
- {
- break;
- }
- }
- }
-
- // 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物
- int NextIsFood(pSnake ps, pSnakeNode pNext)
- {
- // 两者坐标重合 就是了
- if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物
- else return 0;
- }
-
- //下一步处 是食物就吃掉:将 方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉
- void EatFood(pSnake ps, pSnakeNode pNext)
- {
- pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点
- ps->pSnake = pNext; // 更新头指针
-
- // 打印蛇身:每次更新一轮,就打印一次
- // PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本
- //打印蛇
- pSnakeNode cur = ps->pSnake;
- while (cur)
- {
- SetPos(cur->x, cur->y);
- wprintf(L"%c", BODY);
- cur = cur->next;
- }
-
- // 分数变化:
- ps->Score += ps->FoodWeight;
-
- // 释放 旧食物节点:吃掉了 就不存在了
- free(ps->pFood);
- //创建新食物;
- CreateFood(ps);
- }
-
- void NotEatFood(pSnake ps, pSnakeNode pNext)
- {
- // 头插法:链接方向指针,相当于移动了一步
- pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点
- ps->pSnake = pNext;// 更新头指针
-
- // 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法
- pSnakeNode 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;//易错
-
- // 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点
- // 打印蛇身:每次更新一轮,就打印一次
- // PrintSnake(ps);
- }
-
- void KillByWall(pSnake ps)
- {
- if (ps->pSnake->x == 0 ||
- ps->pSnake->x == 56 ||
- ps->pSnake->y == 0 ||
- ps->pSnake->y == 26)
- {
- ps->state = KILL_BY_WALL;
- }
- }
-
- void KillBySelf(pSnake ps)
- {
- pSnakeNode cur = ps->pSnake->next;
- while (cur)
- {
- if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
- {
- ps->state = KILL_BY_SELF;
- return;
- }
- cur = cur->next;
- }
- }
-
- // 蛇身 移动
- // 移动的本质: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
- void SnakeMove(pSnake ps)
- {
- // 根据按键:调整移动方向
- // 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min
- // 取名:方向节点
- pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
- if (pNext == NULL)
- {
- perror("SnakeMove():malloc:");
- return;
- }
- pNext->next = NULL;// 指向空就好
-
- // 利用 方向节点,
- // pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头
- // 通过 改变 方向节点 的坐标位置,引导蛇头的移动
- switch (ps->dir)
- {
- case UP:
- pNext->x = ps->pSnake->x;
- pNext->y = ps->pSnake->y - 1;
- break;
- case DOWN:
- pNext->x = ps->pSnake->x;
- pNext->y = ps->pSnake->y + 1;
- break;
- case LEFT:
- pNext->x = ps->pSnake->x - 2;
- pNext->y = ps->pSnake->y;
- break;
- case RIGHT:
- pNext->x = ps->pSnake->x + 2;
- pNext->y = ps->pSnake->y;
- break;
- }
- //下一个坐标处是否是食物
- if (NextIsFood(ps, pNext))
- {
- //是食物就吃掉:将 方向节点 链接到 蛇身上,将 食物指针 free 释放掉
- EatFood(ps, pNext);
- }
- else
- {
- //不是食物就正常一步: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
- NotEatFood(ps, pNext);
- }
- // getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了
-
- //检测撞墙
- KillByWall(ps);
- //是否撞到自己
- KillBySelf(ps);
- }
-
- // 游戏运行过程
- void GameRun(pSnake ps)
- {
- // 一、打印界面右侧的帮助提示信息
- PrintHelpInfo();
- // 游戏一般 是 do while循环:总之都需要先让游戏先运行一次
-
- do
- {
- //当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
- SetPos(65, 4);
- //[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
- // SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | BACKGROUND_INTENSITY);
- CONSOLE_FONT_INFOEX cfi;
- cfi.cbSize = sizeof cfi;
- cfi.dwFontSize.X = 0; //字宽
- cfi.dwFontSize.Y = 20;//字高
- cfi.FontWeight = FW_NORMAL;//粗细
- SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
-
- printf("总分:%5d", ps->Score);
- SetPos(65, 6 );
- printf("食物的分值:%02d", ps->FoodWeight);
-
- // 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
- // 上下左右,ESC,空格,F3,F4
- // 不能同时 方向 等于 相反方向
- if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于 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_ESCAPE))
- {
- ps->state = ESC; // 将状态该成 退出状态
- break;
- }
- else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
- {
- //游戏要暂定
- // 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
- Pause();//暂定和回复暂定
- }
- else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
- {
- if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
- {
- ps->SleepTime -= 30;
- ps->FoodWeight += 2;
- }
- }
- else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
- {
- if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
- {
- ps->SleepTime += 30;
- ps->FoodWeight -= 2;
- }
- }
- // 三 四 步 别写反
- // 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
- // 蛇身移动较为复杂,封装成一个函数便于分析
- SnakeMove(ps);
- // 四、睡眠一下
- Sleep(ps->SleepTime);
-
- } while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束
-
- }
-
- void GameEnd(pSnake ps)
- {
- SetPos(15, 12);
- switch (ps->state)
- {
- case ESC:
- printf("主动退出游戏\n");
- break;
- case KILL_BY_WALL:
- printf("很遗憾,撞墙了,游戏结束\n");
- break;
- case KILL_BY_SELF:
- printf("很遗憾,撞到自己,游戏结束\n");
- break;
- }
-
- //释放
- pSnakeNode cur = ps->pSnake;
- pSnakeNode del = NULL;
-
- while (cur)
- {
- del = cur;
- cur = cur->next;
- free(del);
- }
-
- free(ps->pFood);
- ps = NULL;
-
- }
- #pragma once
- #include<locale.h>
- #include<stdlib.h>
- #include<stdio.h>
- #include<windows.h>
- #include<stdbool.h>
- #define WALL L'□' // 因为 每次打印都要写一次 L什么什么的 还不如直接定义一个宏
- #define BODY L'●' // 打印是 蛇身
- #define FOOD L'※' // 打印食物
- //Đʘ◨ↇↀↈ:不行的
- // D●
- // 蛇 默认的起始坐标位置:要确定 起始的蛇身 每个 节点的 坐标
-
- #define POS_X 24//蛇初始坐标
- #define POS_Y 5
-
- #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
- // 枚举类型不熟的可以去翻笔记
- // enum 就是 集合版的 define
-
-
- //蛇走的方向
- enum DIRECTION
- {
- UP = 1, // 上
- DOWN, // 下
- LEFT, // 左
- RIGHT // 右
- };
-
- //蛇的状态: 维护游戏状态的 枚举类型
- enum GAME_STATE
- {
- OK,//正常运行
- ESC, // 按 ESC 键退出
- KILL_BY_WALL,//撞墙
- KILL_BY_SELF,//咬到自己
- //END_NOMAL//正常结束
- };
-
- // 蛇身节点
- // 蛇身用链表来维护:一个蛇身节点就是一个链表节点
- // 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来
- typedef struct SnakeNode
- {
- int x;
- int y;
- struct SnakeNode* next;
- }SnakeNode, * pSnakeNode;
- // 创建一个指向蛇身节点的结构体指针
-
-
- //贪吃蛇
- // 整条蛇
- // 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇:
- typedef struct Snake
- {
- pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头
- pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样
- enum DIRECTION dir;//蛇头的方向默认是向右
- enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型)
- int Score;//当前获得分数
- int FoodWeight;//默认每个食物 10 分
- int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度)
- }Snake, * pSnake; // pSnake 是 指向贪吃蛇的指针: 下面就用上了,其实就是 Snake*
-
- //定位控制台光标位置
- void SetPos(int x, int y);
-
- //游戏开始前的准备
- void GameStart(pSnake ps);
-
- //欢迎界面
- void WelcomeToGame();//打印欢迎信息
-
- //绘制地图
- void CreateMap();//创建地图
-
- //初始化蛇
- void InitSnake(pSnake ps);
-
- //创建食物
- void CreateFood(pSnake ps);
-
- //游戏运行的整个逻辑
- void GameRun(pSnake ps);
-
- //打印帮助信息
- void PrintHelpInfo();
-
- //蛇移动的函数- 每次走一步
- void SnakeMove(pSnake ps);
-
- //判断蛇头的下一步要走的位置处是否是食物
- int NextIsFood(pSnake ps, pSnakeNode pNext);
-
- //下一步要走的位置处就是食物,就吃掉食物
- void EatFood(pSnake ps, pSnakeNode pNext);
-
- //下一步要走的位置处不是食物,不吃食物
- void NotEatFood(pSnake ps, pSnakeNode pNext);
-
- //检测撞墙
- void KillByWall(pSnake ps);
-
- //撞到自己
- void KillBySelf(pSnake ps);
-
- //游戏结束资源释放
- void GameEnd(pSnake ps);
-
- #define _CRT_SECURE_NO_WARNINGS 1
- #include"snake.h"
-
- void test()
- {
- int ch = 0;
- do
- {
- // 创建一条贪吃蛇
- Snake snake = { 0 };
- // 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
- GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
- GameRun(&snake); // 游戏过程
- GameEnd(&snake);//善后工作
- SetPos(20, 15);
- printf("再来一局?(y/n)");
- ch = getchar();
- getchar();
- } while (ch == 'Y' || ch == 'y');
-
- }
- int main()
- {
- //适配本地中文环境
- setlocale(LC_ALL, "");
-
- //贪吃蛇游戏的设置
- test();
- return 0;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。