当前位置:   article > 正文

【C语言】贪吃蛇 详解

【C语言】贪吃蛇 详解

该项目需要的技术要点

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。

由于篇幅限制 和 使知识模块化

若想了解 使用到的 Win32API 的知识:请点击跳转:【Win32API】贪吃蛇会使用到的 Win32API

目录

1. 贪吃蛇游戏设计与分析

1.0 贪吃蛇页面大纲

1.1 地图

1.1.1 控制台窗口的坐标知识

1.1.2 宽字符:

1.1.3 地图坐标

1.2 蛇身和食物

1.3 数据结构设计

1.4 整个游戏流程设计

2. 核⼼逻辑实现分析

2.1 游戏主逻辑 

2.2 游戏开始

2.3 游戏运行

2.4 游戏结束

3.总代码概览

Snake.c

Snake.h

 test.c



准备工作:创建三个文件

1. 贪吃蛇游戏设计与分析

1.0 贪吃蛇页面大纲

 

 我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?

1.1 地图

这里不得不讲一下控制台窗口的一些知识

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 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
  1. #include<locale.h> // 包含头文件
  2. setlocale(LC_ALL, "" );//切换到本地环境:中文环境

(3)宽字符 的 打印格式
宽字符要 在单引号前面加上 L,表示宽字符,宽字符的打印用wprintf,对应wprintf()的占位符为%lc;

假设 A 为一个窄字符,B 为 宽字符

  1. 窄字符:printf("%c", A)
  2. 宽字符:wprintf(L"%lc", B);
从输出的结果来看,我们发现一个普通字符占一个字符的位置
但是打印一个宽字符,占用 2 个字符的位置,那么我们如果
要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

下图,边框上的墙体就是 一种宽字符:□ ,宽度占 2 个字符位

1个坐标可以放1个正常字符,即窄字符

2个坐标可以存放1个宽字符 

1.1.3 地图坐标

我们假设实现一个棋盘 27 行, 58 列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,因为 宽字符的原因:最好 是    2 * 行数  = 列数
如下:
  1. void CreateMap()
  2. {
  3. // 上:29个宽格子
  4. SetPos(0, 0);// 从 (0, 0)开始
  5. for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
  6. {
  7. wprintf(L"%c", WALL);
  8. }
  9. //下
  10. SetPos(0, 26);
  11. for (int i = 0; i <= 56; i += 2)
  12. {
  13. wprintf(L"%c", WALL);
  14. }
  15. // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的
  16. //左
  17. for (int i = 1; i <= 25; i++)
  18. {
  19. SetPos(0, i);
  20. wprintf(L"%c", WALL);
  21. }
  22. //右
  23. for (int i = 1; i <= 25; i++)
  24. {
  25. SetPos(56, i);
  26. wprintf(L"%c", WALL);
  27. }
  28. }

1.2 蛇身和食物

初始化状态,假设蛇的长度是 5 ,蛇身的每个节点是●,在固定的一个坐标处,比如 (24,5) 处开始出现蛇,连续 5 个节点。 注意: 蛇的每个节点的 x 坐标必须是 2 的倍数, 即偶数,否则撞墙时可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是 2 的倍数)坐标不能和蛇的身体重合,然后打印★。

  1. void InitSnake(pSnake ps)
  2. {
  3. // 创建5个节点:
  4. pSnakeNode cur = NULL;
  5. for (int i = 0; i < 5; i++)
  6. {
  7. cur = (pSnakeNode)malloc(sizeof(SnakeNode));
  8. if (cur == NULL)
  9. {
  10. perror("InitSnake:malloc");
  11. return;
  12. }
  13. cur->x = POS_X + 2 * i;
  14. cur->y = POS_Y;
  15. cur->next = NULL;
  16. // 头插法:
  17. // 没有节点下
  18. if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
  19. {
  20. ps->pSnake = cur;
  21. }
  22. // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
  23. else
  24. {
  25. cur->next = ps->pSnake;
  26. ps->pSnake = cur;
  27. }
  28. }
  29. //打印蛇的身体
  30. // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
  31. // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
  32. cur = ps->pSnake;
  33. while (cur)
  34. {
  35. SetPos(cur->x, cur->y);
  36. wprintf(L"%lc", BODY);
  37. cur = cur->next;
  38. }
  39. // 初始化贪吃蛇的其他信息
  40. ps->dir = RIGHT; // 初始默认向右
  41. ps->FoodWeight = 10;
  42. ps->pFood = NULL;
  43. ps->Score = 0;
  44. ps->SleepTime = 200; // 休眠 200 ms
  45. ps->state = OK;
  46. }

1.3 数据结构设计

在游戏运行的过程中,蛇每次吃一个⻝物,蛇的身体就会变⻓一节,如果我们使用 链表 存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,
所以蛇节点结构如下:
  1. // 蛇身节点
  2. // 蛇身用链表来维护:一个蛇身节点就是一个链表节点
  3. // 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来
  4. typedef struct SnakeNode
  5. {
  6. int x;
  7. int y;
  8. struct SnakeNode* next;
  9. }SnakeNode, * pSnakeNode;
  10. // 创建一个指向蛇身节点的结构体指针

要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇:

  1. //贪吃蛇
  2. // 整条蛇
  3. // 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇:
  4. typedef struct Snake
  5. {
  6. pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头
  7. pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样
  8. enum DIRECTION dir;//蛇头的方向默认是向右
  9. enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型)
  10. int Score;//当前获得分数
  11. int FoodWeight;//默认每个食物 10 分
  12. int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度)
  13. }Snake, * pSnake; // pSnake 是 指向贪吃蛇的指针: 下面就用上了

蛇的方向:可以一一列举,使用枚举

  1. //蛇走的方向
  2. enum DIRECTION
  3. {
  4. UP = 1, // 上
  5. DOWN, // 下
  6. LEFT, // 左
  7. RIGHT // 右
  8. };

游戏状态:可以一一列举,使用枚举

  1. /蛇的状态: 维护游戏状态的 枚举类型
  2. enum GAME_STATE
  3. {
  4. OK,//正常运行
  5. ESC, // 按 ESC 键退出
  6. KILL_BY_WALL,//撞墙
  7. KILL_BY_SELF,//咬到自己
  8. //END_NOMAL//正常结束
  9. };

1.4 整个游戏流程设计

2. 核⼼逻辑实现分析

2.1 游戏主逻辑 

GameStart(&snake);   游戏开始
GameRun(&snake);    游戏过程
GameEnd(&snake);    游戏结束

  1. #include"snake.h"
  2. void test()
  3. {
  4. int ch = 0;
  5. do
  6. {
  7. // 创建一条贪吃蛇
  8. Snake snake = { 0 };
  9. // 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
  10. GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
  11. GameRun(&snake); // 游戏过程
  12. GameEnd(&snake);//善后工作
  13. SetPos(20, 15);
  14. printf("再来一局?(y/n)");
  15. ch = getchar();
  16. getchar();
  17. } while (ch == 'Y' || ch == 'y');
  18. }
  19. int main()
  20. {
  21. //适配本地中文环境
  22. setlocale(LC_ALL, "");
  23. //贪吃蛇游戏的设置
  24. test();
  25. return 0;
  26. }

2.2 游戏开始

    一、打印欢迎信息
    WelcomeToGame();
    二、绘制地图
    CreateMap(); 
    三、初始化蛇
    InitSnake(ps); 
    四、创建食物
    CreateFood(ps);

  1. // 游戏前准备
  2. void GameStart(pSnake ps)
  3. {
  4. // 一、设置控制台的信息:窗口大小、窗口名
  5. // 设置控制台窗口的长宽:设置控制台窗口的大小
  6. // 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆 列为 100, 行 30
  7. system("mode con cols=100 lines=30");
  8. system("title 贪吃蛇");// 设置cmd窗口名称
  9. // 二、隐藏光标:不想光标在一直闪
  10. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  11. //隐藏光标操作: 6.5.1节 那里有示例
  12. CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
  13. GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
  14. //先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
  15. cursorinfo.bVisible = false; //隐藏控制台光标
  16. SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
  17. //一、打印欢迎信息
  18. WelcomeToGame();
  19. //二、绘制地图
  20. CreateMap();
  21. //三、初始化蛇
  22. InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
  23. //四、创建食物
  24. CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
  25. }

游戏前准备  GameStart(pSnake ps)   的 四个子函数

先封装 设置坐标函数

  1. //封装一个设置光标位置的函数 SetPos:
  2. void SetPos(int x, int y)
  3. {
  4. //获取标准输出设备的句柄
  5. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  6. //设置标准输出上光标的位置为pos
  7. COORD pos = { x,y };
  8. SetConsoleCursorPosition(handle, pos);
  9. }

 一、打印欢迎信息

  1. void WelcomeToGame()
  2. {
  3. //一、 欢迎页面
  4. // 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30
  5. SetPos(38, 13); // 坐标的设置 看感觉来吧
  6. printf("欢迎来到贪吃蛇小游戏\n");
  7. // 打印完上面这句后:会有一句:请按任意键继续. . . 位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标
  8. SetPos(38, 16); // 请按任意键继续. . . 的位置
  9. system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . . 然后打印另一张
  10. // 清空屏幕信息:要打印下一张页面
  11. system("cls");
  12. //二、功能介绍页面
  13. SetPos(29, 8);
  14. printf("1、用 ↑ ↓ ← → 来控制蛇的移动 \n");
  15. SetPos(29, 10);
  16. printf("2、F3 是 加速,F4 是 减速 O(∩_∩)O\n");
  17. SetPos(29, 12);
  18. printf("3、加速可以获得更高的分数 (づ ̄ 3 ̄)づ\n");
  19. SetPos(38, 16); // 请按任意键继续. . . 的位置
  20. system("pause"); //一张页面打印完成后 暂停一下
  21. // 清空屏幕信息:要打印下一张页面
  22. system("cls");
  23. }

 

 二、绘制地图

  1. void CreateMap()
  2. {
  3. // 上:29个宽格子
  4. SetPos(0, 0);// 从 (0, 0)开始
  5. for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
  6. {
  7. wprintf(L"%c", WALL);
  8. }
  9. //下
  10. SetPos(0, 26);
  11. for (int i = 0; i <= 56; i += 2)
  12. {
  13. wprintf(L"%c", WALL);
  14. }
  15. // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的
  16. //左
  17. for (int i = 1; i <= 25; i++)
  18. {
  19. SetPos(0, i);
  20. wprintf(L"%c", WALL);
  21. }
  22. //右
  23. for (int i = 1; i <= 25; i++)
  24. {
  25. SetPos(56, i);
  26. wprintf(L"%c", WALL);
  27. }
  28. }

三、初始化 蛇

  1. // 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要
  2. // 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
  3. // 创建5个节点,然后将每个节点存放在链表中进⾏管理。
  4. void InitSnake(pSnake ps)
  5. {
  6. // 创建5个节点:
  7. pSnakeNode cur = NULL;
  8. for (int i = 0; i < 5; i++)
  9. {
  10. cur = (pSnakeNode)malloc(sizeof(SnakeNode));
  11. if (cur == NULL)
  12. {
  13. perror("InitSnake:malloc");
  14. return;
  15. }
  16. cur->x = POS_X + 2 * i;
  17. cur->y = POS_Y;
  18. cur->next = NULL;
  19. // 头插法:
  20. // 没有节点下
  21. if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
  22. {
  23. ps->pSnake = cur;
  24. }
  25. // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
  26. else
  27. {
  28. cur->next = ps->pSnake;
  29. ps->pSnake = cur;
  30. }
  31. }
  32. //打印蛇的身体
  33. // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
  34. // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
  35. cur = ps->pSnake;
  36. while (cur)
  37. {
  38. SetPos(cur->x, cur->y);
  39. wprintf(L"%lc", BODY);
  40. cur = cur->next;
  41. }
  42. // 初始化贪吃蛇的其他信息
  43. ps->dir = RIGHT; // 初始默认向右
  44. ps->FoodWeight = 10;
  45. ps->pFood = NULL;
  46. ps->Score = 0;
  47. ps->SleepTime = 200; // 休眠 200 ms
  48. ps->state = OK;
  49. }

 四、创建食物

  1. // 思路:
  2. // 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模
  3. // 2、x 的范围:2 ~ 54 ---> 生成 0 ~ 52 + 2 ---> rand()%53 + 2 因为 x 是占 2 个窄字符的, x 的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖
  4. // 3、y 的范围:1 ~ 25 ---> 生成 0 ~ 24 + 2 ---> rand()%25 + 1
  5. // 创建食物注意事项
  6. // 1.食物是随机出现的,坐标就是随机的
  7. // 2.坐标必须在墙内
  8. // 3.坐标不能在蛇的身体上
  9. void CreateFood(pSnake ps)
  10. {
  11. // 1.食物是随机出现的,坐标就是随机的
  12. // 2.坐标必须在墙内
  13. int x = 0;
  14. int y = 0;
  15. again:
  16. do
  17. {
  18. x = rand() % 53 + 2;
  19. y = rand() % 24 + 1;
  20. } while (x % 2 != 0); // 这一步啥意思呀:42min 左右
  21. // 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比
  22. //判断食物是否在蛇身上
  23. pSnakeNode cur = ps->pSnake;
  24. while (cur)
  25. {
  26. if (x == cur->x && y == cur->y)
  27. {
  28. goto again;
  29. }
  30. cur = cur->next;
  31. }
  32. //创建食物: 一个新节点
  33. pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));// (pSnakeNode) 和 (SnakeNode) 千万别混淆!!!
  34. if (pFood == NULL)
  35. {
  36. perror("CreateFood():malloc()");
  37. return;
  38. }
  39. pFood->x = x;
  40. pFood->y = y;
  41. ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点
  42. // 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动
  43. //打印食物
  44. SetPos(x, y);
  45. wprintf(L"%c", FOOD);
  46. //getchar(); // 注意 :这个别忘了关闭
  47. }

2.3 游戏运行

游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

确定了蛇的方向和速度,蛇就可以移动了。

 

  1. // 游戏运行过程
  2. void GameRun(pSnake ps)
  3. {
  4. // 一、打印界面右侧的帮助提示信息
  5. PrintHelpInfo();
  6. // 游戏一般 是 do while循环:总之都需要先让游戏先运行一次
  7. do
  8. {
  9. //当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
  10. SetPos(65, 4);
  11. //[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
  12. // SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | BACKGROUND_INTENSITY);
  13. CONSOLE_FONT_INFOEX cfi;
  14. cfi.cbSize = sizeof cfi;
  15. cfi.dwFontSize.X = 0; //字宽
  16. cfi.dwFontSize.Y = 20;//字高
  17. cfi.FontWeight = FW_NORMAL;//粗细
  18. SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
  19. printf("总分:%5d", ps->Score);
  20. SetPos(65, 6 );
  21. printf("食物的分值:%02d", ps->FoodWeight);
  22. // 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
  23. // 上下左右,ESC,空格,F3,F4
  24. // 不能同时 方向 等于 相反方向
  25. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于 DOWN !!!!
  26. {
  27. ps->dir = UP;
  28. }
  29. else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
  30. {
  31. ps->dir = DOWN;
  32. }
  33. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
  34. {
  35. ps->dir = LEFT;
  36. }
  37. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
  38. {
  39. ps->dir = RIGHT;
  40. }
  41. else if (KEY_PRESS(VK_ESCAPE))
  42. {
  43. ps->state = ESC; // 将状态该成 退出状态
  44. break;
  45. }
  46. else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
  47. {
  48. //游戏要暂定
  49. // 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
  50. Pause();//暂定和回复暂定
  51. }
  52. else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
  53. {
  54. if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
  55. {
  56. ps->SleepTime -= 30;
  57. ps->FoodWeight += 2;
  58. }
  59. }
  60. else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
  61. {
  62. if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
  63. {
  64. ps->SleepTime += 30;
  65. ps->FoodWeight -= 2;
  66. }
  67. }
  68. // 三 四 步 别写反
  69. // 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
  70. // 蛇身移动较为复杂,封装成一个函数便于分析
  71. SnakeMove(ps);
  72. // 四、睡眠一下
  73. Sleep(ps->SleepTime);
  74. } while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束
  75. }

游戏运行过程函数:GameRun(pSnake ps) 的 几个子函数

一、PrintHelpInfo():打印界面右侧的帮助提示信息

二、Pause():游戏暂停键

三、SnakeMove(ps):蛇身移动(较为复杂)

一、PrintHelpInfo():打印界面右侧的帮助提示信息

  1. void PrintHelpInfo()
  2. {
  3. // 设置位置
  4. SetPos(65, 8);
  5. printf("温馨提示\n");
  6. SetPos(65, 10);
  7. printf("1、不能穿墙\n");
  8. SetPos(65, 11);
  9. printf("2、不能咬到自己\n");
  10. SetPos(65, 12);
  11. printf("3、按 ESC 退出游戏\n");
  12. SetPos(65, 13);
  13. printf("4、按 空格 暂停游戏\n");
  14. SetPos(65, 15);
  15. printf("操作回顾\n");
  16. SetPos(65, 17);
  17. printf("5、用 ↑↓ ← → 来控制蛇的移动 \n");
  18. SetPos(65, 18);
  19. printf("6、F3 加速,F4 减速\n");
  20. SetPos(65, 19);
  21. printf("7、加速可以获得更高的分数\n");
  22. SetPos(65, 22);
  23. printf("版权 @时差\n");
  24. // getchar(); // 注意 :这个别忘了关闭
  25. }

二、Pause():游戏暂停键

  1. void Pause()//游戏暂停
  2. {
  3. // tip : 死循环的 sleep , 再循环中 判断 再次点击空格键 即可回复
  4. while (1)
  5. {
  6. Sleep(200);
  7. if (KEY_PRESS(VK_SPACE))
  8. {
  9. break;
  10. }
  11. }
  12. }

三、SnakeMove(ps):蛇身移动(较为复杂)

分成几个子函数:

  1. NextIsFood(ps, pNext):判断蛇头 下一步处 是否是 食物
  2. EatFood(ps, pNext); 是食物就吃掉
  3. NotEatFood(ps, pNext); 不是食物就正常走一步
  4. KillByWall(ps);// 检测撞墙
  5. KillBySelf(ps);// 是否撞到自己

  1. // 蛇身 移动
  2. // 移动的本质: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
  3. void SnakeMove(pSnake ps)
  4. {
  5. // 根据按键:调整移动方向
  6. // 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min
  7. // 取名:方向节点
  8. pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
  9. if (pNext == NULL)
  10. {
  11. perror("SnakeMove():malloc:");
  12. return;
  13. }
  14. pNext->next = NULL;// 指向空就好
  15. // 利用 方向节点,
  16. // pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头
  17. // 通过 改变 方向节点 的坐标位置,引导蛇头的移动
  18. switch (ps->dir)
  19. {
  20. case UP:
  21. pNext->x = ps->pSnake->x;
  22. pNext->y = ps->pSnake->y - 1;
  23. break;
  24. case DOWN:
  25. pNext->x = ps->pSnake->x;
  26. pNext->y = ps->pSnake->y + 1;
  27. break;
  28. case LEFT:
  29. pNext->x = ps->pSnake->x - 2;
  30. pNext->y = ps->pSnake->y;
  31. break;
  32. case RIGHT:
  33. pNext->x = ps->pSnake->x + 2;
  34. pNext->y = ps->pSnake->y;
  35. break;
  36. }
  37. //下一个坐标处是否是食物
  38. if (NextIsFood(ps, pNext))
  39. {
  40. //是食物就吃掉:将 方向节点 链接到 蛇身上,将 食物指针 free 释放掉
  41. EatFood(ps, pNext);
  42. }
  43. else
  44. {
  45. //不是食物就正常一步: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
  46. NotEatFood(ps, pNext);
  47. }
  48. // getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了
  49. //检测撞墙
  50. KillByWall(ps);
  51. //是否撞到自己
  52. KillBySelf(ps);
  53. }

 1、NextIsFood(ps, pNext):判断蛇头 下一步处 是否是 食物

  1. // 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物
  2. int NextIsFood(pSnake ps, pSnakeNode pNext)
  3. {
  4. // 两者坐标重合 就是了
  5. if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物
  6. else return 0;
  7. }

 2、EatFood(ps, pNext); 是食物就吃掉

  1. //下一步处 是食物就吃掉:将 方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉
  2. void EatFood(pSnake ps, pSnakeNode pNext)
  3. {
  4. pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点
  5. ps->pSnake = pNext; // 更新头指针
  6. // 打印蛇身:每次更新一轮,就打印一次
  7. // PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本
  8. //打印蛇
  9. pSnakeNode cur = ps->pSnake;
  10. while (cur)
  11. {
  12. SetPos(cur->x, cur->y);
  13. wprintf(L"%c", BODY);
  14. cur = cur->next;
  15. }
  16. // 分数变化:
  17. ps->Score += ps->FoodWeight;
  18. // 释放 旧食物节点:吃掉了 就不存在了
  19. free(ps->pFood);
  20. //创建新食物;
  21. CreateFood(ps);
  22. }

3、 NotEatFood(ps, pNext); 不是食物就正常走一步

  1. void NotEatFood(pSnake ps, pSnakeNode pNext)
  2. {
  3. // 头插法:链接方向指针,相当于移动了一步
  4. pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点
  5. ps->pSnake = pNext;// 更新头指针
  6. // 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法
  7. pSnakeNode cur = ps->pSnake;
  8. while (cur->next->next)
  9. {
  10. SetPos(cur->x, cur->y);
  11. wprintf(L"%lc", BODY);
  12. cur = cur->next;
  13. }
  14. // 将尾节点的位置 打印成 空白字符:不能 还打印蛇身
  15. // 先 打印成 空白,后再释放:别急着释放
  16. SetPos(cur->next->x, cur->next->y);
  17. printf(" ");// 注意 空格是 两个
  18. free(cur->next);
  19. cur->next = NULL;//易错
  20. // 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点
  21. // 打印蛇身:每次更新一轮,就打印一次
  22. // PrintSnake(ps);
  23. }

4、 KillByWall(ps);// 检测撞墙

  1. void KillByWall(pSnake ps)
  2. {
  3. if (ps->pSnake->x == 0 ||
  4. ps->pSnake->x == 56 ||
  5. ps->pSnake->y == 0 ||
  6. ps->pSnake->y == 26)
  7. {
  8. ps->state = KILL_BY_WALL;
  9. }
  10. }

5、KillBySelf(ps);// 是否撞到自己

  1. void KillBySelf(pSnake ps)
  2. {
  3. pSnakeNode cur = ps->pSnake->next;
  4. while (cur)
  5. {
  6. if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
  7. {
  8. ps->state = KILL_BY_SELF;
  9. return;
  10. }
  11. cur = cur->next;
  12. }
  13. }

2.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
  1. void GameEnd(pSnake ps)
  2. {
  3. SetPos(15, 12);
  4. switch (ps->state)
  5. {
  6. case ESC:
  7. printf("主动退出游戏\n");
  8. break;
  9. case KILL_BY_WALL:
  10. printf("很遗憾,撞墙了,游戏结束\n");
  11. break;
  12. case KILL_BY_SELF:
  13. printf("很遗憾,撞到自己,游戏结束\n");
  14. break;
  15. }
  16. //释放
  17. pSnakeNode cur = ps->pSnake;
  18. pSnakeNode del = NULL;
  19. while (cur)
  20. {
  21. del = cur;
  22. cur = cur->next;
  23. free(del);
  24. }
  25. free(ps->pFood);
  26. ps = NULL;
  27. }

3.总代码概览

Snake.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"snake.h"
  3. //封装一个设置光标位置的函数 SetPos:
  4. void SetPos(int x, int y)
  5. {
  6. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  7. COORD pos = { x,y };
  8. SetConsoleCursorPosition(handle, pos);
  9. }
  10. // 打印欢迎信息
  11. void WelcomeToGame()
  12. {
  13. //一、 欢迎页面
  14. // 1、改变打印光标的坐标: 要处在 窗口中央,前面设置窗口 : 列为 100, 行 30
  15. SetPos(38, 13); // 坐标的设置 看感觉来吧
  16. printf("欢迎来到贪吃蛇小游戏\n");
  17. // 打印完上面这句后:会有一句:请按任意键继续. . . 位置不好看,可以再次设置光标位置:其实本质上就是一个新的光标
  18. SetPos(38, 16); // 请按任意键继续. . . 的位置
  19. system("pause"); //一张页面打印完成后 暂停一下:请按任意键继续. . . 然后打印另一张
  20. // 清空屏幕信息:要打印下一张页面
  21. system("cls");
  22. //二、功能介绍页面
  23. SetPos(29, 8);
  24. printf("1、用 ↑ ↓ ← → 来控制蛇的移动 \n");
  25. SetPos(29, 10);
  26. printf("2、F3 是 加速,F4 是 减速 O(∩_∩)O\n");
  27. SetPos(29, 12);
  28. printf("3、加速可以获得更高的分数 (づ ̄ 3 ̄)づ\n");
  29. SetPos(38, 16); // 请按任意键继续. . . 的位置
  30. system("pause"); //一张页面打印完成后 暂停一下
  31. // 清空屏幕信息:要打印下一张页面
  32. system("cls");
  33. }
  34. // 绘制地图
  35. void CreateMap()
  36. {
  37. // 上:29个宽格子
  38. SetPos(0, 0);// 从 (0, 0)开始
  39. for (int i = 0; i <= 56; i += 2) // 每隔两格打印一个 宽字符
  40. {
  41. wprintf(L"%c", WALL);
  42. }
  43. //下
  44. SetPos(0, 26);
  45. for (int i = 0; i <= 56; i += 2)
  46. {
  47. wprintf(L"%c", WALL);
  48. }
  49. // 打印 左右 坐标需要跟着不断变化 :因为 默认打印是 横着打印的
  50. //左
  51. for (int i = 1; i <= 25; i++)
  52. {
  53. SetPos(0, i);
  54. wprintf(L"%c", WALL);
  55. }
  56. //右
  57. for (int i = 1; i <= 25; i++)
  58. {
  59. SetPos(56, i);
  60. wprintf(L"%c", WALL);
  61. }
  62. }
  63. // 其实,再屏幕上面显示出来相应坐标的 打印数据,本质是要 定位到 相应坐标,SetPos 函数 很重要
  64. // 初始化 蛇
  65. // 蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
  66. // 创建5个节点,然后将每个节点存放在链表中进⾏管理。
  67. void InitSnake(pSnake ps)
  68. {
  69. // 创建5个节点:
  70. pSnakeNode cur = NULL;
  71. for (int i = 0; i < 5; i++)
  72. {
  73. cur = (pSnakeNode)malloc(sizeof(SnakeNode));
  74. if (cur == NULL)
  75. {
  76. perror("InitSnake:malloc");
  77. return;
  78. }
  79. cur->x = POS_X + 2 * i;
  80. cur->y = POS_Y;
  81. cur->next = NULL;
  82. // 头插法:
  83. // 没有节点下
  84. if (ps->pSnake == NULL) // pSnake 是 指向蛇头 的指针:即 单链表中 的头指针
  85. {
  86. ps->pSnake = cur;
  87. }
  88. // 已经有节点 :原本不是有 ps->pSnake == cur; pSnake 存放着 上一个节点的地址,借这个来 链接下一个节点,然后更新 pSnake, 最后 pSnake 一直保持指向蛇头
  89. else
  90. {
  91. cur->next = ps->pSnake;
  92. ps->pSnake = cur;
  93. }
  94. }
  95. //打印蛇的身体
  96. // 打印 蛇身: 找到蛇头指针,循环遍历 打印相应坐标 就行
  97. // 注意:这个 cur 和上面的 cur 含义 不同,其实是 临时变量的 意思
  98. cur = ps->pSnake;
  99. while (cur)
  100. {
  101. SetPos(cur->x, cur->y);
  102. wprintf(L"%lc", BODY);
  103. cur = cur->next;
  104. }
  105. // 初始化贪吃蛇的其他信息
  106. ps->dir = RIGHT; // 初始默认向右
  107. ps->FoodWeight = 10;
  108. ps->pFood = NULL;
  109. ps->Score = 0;
  110. ps->SleepTime = 200; // 休眠 200 ms
  111. ps->state = OK;
  112. }
  113. // 创建食物
  114. // 思路:
  115. // 1、食物随机出现,就是 随机一个坐标打印,难点:运算 随机数的取模
  116. // 2、x 的范围:2 ~ 54 ---> 生成 0 ~ 52 + 2 ---> rand()%53 + 2 因为 x 是占 2 个窄字符的, x 的取值必须是 偶数,不然蛇 吃 不全,即 不能全覆盖
  117. // 3、y 的范围:1 ~ 25 ---> 生成 0 ~ 24 + 2 ---> rand()%25 + 1
  118. // 创建食物注意事项
  119. // 1.食物是随机出现的,坐标就是随机的
  120. // 2.坐标必须在墙内
  121. // 3.坐标不能在蛇的身体上
  122. void CreateFood(pSnake ps)
  123. {
  124. // 1.食物是随机出现的,坐标就是随机的
  125. // 2.坐标必须在墙内
  126. int x = 0;
  127. int y = 0;
  128. again:
  129. do
  130. {
  131. x = rand() % 53 + 2;
  132. y = rand() % 24 + 1;
  133. } while (x % 2 != 0); // 这一步啥意思呀:42min 左右
  134. // 3.坐标不能在蛇的身体上:遍历蛇身一一对比对比
  135. //判断食物是否在蛇身上
  136. pSnakeNode cur = ps->pSnake;
  137. while (cur)
  138. {
  139. if (x == cur->x && y == cur->y)
  140. {
  141. goto again;
  142. }
  143. cur = cur->next;
  144. }
  145. //创建食物: 一个新节点
  146. pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));// (pSnakeNode) 和 (SnakeNode) 千万别混淆!!!
  147. if (pFood == NULL)
  148. {
  149. perror("CreateFood():malloc()");
  150. return;
  151. }
  152. pFood->x = x;
  153. pFood->y = y;
  154. ps->pFood = pFood; // pFood 是 贪吃蛇 结构体中的成员变量, pfood 是新创建的新食物节点
  155. // 思路:将食物放进 贪吃蛇 结构体中,判定重合后,就头插法,没有就一直 移动
  156. //打印食物
  157. SetPos(x, y);
  158. wprintf(L"%c", FOOD);
  159. //getchar(); // 注意 :这个别忘了关闭
  160. }
  161. // 游戏前准备
  162. void GameStart(pSnake ps)
  163. {
  164. // 一、设置控制台的信息:窗口大小、窗口名
  165. // 设置控制台窗口的长宽:设置控制台窗口的大小
  166. // 因为光是墙体就 27行 58列 了,界面右侧还要 写一些介绍指引,则干脆 列为 100, 行 30
  167. system("mode con cols=100 lines=30");
  168. system("title 贪吃蛇");// 设置cmd窗口名称
  169. // 二、隐藏光标:不想光标在一直闪
  170. HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
  171. //隐藏光标操作: 6.5.1节 那里有示例
  172. CONSOLE_CURSOR_INFO cursorinfo; //使用 6.5.1 的结构体,定义一个结构体变量,名为 CursorInfo
  173. GetConsoleCursorInfo(handle, &cursorinfo); //???? 为什么没有这句话还不行了??获取控制台光标信息,若不想查看也可以不写此句
  174. //先修改成员变量,再传进Set函数中:句柄 + 光标结构体,相当于对 句柄所指定的控制台窗口,就行光标修改设置
  175. cursorinfo.bVisible = false; //隐藏控制台光标
  176. SetConsoleCursorInfo(handle, &cursorinfo); //设置控制台光标状态
  177. //打印欢迎信息
  178. WelcomeToGame();
  179. //绘制地图
  180. CreateMap();
  181. //初始化蛇
  182. InitSnake(ps); // 要把 蛇 的结构体的传过去,才能修改蛇的状态信息
  183. //创建食物
  184. CreateFood(ps); // 因为 食物也是 链表节点,最后要链接到 蛇身链表上的,因此涉及到 蛇身的修改,要传入蛇的头指针
  185. }
  186. void PrintHelpInfo()
  187. {
  188. // 设置位置
  189. SetPos(65, 8);
  190. printf("温馨提示\n");
  191. SetPos(65, 10);
  192. printf("1、不能穿墙\n");
  193. SetPos(65, 11);
  194. printf("2、不能咬到自己\n");
  195. SetPos(65, 12);
  196. printf("3、按 ESC 退出游戏\n");
  197. SetPos(65, 13);
  198. printf("4、按 空格 暂停游戏\n");
  199. SetPos(65, 15);
  200. printf("操作回顾\n");
  201. SetPos(65, 17);
  202. printf("5、用 ↑↓ ← → 来控制蛇的移动 \n");
  203. SetPos(65, 18);
  204. printf("6、F3 加速,F4 减速\n");
  205. SetPos(65, 19);
  206. printf("7、加速可以获得更高的分数\n");
  207. SetPos(65, 22);
  208. printf("版权 @时差\n");
  209. // getchar(); // 注意 :这个别忘了关闭
  210. }
  211. // 按 空格 :暂定和回复暂定
  212. void Pause()//游戏暂停
  213. {
  214. // tip : 死循环的 sleep , 再循环中 判断 再次点击空格键 即可回复
  215. while (1)
  216. {
  217. Sleep(200);
  218. if (KEY_PRESS(VK_SPACE))
  219. {
  220. break;
  221. }
  222. }
  223. }
  224. // 判断蛇头 下一步处 是否是 食物:就是判断 方向指针pNext 是不是 食物
  225. int NextIsFood(pSnake ps, pSnakeNode pNext)
  226. {
  227. // 两者坐标重合 就是了
  228. if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个坐标处是食物
  229. else return 0;
  230. }
  231. //下一步处 是食物就吃掉:将 方向节点 链接到 蛇身上(头插法),将 食物指针 free 释放掉
  232. void EatFood(pSnake ps, pSnakeNode pNext)
  233. {
  234. pNext->next = ps->pSnake; // 直接让 方向节点 指向头节点
  235. ps->pSnake = pNext; // 更新头指针
  236. // 打印蛇身:每次更新一轮,就打印一次
  237. // PrintSnake(ps); // 这里注意一下 是不是不能用:好像不是,改了后 依旧停留在 1.0 版本
  238. //打印蛇
  239. pSnakeNode cur = ps->pSnake;
  240. while (cur)
  241. {
  242. SetPos(cur->x, cur->y);
  243. wprintf(L"%c", BODY);
  244. cur = cur->next;
  245. }
  246. // 分数变化:
  247. ps->Score += ps->FoodWeight;
  248. // 释放 旧食物节点:吃掉了 就不存在了
  249. free(ps->pFood);
  250. //创建新食物;
  251. CreateFood(ps);
  252. }
  253. void NotEatFood(pSnake ps, pSnakeNode pNext)
  254. {
  255. // 头插法:链接方向指针,相当于移动了一步
  256. pNext->next = ps->pSnake;// 直接让 方向节点 指向头节点
  257. ps->pSnake = pNext;// 更新头指针
  258. // 释放 尾节点,同时还要保留 尾节点 的前一个:倒数第二个:实际上 就是 单链表的 尾删法
  259. pSnakeNode cur = ps->pSnake;
  260. while (cur->next->next)
  261. {
  262. SetPos(cur->x, cur->y);
  263. wprintf(L"%lc", BODY);
  264. cur = cur->next;
  265. }
  266. // 将尾节点的位置 打印成 空白字符:不能 还打印蛇身
  267. // 先 打印成 空白,后再释放:别急着释放
  268. SetPos(cur->next->x, cur->next->y);
  269. printf(" ");// 注意 空格是 两个
  270. free(cur->next);
  271. cur->next = NULL;//易错
  272. // 注意前面以及在 循环遍历蛇身了:同时就可以进行 打印 蛇身节点
  273. // 打印蛇身:每次更新一轮,就打印一次
  274. // PrintSnake(ps);
  275. }
  276. void KillByWall(pSnake ps)
  277. {
  278. if (ps->pSnake->x == 0 ||
  279. ps->pSnake->x == 56 ||
  280. ps->pSnake->y == 0 ||
  281. ps->pSnake->y == 26)
  282. {
  283. ps->state = KILL_BY_WALL;
  284. }
  285. }
  286. void KillBySelf(pSnake ps)
  287. {
  288. pSnakeNode cur = ps->pSnake->next;
  289. while (cur)
  290. {
  291. if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
  292. {
  293. ps->state = KILL_BY_SELF;
  294. return;
  295. }
  296. cur = cur->next;
  297. }
  298. }
  299. // 蛇身 移动
  300. // 移动的本质: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
  301. void SnakeMove(pSnake ps)
  302. {
  303. // 根据按键:调整移动方向
  304. // 创建下一个节点:表示下一个方向的第一个节点:根据图 更好理解 1h43min
  305. // 取名:方向节点
  306. pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
  307. if (pNext == NULL)
  308. {
  309. perror("SnakeMove():malloc:");
  310. return;
  311. }
  312. pNext->next = NULL;// 指向空就好
  313. // 利用 方向节点,
  314. // pSnake 是 贪吃蛇 结构体的成员变量:指向蛇头
  315. // 通过 改变 方向节点 的坐标位置,引导蛇头的移动
  316. switch (ps->dir)
  317. {
  318. case UP:
  319. pNext->x = ps->pSnake->x;
  320. pNext->y = ps->pSnake->y - 1;
  321. break;
  322. case DOWN:
  323. pNext->x = ps->pSnake->x;
  324. pNext->y = ps->pSnake->y + 1;
  325. break;
  326. case LEFT:
  327. pNext->x = ps->pSnake->x - 2;
  328. pNext->y = ps->pSnake->y;
  329. break;
  330. case RIGHT:
  331. pNext->x = ps->pSnake->x + 2;
  332. pNext->y = ps->pSnake->y;
  333. break;
  334. }
  335. //下一个坐标处是否是食物
  336. if (NextIsFood(ps, pNext))
  337. {
  338. //是食物就吃掉:将 方向节点 链接到 蛇身上,将 食物指针 free 释放掉
  339. EatFood(ps, pNext);
  340. }
  341. else
  342. {
  343. //不是食物就正常一步: 没吃食物,则长度不变,将 方向节点 链接到 蛇头 上,free 掉 尾节点
  344. NotEatFood(ps, pNext);
  345. }
  346. // getchar(); // 检测蛇移动时,不能用 getchar 了 否则,蛇 走一步 就不走了
  347. //检测撞墙
  348. KillByWall(ps);
  349. //是否撞到自己
  350. KillBySelf(ps);
  351. }
  352. // 游戏运行过程
  353. void GameRun(pSnake ps)
  354. {
  355. // 一、打印界面右侧的帮助提示信息
  356. PrintHelpInfo();
  357. // 游戏一般 是 do while循环:总之都需要先让游戏先运行一次
  358. do
  359. {
  360. //当前的分数情况: 打印不断变化的分数技巧:不断更新 蛇节点的 Score 更新就打印
  361. SetPos(65, 4);
  362. //[BACKGROUND代表背景:就是背景;FOREGROUND代表前景:就是字体颜色 【http://t.csdnimg.cn/VFHPV】
  363. // SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | BACKGROUND_INTENSITY);
  364. CONSOLE_FONT_INFOEX cfi;
  365. cfi.cbSize = sizeof cfi;
  366. cfi.dwFontSize.X = 0; //字宽
  367. cfi.dwFontSize.Y = 20;//字高
  368. cfi.FontWeight = FW_NORMAL;//粗细
  369. SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
  370. printf("总分:%5d", ps->Score);
  371. SetPos(65, 6 );
  372. printf("食物的分值:%02d", ps->FoodWeight);
  373. // 二、检测按键:判断发出的指令:使用 那个判断虚拟键值的函数
  374. // 上下左右,ESC,空格,F3,F4
  375. // 不能同时 方向 等于 相反方向
  376. if (KEY_PRESS(VK_UP) && ps->dir != DOWN)// 上 :正在向上,则同时 蛇 的方向不能等于 DOWN !!!!
  377. {
  378. ps->dir = UP;
  379. }
  380. else if (KEY_PRESS(VK_DOWN) && ps->dir != UP)
  381. {
  382. ps->dir = DOWN;
  383. }
  384. else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)
  385. {
  386. ps->dir = LEFT;
  387. }
  388. else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)
  389. {
  390. ps->dir = RIGHT;
  391. }
  392. else if (KEY_PRESS(VK_ESCAPE))
  393. {
  394. ps->state = ESC; // 将状态该成 退出状态
  395. break;
  396. }
  397. else if (KEY_PRESS(VK_SPACE)) // 空格 :(VK_SPACE)
  398. {
  399. //游戏要暂定
  400. // 封装一个函数 Pause() : 暂定和回复暂定,总不能一直暂停下去呀
  401. Pause();//暂定和回复暂定
  402. }
  403. else if (KEY_PRESS(VK_F3)) // 加速即休眠时间 变短,同时分数变多,而且 次数有限制
  404. {
  405. if (ps->SleepTime >= 80) // 最低下限:SleepTime = 80 (初始化sleep 是 200ms)
  406. {
  407. ps->SleepTime -= 30;
  408. ps->FoodWeight += 2;
  409. }
  410. }
  411. else if (KEY_PRESS(VK_F4)) // 减速 即休眠时间 变长,同时分数变少
  412. {
  413. if (ps->FoodWeight > 2) // 最低下限:FoodWeight > 2
  414. {
  415. ps->SleepTime += 30;
  416. ps->FoodWeight -= 2;
  417. }
  418. }
  419. // 三 四 步 别写反
  420. // 三、走一步: 每轮移动一步,要检测是否撞墙.....等状态
  421. // 蛇身移动较为复杂,封装成一个函数便于分析
  422. SnakeMove(ps);
  423. // 四、睡眠一下
  424. Sleep(ps->SleepTime);
  425. } while (ps->state == OK); // 结束条件:当游戏状态 不是 OK 时 就结束
  426. }
  427. void GameEnd(pSnake ps)
  428. {
  429. SetPos(15, 12);
  430. switch (ps->state)
  431. {
  432. case ESC:
  433. printf("主动退出游戏\n");
  434. break;
  435. case KILL_BY_WALL:
  436. printf("很遗憾,撞墙了,游戏结束\n");
  437. break;
  438. case KILL_BY_SELF:
  439. printf("很遗憾,撞到自己,游戏结束\n");
  440. break;
  441. }
  442. //释放
  443. pSnakeNode cur = ps->pSnake;
  444. pSnakeNode del = NULL;
  445. while (cur)
  446. {
  447. del = cur;
  448. cur = cur->next;
  449. free(del);
  450. }
  451. free(ps->pFood);
  452. ps = NULL;
  453. }

Snake.h

  1. #pragma once
  2. #include<locale.h>
  3. #include<stdlib.h>
  4. #include<stdio.h>
  5. #include<windows.h>
  6. #include<stdbool.h>
  7. #define WALL L'□' // 因为 每次打印都要写一次 L什么什么的 还不如直接定义一个宏
  8. #define BODY L'●' // 打印是 蛇身
  9. #define FOOD L'※' // 打印食物
  10. //Đʘ◨ↇↀↈ:不行的
  11. // D●
  12. // 蛇 默认的起始坐标位置:要确定 起始的蛇身 每个 节点的 坐标
  13. #define POS_X 24//蛇初始坐标
  14. #define POS_Y 5
  15. #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
  16. // 枚举类型不熟的可以去翻笔记
  17. // enum 就是 集合版的 define
  18. //蛇走的方向
  19. enum DIRECTION
  20. {
  21. UP = 1, // 上
  22. DOWN, // 下
  23. LEFT, // 左
  24. RIGHT // 右
  25. };
  26. //蛇的状态: 维护游戏状态的 枚举类型
  27. enum GAME_STATE
  28. {
  29. OK,//正常运行
  30. ESC, // 按 ESC 键退出
  31. KILL_BY_WALL,//撞墙
  32. KILL_BY_SELF,//咬到自己
  33. //END_NOMAL//正常结束
  34. };
  35. // 蛇身节点
  36. // 蛇身用链表来维护:一个蛇身节点就是一个链表节点
  37. // 蛇在行动时,蛇身的每一个节点的坐标都在变化,所以要不断记录下来
  38. typedef struct SnakeNode
  39. {
  40. int x;
  41. int y;
  42. struct SnakeNode* next;
  43. }SnakeNode, * pSnakeNode;
  44. // 创建一个指向蛇身节点的结构体指针
  45. //贪吃蛇
  46. // 整条蛇
  47. // 要管理整条贪吃蛇当前状态过程的信息,我们再封装一个Snake的结构来维护整条贪吃蛇:
  48. typedef struct Snake
  49. {
  50. pSnakeNode pSnake;//维护整条蛇的指针:指向蛇头
  51. pSnakeNode pFood;//维护食物的指针:食物实际上也是蛇身节点,只是打印不一样
  52. enum DIRECTION dir;//蛇头的方向默认是向右
  53. enum GAME_STATUS state;//维护游戏进行状态:蛇身撞墙、吃到自己、手动退出(上面定义了一个枚举类型)
  54. int Score;//当前获得分数
  55. int FoodWeight;//默认每个食物 10 分
  56. int SleepTime;//每走一步休眠时间:控制蛇移动的速度(本质:蛇身在移动过程中 可以发现,蛇身节点在 一闪一闪的停顿,其实是Sleep 控制睡眠时间,来控制总体移动速度)
  57. }Snake, * pSnake; // pSnake 是 指向贪吃蛇的指针: 下面就用上了,其实就是 Snake*
  58. //定位控制台光标位置
  59. void SetPos(int x, int y);
  60. //游戏开始前的准备
  61. void GameStart(pSnake ps);
  62. //欢迎界面
  63. void WelcomeToGame();//打印欢迎信息
  64. //绘制地图
  65. void CreateMap();//创建地图
  66. //初始化蛇
  67. void InitSnake(pSnake ps);
  68. //创建食物
  69. void CreateFood(pSnake ps);
  70. //游戏运行的整个逻辑
  71. void GameRun(pSnake ps);
  72. //打印帮助信息
  73. void PrintHelpInfo();
  74. //蛇移动的函数- 每次走一步
  75. void SnakeMove(pSnake ps);
  76. //判断蛇头的下一步要走的位置处是否是食物
  77. int NextIsFood(pSnake ps, pSnakeNode pNext);
  78. //下一步要走的位置处就是食物,就吃掉食物
  79. void EatFood(pSnake ps, pSnakeNode pNext);
  80. //下一步要走的位置处不是食物,不吃食物
  81. void NotEatFood(pSnake ps, pSnakeNode pNext);
  82. //检测撞墙
  83. void KillByWall(pSnake ps);
  84. //撞到自己
  85. void KillBySelf(pSnake ps);
  86. //游戏结束资源释放
  87. void GameEnd(pSnake ps);

 test.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"snake.h"
  3. void test()
  4. {
  5. int ch = 0;
  6. do
  7. {
  8. // 创建一条贪吃蛇
  9. Snake snake = { 0 };
  10. // 把蛇传过去:因为蛇移动时,就是变化的过程,则应该传地址
  11. GameStart(&snake); // 游戏开始前的初始化:按键功能提示、游戏界面打印、墙体初始化等等
  12. GameRun(&snake); // 游戏过程
  13. GameEnd(&snake);//善后工作
  14. SetPos(20, 15);
  15. printf("再来一局?(y/n)");
  16. ch = getchar();
  17. getchar();
  18. } while (ch == 'Y' || ch == 'y');
  19. }
  20. int main()
  21. {
  22. //适配本地中文环境
  23. setlocale(LC_ALL, "");
  24. //贪吃蛇游戏的设置
  25. test();
  26. return 0;
  27. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/64440
推荐阅读
相关标签
  

闽ICP备14008679号