一 概述
1 2
| 本文详细阐述了如何利用双向链表构建贪吃蛇游戏,涉及节点结构、移动机制、食物处理、界面显示以及内存管理。 着重介绍了链表节点的定位、移动函数的设计以及如何优化界面渲染以减少闪烁
|
二 贪吃蛇演示

三 过程分析
使用双向链表实现此游戏有以下几点需要做重点分析
3.1 分析1(节点构成)
1 2 3 4 5 6 7 8 9 10 11 12
| 双向链表中各个节点的标准构成是一个数据域和 2 个指针域,但对于实现贪吃蛇游戏来说, 由于各个节点的位置是随贪吃蛇的移动而变化的,因此链表中的各节点还需要随时进行定位。
在一个二维画面中,定义一个节点的位置,至少需要所在的行号和列号这 2 个数据。 由此,可以得出构成贪吃蛇的双向链表中各节点的构成:
// 创建表示蛇各个节点的结构体 typedef struct SnakeNode { int x, y; // 记录节点所在的行和列 struct SnakeNode *pre; // 指向前驱节点的指针 struct SnakeNode *next; // 指向后续节点的指针 }Node, *pNode;
|
3.2 分析2(贪吃蛇移动)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 贪吃蛇的移动,本质上就是对链表中各个节点的重新定位。 除非贪吃蛇吃到食物,否则无论怎样移动都不会对双向链表的整个结构(节点数)产生影响, 唯一受影响的是各个节点中(x,y)这对定位数据。
由此,可以试着设计出实现贪吃蛇移动的功能函数: 1.从蛇尾(双向链表尾节点)开始,移动向前遍历,过程中依次将当前节点的(x,y)修改为前驱节点的(x,y), 由此可实现整个蛇身(除首元节点外的其它所有节点)的向前移动 2.接收用户输入的移动指令,根据用户指示贪吃蛇向左、向右、向上还是向下移动, 首元节点中的 (x,y) 分别做 x-1、x+1、y-1 和 y+1 运算
move()函数实现了贪吃蛇的移动: 注意:此段代码中还调用了SnakeDeath()函数,用于判断贪吃蛇移动时是否撞墙、撞自身,如果是则游戏结束 // 贪吃蛇移动的过程, 即链表中所有节点从尾结点开始逐个向前移动一个位置 bool Move(pNode pHead, char key) { bool game_over = false; pNode pt = pTail; while (pt != pHead) { // 每个节点依次向前完成蛇的移动 pt->x = pt->pre->x; pt->y = pt->pre->y; pt = pt->pre; } switch (key) { case'd': { pHead->x += 1; if (pHead->x >= ROW) game_over = true; break; } case'a': { pHead->x -= 1; if (pHead->x < 0) game_over = true; break; } case's': { pHead->y += 1; if (pHead->y >= COL) game_over = true; break; } case'w': { pHead->y -= 1; if (pHead->y < 0) game_over = true;; break; } } if (SnakeDeath(pHead)) game_over = true; return game_over; }
|
3.3 分析3(贪吃蛇吃到食物)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 当贪吃蛇吃到食物时,贪吃蛇需要增加一截,其本质就是双向链表增加一个节点。 前面章节已经介绍如何在双向链表中增加一个节点,因此实现这个功能唯一的难点在于:如何为该节点初始化(x,y)?
本节所设计的贪吃蛇游戏,针对此问题,提供了最简单的解决方案: 不对新节点 x 和 y 做初始化。 贪吃蛇是时刻移动的,而在上面的 move() 函数中,会时刻修正贪吃蛇每个节点的位置, 因此当为双向链表添加新节点后,只要贪吃蛇移动一步,新节点的位置就会自行更正。
也就是说,贪吃蛇吃到食物的实现,就仅是给双向链表添加一个新节点。 如下即为实现此功能的代码: // 创建表示食物的结构体, 其中只需要记录其所在的行和列 typedef struct Food { int x; int y; }Food, *pFood; // 吃食物, 等同于链表中新增一个节点 pNode EatFood(pNode pHead, pFood pFood) { pNode p_add = NULL, pt = NULL; if (pFood->x == pHead->x&&pFood->y == pHead->y) { p_add = (pNode)malloc(sizeof(Node)); score++; pTail->next = p_add; p_add->pre = pTail; p_add->next = NULL; pTail = p_add; // 检查食物是否出现在蛇身的位置上 do { *pFood = CreateFood(); } while (FoodInSnake(pHead, pFood)); } return pHead; }
|
二、Food说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Food 结构体用来表示食物,其内部仅包含能够定位食物位置的 (x,y) 即可。
另外,此段代码中还调用了 FoodeInSnake() 函数,由于食物的位置是随机的, 因此极有可能会和贪吃蛇重合,所以此函数的功能是:如果重合,就重新生成食物。
FoodInSnake() 函数的实现: // 判断食物的出现位置是否和蛇身重合 bool FoodInSnake(pNode pHead, pFood pFood) { pNode pt = NULL; for (pt = pHead; pt != NULL; pt = pt->next) { if (pFood->x == pt->x&&pFood->y == pt->y) return true; } return false; }
|
3.4 分析4(贪吃蛇界面显示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 贪吃蛇游戏界面的显示,最简单的制作方法:贪吃蛇每移动一次,都清除屏幕并重新生成一次。 这样实现的问题在于,如果贪吃蛇的移动速度过快,则整个界面在渲染的同时,会掺杂着光标,并且屏幕界面会频繁闪动。 因此,在渲染界面时,有必要将光标隐藏起来,这需要用到<windows.h>头文件,实现代码如下: // 隐藏光标 void gotoxy(int x, int y) { HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos; pos.X = x; pos.Y = y; SetConsoleCursorPosition(handle, pos); } void HideCursor() { CONSOLE_CURSOR_INFO cursor_info = { 1, 0 }; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info); }
同时,为了给整个界面渲染上颜色,也需要引入<windows.h>头文件,并使用如下函数 void color(int m) { HANDLE consolehend; consolehend = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(consolehend, m); }
|
3.5 分析5(游戏结束,释放空间)
注意:由此结束后,一定要手动释放双向链表占用的堆空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // 退出游戏前, 手动销毁链表中各个节点 void ExitGame(pNode *pHead) { pNode p_delete = NULL, p_head = NULL; while (*pHead != NULL) { p_head = (*pHead)->next; if (p_head != NULL) p_head->pre = NULL; p_delete = *pHead; free(p_delete); p_delete = NULL; *pHead = p_head; } }
|
四 参考