C++基于EasyX制作贪吃蛇游戏(一)文档

本文最后更新于:2019年11月14日 晚上

很早就想制作一个贪吃蛇游戏,老是写了一点就半途而废,终于狠下心来花了半天的时间写完了第一版本的贪吃蛇游戏,虽然因为坐标设置的原因导致有些小失败,但依旧完成了主要的功能模块,记录一下。

前提:EasyX

EasyX 是针对 C++ 的图形库,可以帮助 C 语言初学者快速上手图形和游戏编程。

官网链接:https://easyx.cn/

制作流程

先开始写文档,分析要实现哪些功能,然后对功能进行细分,梳理操作。以下80%的内容都是当时写贪吃蛇的文档,后期整理一下发博客记录。

草稿:想要实现的功能

贪吃蛇游戏的简单版本实现

  1. 贪吃蛇吃食物身体加长
  2. 食物被吃后随机出现(不能够出现在蛇的身上)
  3. 无操作时贪吃蛇自动向蛇头方向前进
  4. 上下左右移动位置
  5. 撞到身体或者墙壁死亡
  6. 显示时间与分数

功能梳理

蛇的操作

  1. 上下左右四个按键控制蛇头的朝向。当按键方向与蛇头方向相同或者相反时无反应。
  2. 不受操作时蛇自动按照蛇头方向前行
  3. 蛇头吃到食物之后,身体加长
  4. 蛇头撞到自身或者墙壁后死亡
  5. 蛇头控制方向,其余结点均是复制前一个结点的坐标。

食物的操作

  1. 初始时刻食物出现在某个固定位置
  2. 食物被吃掉后随机出现在其他地方,但是不能够出现在蛇的身上。

界面显示

  1. 分为游戏区与功能区,游戏区展现蛇与食物,功能区展示时间、分数、玩法介绍以及作者信息
  2. 蛇使用方块表示,蛇头与蛇身容易区分。食物使用圆形表示。
  3. 游戏结束后,显示“GAME OVER”,然后显示游戏时长以及游戏分数。

界面大小设置

  1. 坐标系。这是一个纯2D游戏,一般来说坐标都是在最左上角,为了便于计算(个人习惯),将坐标原点放置在左下角,x轴向右,y轴向上。

实际上游戏的坐标都是采用原点在左上角,这里因为我设置了坐标轴的原因导致了后面文字摆放无法放置正确,也是因为这个原因,要重写了第二版代码。

  1. 整体界面大小为:长640px,宽480px。游戏区为 480 * 480的正方形区域,位居界面左侧。功能区为 160 * 480的区域,位居界面右侧。
  2. 游戏区全部分为20 * 20 px的小正方形区域,便于蛇与食物的放置即坐标计算。蛇使用方块,食物使用圆形。采取图形中心点代表整个图形的方式,即(10,10)表示一个图形。为了防止边界的图像颜色干扰,设置蛇的方块中心点到四边的距离为9px,食物的半径为8px。

变量结构设置

食物结构体设置

对于食物而言,一次只会出现一个食物,只需要保存它的坐标位置,以及存在状态即可。

1
2
3
4
5
6
struct Food			//食物结构体
{
int x; //横坐标
int y; //纵坐标
bool exist; //是否存在,1表示存在
};

蛇结构体设置

对于蛇而言,蛇是由一个个节点构成的,每吃一个食物就会增加一个节点。并且所有节点都连接起来,采用链表是一个不错的选择。

结构体设置模仿自严蔚敏老师的数据结构链表的设置。定义蛇节点结构体,然后定义蛇的结构体。

  1. 本来采用的单向链表,但后续处理蛇的移动算法时发现需要从后向前复制节点的操作,于是改成了双向链表。

  2. 这里也可以采用数组啦,STL的vector容器啦来处理蛇的结构体。采用链表复习复习数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct Node	//蛇的节点
{
int x; //横坐标
int y; //纵坐标
struct Node * next; //指向下一个节点的指针
struct Node * pre; //指向前一个节点的指针
}* LinkNode;

struct Snake //蛇的结构体
{
LinkNode head; //指向头节点的指针
LinkNode tail; //指向尾节点的指针
int direction; //蛇头方向
int num; //节点数目
};

游戏核心算法

蛇身移动算法

贪吃蛇的移动完全是复制头部节点,蛇头控制方向与前进,其他节点均是重复前一个节点的行为。

而在处理蛇移动的时候,先要用后面节点的保存了它的前一个节点的坐标信息。故蛇身移动的算法,是从最后一个节点(tail)向前复制前一个节点的坐标,达成移动效果。

想到这里就把蛇的结构体改为双向链表。便于我的操作。

1
2
3
4
5
6
7
8
LinkNode linknode = snake.tail;
while (linknode != snake.head)
{
linknode->x = linknode->pre->x;
linknode->y = linknode->pre->y;

linknode = linknode->pre;
}

食物生成算法

食物生成要具有随机性,并且要求食物不能够生成在蛇的身体上。

随机数函数rand()产生随机数,rand()函数需要的头文件是:<stdlib.h>。

rand()会返回一个范围在0到RAND_MAX(32767)之间的伪随机数(整数)。

在调用rand()函数之前,可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。

srand( usigned int seed)函数用来设置rand()产生随机数时的随机数种子。参数seed是整数,通常可以利用time(0)的返回值作为seed。srand()函数需要的头文件是:<stdlib.h>

例如:生成0-6之间的任意一个随机数

srand(time(0));

int num = (rand()%7)//模求余

食物不能够在蛇的身体上,即生成坐标之后遍历蛇的节点进行匹配,若有对应的就直接重新生成一次。

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
int x;
int y;
while (1)
{
srand(time(0));
x = (rand() % 24) * 20 + 10;
y = (rand() % 24) * 20 + 10;

//食物位置检测算法
LinkNode linknode = snake.head;
bool cont = true;
while (linknode != snake.tail->next)//从头遍历到尾巴
{
if (linknode->x == x && linknode->y == y)
{
cont = false;
break;
}
linknode = linknode->next;
}
if (cont)
{
break; //如果食物不在蛇的身体上,就break退出循环
}
}

food.x = x;
food.y = y;
food.exist = true;

按键处理问题

_khbit()函数用来检测是否有键盘输入。如果有按键被按下,会返回一个1,否则返回值为0.它是一个非阻塞函数,无论有没有按键被按下,他都会立即返回结果。可以用来做循环的条件判断是否有按键,或者等待输入。

_getch()函数作用是从控制台获取输入的字符,并返回获取到的字符值。而且这个函数是阻塞性函数,必须要获取输入字符后才会返回。

_kbhit()_getch()都是位于conio.h头文件中。(console IO)

1
2
3
4
5
6
7
8
9
10
int keys = 0;
int key;
while (1)
{
if (keys = _kbhit())
{
key = _getch();
cout << "按键的值key:" << key << endl;
}
}

由此可以测的我们按键对应的键值是多少。

方向键↑ 对应值为72。

方向键↓ 对应值为80。

方向键← 对应值为75。

方向键→ 对应值为77。

但是相信执行这一段代码会发现按一次键会有两个key被打印出来。但是写switch语句(代码中的按键处理模块)时却完全和第一个key值无关。这个问题暂时还没有解决!路过的大佬欢迎留言~

这个方法我是从某潭某州的公开广告课上学到的。

其他参考链接:C++中_kbhit()函数与_getch()函数

伪流程

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
int main()
{
初始化资源;

绘制界面;

开始游戏:
while(1)
{
if(!食物存在)
{
刷新食物并显示
}

if(按键)
{
相应操作:蛇头方向:上下左右
}

向蛇头方向前进1格。绘制蛇头
(switch case结构)

消除蛇尾图像。

judge(蛇头吃食物)
{
蛇结点+1,尾巴加长并显示出来
分数增加
食物状态刷新
}

judge(撞墙or撞自己)
{
死亡
game over
break
}

sleep(200)
}
结束 回收资源;
}