当前位置:   article > 正文

C语言项目实践——贪吃蛇

C语言项目实践——贪吃蛇

引言:本篇博客中,我将会使用结构体链表WIN32 API等一系列知识完成C语言项目——贪吃蛇的实现。在观看此篇博客之前,请将这些知识所熟悉,不然可能会造成理解困难。

更多有关C语言的知识详解可前往个人主页:计信猫

目录

一,贪吃蛇项目的准备

1,三文件操作

2,贪吃蛇有关结构体的定义

3,改变窗口信息与隐藏光标

4,确定光标位置函数

 二,游戏开始(GameStart)

 1,欢迎界面的设计

2,游戏地图的创建

3,蛇的初始化

4,创建食物

​编辑

 三,游戏运行(GameRun)

 1,打印游戏帮助信息

​编辑2,游戏得分的打印与蛇的移动方向的判定

3,蛇的移动(SnakeMove)

4,检测下一个坐标是否为食物

Ⅰ,EatFood

Ⅱ,NoFood

5,判断蛇是否撞墙或者撞到自己

Ⅰ,KillByWall

Ⅱ,KillBySelf 

 四,游戏结束(GameEnd)

1,根据蛇的状态打印结束语

2,释放蛇的空间

五,游戏的优化设置

六,代码参考

1,test.c

2,  snake.c

3,  snake.h

一,贪吃蛇项目的准备

1,三文件操作

        在贪吃蛇项目中,我们会创建三个文件分别为:test.c,snake.c,snake.h。它们的作用分别为游戏函数的测试,游戏函数的实现,游戏函数的声明和结构体变量的定义。

2,贪吃蛇有关结构体的定义

        在snake.h中,我们将会定义与贪吃蛇有关的结构体。

        1,贪吃蛇身体的节点:

  1. //定义蛇身体的节点
  2. typedef struct snakenode
  3. {
  4. //坐标
  5. int x;
  6. int y;
  7. struct snakenode* next;//下一个节点
  8. }snakenode,*psnakenode;

        2,蛇的方向:

  1. //定义方向
  2. enum direction
  3. {
  4. UP = 1,
  5. DOWN,
  6. LEFT,
  7. RIGHT
  8. };

        3,蛇的状态:

  1. //蛇的状态
  2. enum game_state
  3. {
  4. OK,//正常状态
  5. KILL_BY_WALL,//撞到墙
  6. KILL_BY_SELF,//撞到了自己
  7. END_NORMAL//正常结束游戏
  8. };

        4,贪吃蛇游戏:

  1. //贪吃蛇
  2. typedef struct snake
  3. {
  4. psnakenode _psnake;//蛇头节点
  5. psnakenode _pfood;//食物节点
  6. enum direction _dir;//蛇的方向
  7. enum game_state _sta;//游戏状态
  8. int _food_weight;//一个食物的分数
  9. int _score;//总成绩
  10. int _sleep_time;//蛇的速度
  11. }snake,* psnake;

3,改变窗口信息与隐藏光标

        snake.c中,我们定义一个GameStart函数来包含开始游戏之前的窗口改变,光标隐藏,欢迎界面与地图打印函数

        在前文WIN32 API中我们已经对窗口信息修改和隐藏光标的函数进行过解释,所以我们这里直接使用:

  1. void GameStart(psnake ps)
  2. {
  3. //设置窗口大小,改变窗口名字
  4. system("mode con cols=100 lines=30");
  5. system("title 贪吃蛇");
  6. //隐藏光标
  7. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取光标句柄
  8. CONSOLE_CURSOR_INFO cursorinfo;
  9. GetConsoleCursorInfo(houtput, &cursorinfo);//获取光标信息
  10. cursorinfo.bVisible = false;//隐藏鼠标
  11. SetConsoleCursorInfo(houtput, &cursorinfo);//设置光标状态
  12. }

4,确定光标位置函数

        在欢迎界面与游戏界面中,我们需要将游戏开始信息与游戏信息等打印在指定的位置,这时候我们就需要有特定的函数来确定光标的位置,以此来完成指定位置信息的打印。所以我们如下设计确定光标位置setpos()函数。

  1. void setpos(int x,int y)
  2. {
  3. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出句柄
  4. COORD pos = { x,y };//设定坐标
  5. SetConsoleCursorPosition(houtput, pos);//设定光标的坐标
  6. }

 二,游戏开始(GameStart)

 1,欢迎界面的设计

       

        如图中的界面就是我们想要达到的欢迎界面效果,所以我们在GameStart()函数中设计一个新函数名为welcometogame()函数,函数内容如下:

  1. void welcometogame()
  2. {
  3. setpos(40, 14);
  4. wprintf(L"欢迎来到贪吃蛇游戏\n");
  5. setpos(42, 20);
  6. system("pause");
  7. system("cls");
  8. setpos(25, 14);
  9. wprintf(L"使用 ↑ . ↓ . ← . → 来进行方向移动,f3加速,f4减速\n");
  10. setpos(25, 15);
  11. wprintf(L"加速吃到食物可以增加得分\n");
  12. setpos(42, 20);
  13. system("pause");
  14. system("cls");
  15. }

        首先我们使用setpos函数来确定光标的位置,再使用wprintf函数来打印内容,system(“pause”)语句则代表着暂停程序,按任意键表示继续程序,而system(“cls”)语句则代表着将当前控制台的页面全部清空,这样我们就打印出了我们的欢迎界面。

2,游戏地图的创建

        在贪吃蛇游戏中,我们将要创建如图所示的游戏地图:

        正如我们前一篇博客所讲到的,我们的控制台界面的各个地点都有着对应的坐标,如下:

        而我们所想要设计的地图的大小,就可以通过坐标的范围来圈定。所以我们的地图大小应该如下图坐标所示:

        有了坐标,我们就知道了地图的上边需要29个宽字符‘▢’,下边也是一样,而左右两边我们则可以使用setpos函数来移动光标进行左右两边边界的打印。所以我们在GameStart()函数中创建一个creatmap()函数,代码如下:

  1. void creatmap()
  2. {
  3. int i = 0;
  4. //上
  5. for (i = 0; i < 29; i++)
  6. {
  7. wprintf(L"%lc", L'□');
  8. }
  9. //下
  10. setpos(0, 26);//移动光标以便打印下标
  11. for (i = 0; i < 29; i++)
  12. {
  13. wprintf(L"%lc", L'□');
  14. }
  15. //左
  16. for (i = 1; i <= 25; i++)
  17. {
  18. setpos(0, i);//移动光标
  19. wprintf(L"%lc", L'□');
  20. }
  21. //右
  22. for (i = 1; i <= 25; i++)
  23. {
  24. setpos(56, i);//移动光标
  25. wprintf(L"%lc", L'□');
  26. }
  27. system("pause");//按任意键继续
  28. }

        代码一走,则地图创建成功!

3,蛇的初始化

        我们将蛇想象成我们以前学到的链表,刚开始的蛇身长度为5个节点,所以我们便可以使用头插的办法来完成蛇身的初始化。

        但是,在初始化蛇身的时候,我们一定需要注意蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中, 另外⼀半在墙外的现象,坐标不好对齐。如下图所示:

        而对于蛇的身体,我们则使用宽字符‘●‘进行表示。为了方便以后更改蛇身体的坐标,墙和蛇的图标,于是我们选择在snake.h中使用define定义:

  1. #define POS_X 24
  2. #define POS_Y 5
  3. #define WALL L'□'
  4. #define BODY L'●'

        最后,我们还不要忘记对蛇的结构体中的其他成员变量的初始化。所以在GameStart()函数中,我们继续定义一个InitSnake()函数来进行对蛇进行初始化。

  1. void InitSnake(psnake ps)
  2. {
  3. int i = 0;
  4. psnakenode cur = NULL;
  5. for (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->next = NULL;
  14. //设置节点的坐标
  15. cur->x = POS_X + 2 * i;
  16. cur->y = POS_Y;
  17. //使用头插法
  18. if (ps->_psnake == NULL)//空链表
  19. {
  20. ps->_psnake = cur;
  21. }
  22. else//非空链表
  23. {
  24. cur->next = ps->_psnake;
  25. ps->_psnake = cur;
  26. }
  27. }
  28. cur = ps->_psnake;//从蛇头开始遍历
  29. while (cur)
  30. {
  31. setpos(cur->x, cur->y);//设定光标位置
  32. wprintf(L"%lc", BODY);
  33. cur = cur->next;
  34. }
  35. //设置蛇的其他属性
  36. ps->_dir = RIGHT;
  37. ps->_food_weight = 10;
  38. ps->_score = 0;
  39. ps->_sleep_time = 200;//单位是毫秒
  40. ps->_sta = OK;
  41. getchar();//暂停程序观察效果,不需要观察效果后可将该语句删除
  42. }

4,创建食物

        当蛇的初始化完成后,我们就需要完成食物的初始化。但在初始化食物的时候,我们需要注意到以下三点:

1,食物必须出现在墙体之内

2,食物的x坐标一定为2的倍数

3,食物坐标不可以与蛇身的坐标重复

        在该函数中,我们也给食物下定义:

#define FOOD L'※'

为了达到坐标随机生成的目的,我们将在主函数中使用srand((unsigned int)time);(需要包含<time.h>)。所以我们的创建食物函数CreatFood()函数的实现如下:

  1. void CreatFood(psnake ps)
  2. {
  3. int x = 0;
  4. int y = 0;
  5. again:
  6. do
  7. {
  8. x = rand() % 53 + 2;//使横坐标在2~54之内
  9. y = rand() % 24 + 1;//使纵坐标在1~25之内
  10. } while (x % 2 != 0);//若生成的x为奇数,那么就重新生成
  11. psnakenode cur = ps->_psnake;//遍历链表,防止食物与蛇身重合
  12. while (cur)
  13. {
  14. if (x == cur->x && y == cur->y)//如果食物的坐标与蛇身重合
  15. {
  16. goto again;//重新生成
  17. }
  18. cur = cur->next;
  19. }
  20. //打印食物标号
  21. psnakenode food = (psnakenode)malloc(sizeof(snakenode));
  22. if (food == NULL)//防止申请内存失败
  23. {
  24. perror("CreatFood()::malloc()");
  25. return;
  26. }
  27. food->x = x;
  28. food->y = y;
  29. food->next = NULL;
  30. setpos(food->x, food->y);
  31. wprintf(L"%lc", FOOD);
  32. ps->_pfood = food;
  33. getchar();//观察食物打印效果,之后若不需要观察则可以删除该语句
  34. }

代码一走,食物生成成功!

 三,游戏运行(GameRun)

 1,打印游戏帮助信息

        在正式开始游戏时,我们需要在游戏地图之外打印一系列信息,以帮助用户了解游戏的玩法。所以我们在GameRun()函数中封装一个PrintHelpInfo()函数来进行游戏帮助信息的打印。

  1. //打印游戏帮助信息
  2. void PrintHelpInfo()
  3. {
  4. //打印提⽰信息
  5. setpos(64, 15);
  6. printf("不能穿墙,不能咬到自己\n");
  7. setpos(64, 16);
  8. printf("⽤↑.↓.←.→分别控制蛇的移动\n");
  9. setpos(64, 17);
  10. printf("F3 为加速,F4 为减速\n");
  11. setpos(64, 18);
  12. printf("ESC :退出游戏.space:暂停游戏.");
  13. }

        代码一走,效果如图:

2,游戏得分的打印与蛇的移动方向的判定

        首先,我们实现一个KEY_PRESS宏来判断某个按键是否被按下:

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

        所以,我们便可以在GameStart()函数中使用do-while循环来打印游戏的得分与了解按键按下的情况,以便控制蛇的移动方向。

  1. //游戏的运行
  2. void GameRun(psnake ps)
  3. {
  4. //打印游戏帮助信息
  5. PrintHelpInfo();
  6. do
  7. {
  8. //打印游戏得分信息
  9. setpos(64, 10);
  10. printf("总得分:%d\n", ps->_score);
  11. setpos(64, 11);
  12. printf("当前一个食物的分数:%2d\n", ps->_food_weight);
  13. //判断按键以控制蛇的移动
  14. if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//VK_UP为↑的虚拟键码,同时蛇的当前方向不可以向下
  15. {
  16. ps->_dir = UP;
  17. }
  18. else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
  19. {
  20. ps->_dir = DOWN;
  21. }
  22. else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
  23. {
  24. ps->_dir = LEFT;
  25. }
  26. else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
  27. {
  28. ps->_dir = RIGHT;
  29. }
  30. else if (KEY_PRESS(VK_SPACE))//空格为暂停
  31. {
  32. Pause();//Pause函数执行暂停操作
  33. }
  34. else if (KEY_PRESS(VK_ESCAPE))
  35. {
  36. //正常退出游戏
  37. ps->_sta = END_NORMAL;
  38. }
  39. else if (KEY_PRESS(VK_F3))
  40. {
  41. //加速
  42. if (ps->_sleep_time > 80)
  43. {
  44. ps->_sleep_time -= 30;
  45. ps->_food_weight += 2;
  46. }
  47. }
  48. else if (KEY_PRESS(VK_F4))
  49. {
  50. //减速
  51. if (ps->_sleep_time < 300)
  52. {
  53. ps->_sleep_time += 30;
  54. ps->_food_weight -= 2;
  55. }
  56. }
  57. SnakeMove(ps);//蛇走一步
  58. Sleep(ps->_sleep_time);
  59. } while (ps->_sta == OK);
  60. }

        其中Pause()函数如下:

  1. //游戏暂停操作
  2. void Pause()
  3. {
  4. while (1)
  5. {
  6. Sleep(200);//死循环休眠则视为暂停
  7. if (KEY_PRESS(VK_SPACE))
  8. {
  9. break;//再次按下空格则跳出循环
  10. }
  11. }
  12. }

3,蛇的移动(SnakeMove)

        当我们在前面获取了按键按下的情况,即蛇的状态信息之后,我们就可以使用SnakeMove()函数来对蛇的移动进行控制。

        在该函数中,我们就可以再创建一个节点(pNextNode)表示蛇头即将到达的节点位置,而该新节点的位置与当前蛇头的坐标x,y有关:

        于是新节点的位置设置如下:

  1. //蛇走一步
  2. void SnakeMove(psnake ps)
  3. {//先创建一个新节点,表示蛇头即将到达的位置
  4. psnakenode pNextNode = (psnakenode)malloc(sizeof(snakenode));
  5. if (pNextNode == NULL)
  6. {
  7. perror("SnakeMove()::malloc()");
  8. return;
  9. }
  10. switch (ps->_dir)
  11. {
  12. case UP:
  13. pNextNode->x = ps->_psnake->x;
  14. pNextNode->y = ps->_psnake->y - 1;
  15. break;
  16. case DOWN:
  17. pNextNode->x = ps->_psnake->x;
  18. pNextNode->y = ps->_psnake->y + 1;
  19. break;
  20. case LEFT:
  21. pNextNode->x = ps->_psnake->x-2;
  22. pNextNode->y = ps->_psnake->y;
  23. break;
  24. case RIGHT:
  25. pNextNode->x = ps->_psnake->x+2;
  26. pNextNode->y = ps->_psnake->y;
  27. break;
  28. }
  29. }

4,检测下一个坐标是否为食物

        当我们的蛇头移动到下一个坐标的时候,就会产生两种情况:1,下一个坐标为食物。2,下一个坐标不为食物。两种情况应该不同方式来处理,所以我们在SnakeMove()函数中的switch循环之后再定义两个函数来处理不同情况:

  1. //判断下一个坐标是否为食物
  2. int NextIsFood(psnakenode pNextNode, psnake ps)
  3. {
  4. return (pNextNode->x == ps->_pfood->x && pNextNode->y == ps->_pfood->y);
  5. }
  6. //检测下一个坐标是否为食物
  7. if (NextIsFood(pNextNode, ps))
  8. {
  9. EatFood(pNextNode, ps);
  10. }
  11. else
  12. {
  13. NoFood(pNextNode, ps);
  14. }

Ⅰ,EatFood

        该函数用于处理下一个坐标为食物的情况。该情况下,我们就可以直接将食物节点变为蛇的头节点,然后打印蛇,加分,最后再次创建食物:

  1. //下一个坐标为食物
  2. void EatFood(psnakenode pNextNode, psnake ps)
  3. {
  4. //使用头插,将食物节点变为蛇头
  5. ps->_pfood->next = ps->_psnake;
  6. ps->_psnake = ps->_pfood;
  7. //释放掉pNextNode
  8. free(pNextNode);
  9. pNextNode = NULL;
  10. //打印出新蛇
  11. psnakenode cur = ps->_psnake;
  12. while (cur)
  13. {
  14. setpos(cur->x, cur->y);
  15. wprintf(L"%lc", BODY);
  16. cur = cur->next;
  17. }
  18. //加分
  19. ps->_score += ps->_food_weight;
  20. //再次创建食物
  21. CreatFood(ps);
  22. }

Ⅱ,NoFood

        该函数用于处理下一个坐标不为食物的情况。该情况下,我们则需要做到的任务如下:

  1. 1,使用头插法将pNextNode变为新的蛇头
  2. 2,把最后一个坐标打印为两个空格
  3. 3,释放掉尾节点
  4. 4,将倒数第二个节点的next置空

        所以我们画图解释:

          于是我们使用cur遍历数组,当cur->next->nextNULL时,那么就证明cur来到了倒数第二个节点,此时我们就跳出while循环,之后将cur->next指向的节点处打印两个空格(以覆盖先前打印而留下的蛇身),再将尾节点释放掉,最后将cur->next置空。 

  1. //下一个坐标不是食物
  2. void NoFood(psnakenode pNextNode, psnake ps)
  3. {
  4. //头插法
  5. pNextNode->next = ps->_psnake;
  6. ps->_psnake = pNextNode;
  7. //遍历链表
  8. psnakenode cur = ps->_psnake;
  9. while (cur->next->next)
  10. {
  11. setpos(cur->x, cur->y);
  12. wprintf(L"%lc", BODY);
  13. cur = cur->next;
  14. }
  15. //出了循环,cur刚好停在倒数第二个节点,所以先打印两个空格
  16. setpos(cur->next->x, cur->next->y);
  17. printf(" ");
  18. //释放掉尾节点
  19. free(cur->next);
  20. //将倒数第二个节点的next置空
  21. cur->next = NULL;
  22. }

        这样,一个贪吃蛇游戏就初步完成了。

5,判断蛇是否撞墙或者撞到自己

        在GameRUN()函数中,我们也需要检测蛇是否在行动的过程中撞到了墙或者撞到了自己。于是我们可以在设计两个函数KillByWall()函数KillBySelf()函数来进行判断。

Ⅰ,KillByWall

        关于蛇是否撞墙的判断十分简单。我们只需要判断蛇头的坐标是否与墙的x或者y坐标重合即可。若重合,则撞墙;反之,则未撞墙。

  1. //检测是否撞墙
  2. void KillByWall(psnake ps)
  3. {
  4. if (ps->_psnake->x == 0 || ps->_psnake->x == 56 ||
  5. ps->_psnake->y == 0 || ps->_psnake->y == 26)
  6. {
  7. ps->_sta = KILL_BY_WALL;
  8. }
  9. }

Ⅱ,KillBySelf 

         在该函数中,我们只需要从蛇头之后的一个节点遍历蛇身,假如某段蛇身的坐标等于蛇头的坐标,那么就是撞到了自己;反之,则没有撞到自己。

  1. //检测是否撞到自己
  2. void KillBySelf(psnake ps)
  3. {
  4. psnakenode cur = ps->_psnake->next;//从蛇头之后的一个节点开始遍历
  5. while (cur)
  6. {
  7. if (cur->x == ps->_psnake->x && cur->y == ps->_psnake->y)
  8. {
  9. ps->_sta = KILL_BY_SELF;
  10. break;//撞到了自己,退出循环
  11. }
  12. cur = cur->next;
  13. }
  14. }

 四,游戏结束(GameEnd)

1,根据蛇的状态打印结束语

        有了前文所获取的蛇的结束状态,那我们就可以在GameEnd()函数中打印对应的结束语。

  1. void GameEnd(psnake ps)
  2. {
  3. setpos(24, 12);//设置光标位置,打印结束信息
  4. switch (ps->_sta)
  5. {
  6. case END_NORMAL:
  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. }

2,释放蛇的空间

        当然,当我们的游戏结束后,我们使用的蛇身节点也需要被释放,而释放方法非常简单,就是我们之前在链表操作函数中学到的销毁链表操作

  1. //游戏结束
  2. void GameEnd(psnake ps)
  3. {
  4. //释放蛇身
  5. psnakenode cur = ps->_psnake;
  6. while (cur)
  7. {
  8. psnakenode del = cur;
  9. cur = cur->next;
  10. free(del);
  11. del = NULL;
  12. }
  13. }

        如此,我们的贪吃蛇的主体函数就完成了!!

五,游戏的优化设置

        在这里,我们将实现游戏的多次使用和正常退出,以及项目结束的信息的位置的设置。这一切的操作,都在test.c中进行。

  1. #include"snake.h"
  2. void test()
  3. {
  4. int ch = 0;
  5. do
  6. {
  7. snake ps = { 0 };
  8. GameStart(&ps);
  9. GameRun(&ps);
  10. GameEnd(&ps);
  11. setpos(20, 15);
  12. printf("再来一局吗?(Y/N):");
  13. ch = getchar();
  14. getchar();//清理\n
  15. } while (ch == 'Y' || ch == 'y');
  16. setpos(0, 27);
  17. }
  18. int main()
  19. {
  20. srand((unsigned int)time);
  21. setlocale(LC_ALL, "");//设置本地化以打印宽字符
  22. test();
  23. return 0;
  24. }

六,代码参考

1,test.c

  1. #include"snake.h"
  2. void test()
  3. {
  4. int ch = 0;
  5. do
  6. {
  7. snake ps = { 0 };
  8. GameStart(&ps);
  9. GameRun(&ps);
  10. GameEnd(&ps);
  11. setpos(20, 15);
  12. printf("再来一局吗?(Y/N):");
  13. ch = getchar();
  14. getchar();//清理\n
  15. } while (ch == 'Y' || ch == 'y');
  16. setpos(0, 27);
  17. }
  18. int main()
  19. {
  20. srand((unsigned int)time);
  21. setlocale(LC_ALL, "");//设置本地化以打印宽字符
  22. test();
  23. return 0;
  24. }

2,  snake.c

  1. #include"snake.h"
  2. void setpos(int x,int y)
  3. {
  4. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出句柄
  5. COORD pos = { x,y };//设定坐标
  6. SetConsoleCursorPosition(houtput, pos);//设定光标的坐标
  7. }
  8. void welcometogame()
  9. {
  10. setpos(40, 14);
  11. wprintf(L"欢迎来到贪吃蛇游戏\n");
  12. setpos(42, 20);
  13. system("pause");
  14. system("cls");
  15. setpos(25, 14);
  16. wprintf(L"使用 ↑ . ↓ . ← . → 来进行方向移动,f3加速,f4减速\n");
  17. setpos(25, 15);
  18. wprintf(L"加速吃到食物可以增加得分\n");
  19. setpos(42, 20);
  20. system("pause");
  21. system("cls");
  22. }
  23. void creatmap()
  24. {
  25. int i = 0;
  26. //上
  27. for (i = 0; i < 29; i++)
  28. {
  29. wprintf(L"%lc", L'□');
  30. }
  31. //下
  32. setpos(0, 26);//移动光标以便打印下标
  33. for (i = 0; i < 29; i++)
  34. {
  35. wprintf(L"%lc", L'□');
  36. }
  37. //左
  38. for (i = 1; i <= 25; i++)
  39. {
  40. setpos(0, i);//移动光标
  41. wprintf(L"%lc", L'□');
  42. }
  43. //右
  44. for (i = 1; i <= 25; i++)
  45. {
  46. setpos(56, i);//移动光标
  47. wprintf(L"%lc", L'□');
  48. }
  49. }
  50. void InitSnake(psnake ps)
  51. {
  52. int i = 0;
  53. psnakenode cur = NULL;
  54. for (i = 0; i < 5; i++)
  55. {
  56. cur = (psnakenode)malloc(sizeof(snakenode));//创建节点
  57. if (cur == NULL)//判断是否申请成功
  58. {
  59. perror("InitSnake()::malloc()");
  60. return;
  61. }
  62. cur->next = NULL;
  63. //设置节点的坐标
  64. cur->x = POS_X + 2 * i;
  65. cur->y = POS_Y;
  66. //使用头插法
  67. if (ps->_psnake == NULL)//空链表
  68. {
  69. ps->_psnake = cur;
  70. }
  71. else//非空链表
  72. {
  73. cur->next = ps->_psnake;
  74. ps->_psnake = cur;
  75. }
  76. }
  77. cur = ps->_psnake;//从蛇头开始遍历
  78. while (cur)
  79. {
  80. setpos(cur->x, cur->y);//设定光标位置
  81. wprintf(L"%lc", BODY);
  82. cur = cur->next;
  83. }
  84. //设置蛇的其他属性
  85. ps->_dir = RIGHT;
  86. ps->_food_weight = 10;
  87. ps->_score = 0;
  88. ps->_sleep_time = 200;//单位是毫秒
  89. ps->_sta = OK;
  90. }
  91. void CreatFood(psnake ps)
  92. {
  93. int x = 0;
  94. int y = 0;
  95. again:
  96. do
  97. {
  98. x = rand() % 53 + 2;//使横坐标在2~54之内
  99. y = rand() % 24 + 1;//使纵坐标在1~25之内
  100. } while (x % 2 != 0);//若生成的x为奇数,那么就重新生成
  101. psnakenode cur = ps->_psnake;//遍历链表,防止食物与蛇身重合
  102. while (cur)
  103. {
  104. if (x == cur->x && y == cur->y)//如果食物的坐标与蛇身重合
  105. {
  106. goto again;//重新生成
  107. }
  108. cur = cur->next;
  109. }
  110. //打印食物标号
  111. psnakenode food = (psnakenode)malloc(sizeof(snakenode));
  112. if (food == NULL)//防止申请内存失败
  113. {
  114. perror("CreatFood()::malloc()");
  115. return;
  116. }
  117. food->x = x;
  118. food->y = y;
  119. food->next = NULL;
  120. setpos(food->x, food->y);
  121. wprintf(L"%lc", FOOD);
  122. ps->_pfood = food;
  123. //getchar();//观察食物打印效果,之后若不需要观察则可以删除该语句
  124. }
  125. void GameStart(psnake ps)
  126. {
  127. //设置窗口大小,改变窗口名字
  128. system("mode con cols=100 lines=30");
  129. system("title 贪吃蛇");
  130. //隐藏光标
  131. HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
  132. CONSOLE_CURSOR_INFO cursorinfo;
  133. GetConsoleCursorInfo(houtput, &cursorinfo);//获取光标信息
  134. cursorinfo.bVisible = false;//隐藏鼠标
  135. SetConsoleCursorInfo(houtput, &cursorinfo);//设置光标状态
  136. //打印欢迎界面
  137. welcometogame();
  138. //打印地图
  139. creatmap();
  140. //初始化蛇
  141. InitSnake(ps);
  142. //创建食物
  143. CreatFood(ps);
  144. }
  145. //游戏暂停操作
  146. void Pause()
  147. {
  148. while (1)
  149. {
  150. Sleep(200);//死循环休眠则视为暂停
  151. if (KEY_PRESS(VK_SPACE))
  152. {
  153. break;//再次按下空格则跳出循环
  154. }
  155. }
  156. }
  157. //打印游戏帮助信息
  158. void PrintHelpInfo()
  159. {
  160. //打印提⽰信息
  161. setpos(64, 15);
  162. printf("不能穿墙,不能咬到自己\n");
  163. setpos(64, 16);
  164. printf("用↑.↓.←.→分别控制蛇的移动\n");
  165. setpos(64, 17);
  166. printf("F3 为加速,F4 为减速\n");
  167. setpos(64, 18);
  168. printf("ESC :退出游戏.space:暂停游戏.");
  169. }
  170. //判断下一个坐标是否为食物
  171. int NextIsFood(psnakenode pNextNode, psnake ps)
  172. {
  173. return (pNextNode->x == ps->_pfood->x && pNextNode->y == ps->_pfood->y);
  174. }
  175. //下一个坐标为食物
  176. void EatFood(psnakenode pNextNode, psnake ps)
  177. {
  178. //使用头插,将食物节点变为蛇头
  179. ps->_pfood->next = ps->_psnake;
  180. ps->_psnake = ps->_pfood;
  181. //释放掉pNextNode
  182. free(pNextNode);
  183. pNextNode = NULL;
  184. //打印出新蛇
  185. psnakenode cur = ps->_psnake;
  186. while (cur)
  187. {
  188. setpos(cur->x, cur->y);
  189. wprintf(L"%lc", BODY);
  190. cur = cur->next;
  191. }
  192. //加分
  193. ps->_score += ps->_food_weight;
  194. //再次创建食物
  195. CreatFood(ps);
  196. }
  197. //下一个坐标不是食物
  198. void NoFood(psnakenode pNextNode, psnake ps)
  199. {
  200. //头插法
  201. pNextNode->next = ps->_psnake;
  202. ps->_psnake = pNextNode;
  203. //遍历链表
  204. psnakenode cur = ps->_psnake;
  205. while (cur->next->next)
  206. {
  207. setpos(cur->x, cur->y);
  208. wprintf(L"%lc", BODY);
  209. cur = cur->next;
  210. }
  211. //出了循环,cur刚好停在倒数第二个节点,所以先打印两个空格
  212. setpos(cur->next->x, cur->next->y);
  213. printf(" ");
  214. //释放掉尾节点
  215. free(cur->next);
  216. //将倒数第二个节点的next置空
  217. cur->next = NULL;
  218. }
  219. //检测是否撞墙
  220. void KillByWall(psnake ps)
  221. {
  222. if (ps->_psnake->x == 0 || ps->_psnake->x == 56 ||
  223. ps->_psnake->y == 0 || ps->_psnake->y == 26)
  224. {
  225. ps->_sta = KILL_BY_WALL;
  226. }
  227. }
  228. //检测是否撞到自己
  229. void KillBySelf(psnake ps)
  230. {
  231. psnakenode cur = ps->_psnake->next;//从蛇头之后的一个节点开始遍历
  232. while (cur)
  233. {
  234. if (cur->x == ps->_psnake->x && cur->y == ps->_psnake->y)
  235. {
  236. ps->_sta = KILL_BY_SELF;
  237. break;
  238. }
  239. cur = cur->next;
  240. }
  241. }
  242. //蛇走一步
  243. void SnakeMove(psnake ps)
  244. {//先创建一个新节点,表示蛇头即将到达的位置
  245. psnakenode pNextNode = (psnakenode)malloc(sizeof(snakenode));
  246. if (pNextNode == NULL)
  247. {
  248. perror("SnakeMove()::malloc()");
  249. return;
  250. }
  251. switch (ps->_dir)
  252. {
  253. case UP:
  254. pNextNode->x = ps->_psnake->x;
  255. pNextNode->y = ps->_psnake->y - 1;
  256. break;
  257. case DOWN:
  258. pNextNode->x = ps->_psnake->x;
  259. pNextNode->y = ps->_psnake->y + 1;
  260. break;
  261. case LEFT:
  262. pNextNode->x = ps->_psnake->x-2;
  263. pNextNode->y = ps->_psnake->y;
  264. break;
  265. case RIGHT:
  266. pNextNode->x = ps->_psnake->x+2;
  267. pNextNode->y = ps->_psnake->y;
  268. break;
  269. }
  270. //检测下一个坐标是否为食物
  271. if (NextIsFood(pNextNode, ps))
  272. {
  273. EatFood(pNextNode, ps);
  274. }
  275. else
  276. {
  277. NoFood(pNextNode, ps);
  278. }
  279. //检测是否撞墙
  280. KillByWall(ps);
  281. //检测是否撞到自己
  282. KillBySelf(ps);
  283. }
  284. //游戏的运行
  285. void GameRun(psnake ps)
  286. {
  287. //打印游戏帮助信息
  288. PrintHelpInfo();
  289. do
  290. {
  291. //打印游戏得分信息
  292. setpos(64, 10);
  293. printf("总得分:%d\n", ps->_score);
  294. setpos(64, 11);
  295. printf("当前一个食物的分数:%2d\n", ps->_food_weight);
  296. //判断按键以控制蛇的移动
  297. if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//VK_UP为↑的虚拟键码,同时蛇的当前方向不可以向下
  298. {
  299. ps->_dir = UP;
  300. }
  301. else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
  302. {
  303. ps->_dir = DOWN;
  304. }
  305. else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
  306. {
  307. ps->_dir = LEFT;
  308. }
  309. else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
  310. {
  311. ps->_dir = RIGHT;
  312. }
  313. else if (KEY_PRESS(VK_SPACE))//空格为暂停
  314. {
  315. Pause();//Pause函数执行暂停操作
  316. }
  317. else if (KEY_PRESS(VK_ESCAPE))
  318. {
  319. //正常退出游戏
  320. ps->_sta = END_NORMAL;
  321. }
  322. else if (KEY_PRESS(VK_F3))
  323. {
  324. //加速
  325. if (ps->_sleep_time > 80)
  326. {
  327. ps->_sleep_time -= 30;
  328. ps->_food_weight += 2;
  329. }
  330. }
  331. else if (KEY_PRESS(VK_F4))
  332. {
  333. //减速
  334. if (ps->_sleep_time < 300)
  335. {
  336. ps->_sleep_time += 30;
  337. ps->_food_weight -= 2;
  338. }
  339. }
  340. SnakeMove(ps);//蛇走一步
  341. Sleep(ps->_sleep_time);
  342. } while (ps->_sta == OK);
  343. }
  344. //游戏结束
  345. void GameEnd(psnake ps)
  346. {
  347. setpos(24, 12);//设置光标位置,打印结束信息
  348. switch (ps->_sta)
  349. {
  350. case END_NORMAL:
  351. printf("游戏正常结束\n");
  352. break;
  353. case KILL_BY_WALL:
  354. printf("您撞到了墙,游戏结束\n");
  355. break;
  356. case KILL_BY_SELF:
  357. printf("您撞到了自己,游戏结束\n");
  358. break;
  359. }
  360. //释放蛇身
  361. psnakenode cur = ps->_psnake;
  362. while (cur)
  363. {
  364. psnakenode del = cur;
  365. cur = cur->next;
  366. free(del);
  367. del = NULL;
  368. }
  369. }

3,  snake.h

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<windows.h>
  5. #include<stdbool.h>
  6. #include<locale.h>
  7. #include<time.h>
  8. #define POS_X 24
  9. #define POS_Y 5
  10. #define WALL L'□'
  11. #define BODY L'●'
  12. #define FOOD L'※'
  13. #define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
  14. //定义蛇身体的节点
  15. typedef struct snakenode
  16. {
  17. //坐标
  18. int x;
  19. int y;
  20. struct snakenode* next;//下一个节点
  21. }snakenode,*psnakenode;
  22. //定义方向
  23. enum direction
  24. {
  25. UP = 1,
  26. DOWN,
  27. LEFT,
  28. RIGHT
  29. };
  30. //蛇的状态
  31. enum game_state
  32. {
  33. OK,//正常状态
  34. KILL_BY_WALL,//撞到墙
  35. KILL_BY_SELF,//撞到了自己
  36. END_NORMAL//正常结束游戏
  37. };
  38. //贪吃蛇
  39. typedef struct snake
  40. {
  41. psnakenode _psnake;//蛇头节点
  42. psnakenode _pfood;//食物节点
  43. enum direction _dir;//蛇的方向
  44. enum game_state _sta;//游戏状态
  45. int _food_weight;//一个食物的分数
  46. int _score;//总成绩
  47. int _sleep_time;//蛇的速度
  48. }snake,* psnake;
  49. //设置光标位置
  50. void setpos(int x, int y);
  51. //开始游戏
  52. void GameStart(psnake ps);
  53. //打印欢迎界面
  54. void welcometogame();
  55. //设置光标坐标
  56. void setpos(int x, int y);
  57. //打印游戏地图
  58. void creatmap();
  59. //蛇的初始化
  60. void InitSnake(psnake ps);
  61. //创建食物
  62. void CreatFood(psnake ps);
  63. //游戏的运行
  64. void GameRun(psnake ps);
  65. //打印游戏帮助信息
  66. void PrintHelpInfo();
  67. //游戏暂停操作
  68. void Pause();
  69. //蛇走一步
  70. void SnakeMove(psnake ps);
  71. //判断下一个坐标是否为食物
  72. int NextIsFood(psnakenode pNextNode, psnake ps);
  73. //下一个坐标为食物
  74. void EatFood(psnakenode pNextNode, psnake ps);
  75. //下一个坐标不是食物
  76. void NoFood(psnakenode pNextNode, psnake ps);
  77. //检测是否撞墙
  78. void KillByWall(psnake ps);
  79. //检测是否撞到自己
  80. void KillBySelf(psnake ps);
  81. //游戏结束
  82. void GameEnd(psnake ps);

        快去享受游戏吧!!! 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/516301
推荐阅读
相关标签
  

闽ICP备14008679号