赞
踩
这一节中,我们来做一款经典小游戏,贪吃蛇。先看看最终效果图
在开始之前,我们把窗体创建好。
创建一个800 * 600的窗体。这一次我们使用默认的原点和坐标轴:原点在窗体左上角,X轴正方向向右,Y轴正方向向下。背景色设置为RGB(164, 225, 202),最后调用cleardevice函数,使用背景色清空整个窗体。
- #include <easyx.h>
- #include <stdio.h>
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- getchar();
- closegraph();
- return 0;
- }
将整个800 * 600的窗体,水平分隔为20等分,垂直分隔为15等分,作为整个游戏的网格坐标系统。在上图中,蛇用5格白色的矩形表示。食物用黄色的一格矩形表示。
这样,蛇的每一格身体坐标为:
(5, 7)
(4, 7)
(3, 7)
(2, 7)
(1, 7)
食物的坐标为:
(12, 7)
为了方便观察,把窗体用线段画上上述网格。网格每一格的宽度设为40像素,用符号常量NODE_WIDTH
表示。
#define NODE_WIDTH 40
先绘制竖向的线段。
竖向线段中,起始点y
坐标固定为0
,终止点y
坐标固定为600
。
每条线段的起始点与终止点的x
坐标一致,且随着线段不同而变化。
设线段条数从0开始计数。
第0条线段: 起始点、终止点的x
坐标为0
。
第10条线段: 起始点、终止点的x
坐标为10 * NODE_WIDTH
。
第20条线段: 起始点、终止点的x
坐标为20 * NODE_WIDTH
。
观察各线段起始点终止点坐标,可以总结出规律:
第n条线段:起始点(n * NODE_WIDTH, 0)
、终止点(n * NODE_WIDTH, 600)
。且x
坐标的范围为[0, 800]
。
- // 竖线
- for (int x = 0; x <= 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
再绘制横向的线段。
横向线段中,起始点x
坐标固定为0
,终止点x
坐标固定为800
。
每条线段的起始点与终止点的y
坐标一致,且随着线段不同而变化。
设线段条数从0开始计数。
第0条线段: 起始点、终止点的y
坐标为0
。
第8条线段: 起始点、终止点的y
坐标为8 * NODE_WIDTH
。
第15条线段: 起始点、终止点的y
坐标为15 * NODE_WIDTH
。
观察各线段起始点终止点坐标,可以总结出规律:
第n条线段:起始点(0, n * NODE_WIDTH)
、终止点(800, n * NODE_WIDTH)
。且y
坐标的范围为[0, 600]
。
- // 横线
- for (int y = 0; y <= 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
将绘制网格的代码封装成函数paintGrid
。
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }

主函数中调用paintGrid
函数,给整个窗体绘制上网格。
现阶段代码如下:
- #include <easyx.h>
- #include <stdio.h>
-
- #define NODE_WIDTH 40
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 绘制网格
- paintGrid();
-
- getchar();
- closegraph();
- return 0;
- }

设定初始状态下,蛇有5个节点。我们把之前设置的网格规划出来的坐标称作网格坐标。每个节点的网格坐标为:
(5, 7)
(4, 7)
(3, 7)
(2, 7)
(1, 7)
每一个蛇节点使用一个白色正方形表示,而绘制矩形需要左上角和右下角的实际坐标。白色矩形的左上角坐标与右下角实际坐标为:
左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
右下角:【(网格x坐标
+ 1) * 网格宽度
, (网格y坐标
+ 1) * 网格宽度
】
每个节点需要存储x
坐标、y
坐标两个坐标,可以定义含有两个int
成员的结构用于表示节点。
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
在主函数中,声明node
的数组,并将前5个元素初始化为蛇的初始位置。设节点(5, 7)
为蛇头,蛇头存储在数组的首元素中。
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
现在,我们定义一个函数paintSnake
用于将所有组成蛇的矩形绘制出来。
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
paintSnake
函数需要蛇节点数组首元素指针和蛇节点个数两个参数。在主函数中定义一个变量int length
用于记录蛇长度,目前初始化为5。声明蛇节点数组,绘制网格及蛇节点。
// 蛇节点坐标
node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
// 蛇节点长度
int length = 5;
// 绘制网格
paintGrid();
// 绘制蛇节点
paintSnake(snake, length);
现阶段代码如下:
- #include <easyx.h>
- #include <stdio.h>
-
- #define NODE_WIDTH 40
-
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
- // 蛇节点长度
- int length = 5;
-
- // 绘制网格
- paintGrid();
- // 绘制蛇节点
- paintSnake(snake, length);
-
- getchar();
- closegraph();
- return 0;
- }

现在,我们希望让蛇向右运动,蛇头节点为(5, 7)
。以下两幅图为初始位置和向右移动一步后的位置。
初始位置:
(5, 7)
(4, 7)
(3, 7)
(2, 7)
(1, 7)
向右移动一步后位置:
(6, 7)
(5, 7)
(4, 7)
(3, 7)
(2, 7)
可以看出移动后,蛇节点中,尾部节点(1, 7)
被删除,而新增了一个头部节点(6,7)
。
怎样对存储蛇节点的数组进行操作,可以达到删除尾部节点,而新增一个头节点的效果呢?
从蛇尾节点前一个节点开始,即从数组下标为3
的元素开始。元素3
设置为当前元素,将当前元素的值,移动到后一个元素当中。接着,将当前元素设置为元素2
,重复该动作,直到数组元素0
的值,移动到元素1
为止。
元素3-->元素4
元素2-->元素3
元素1-->元素2
元素0-->元素1
这样即可把蛇尾节点去掉,并留出了新蛇头节点的位置。
若蛇向右运动,那么将新蛇头设置为(6,7)
即可。
但是,蛇头除了可以向右运动,还可以做另外3个方向的运动,共4个方向的运动。
上
下
左
右
设旧蛇头坐标为(x, y)
。对于各个方向的运动,新蛇头的网格坐标相对于旧蛇头的坐标作如下运算:
上:(x, y + 1)
下:(x, y - 1)
左:(x - 1, y)
右:(x + 1, y)
为了更加明确地在程序中表明方向,我们将4个方向声明为枚举类型。
- // 方向枚举
- enum direction
- {
- eUp,
- eDown,
- eLeft,
- eRight
- };
接着定义一个函数snakeMove
,传入数组首元素指针、蛇节点个数和蛇前进方向。它将按照上述方法,依次移动蛇节点并根据前进方向设置蛇头。
- // 蛇节点移动
- void snakeMove(node* snake, int length, int direction)
- {
- // 从尾结点开始,前一个节点覆盖后一个节点
- // 4 3 2 1 0 4 3 2 1 0
- // E D C B A ---> D E C B A
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
-
- // 根据方向,确定下一个头节点
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else // right
- {
- newHead.x++;
- }
-
- // 更新头节点
- // D E C B A ---> D E C B N
- snake[0] = newHead;
- }

在主函数中放置一个死循环,循环体中依次执行以下步骤:
清空整个窗体
绘制网格
绘制蛇节点
休眠500ms
向右移动蛇节点
while (1)
{
// 清空整个窗体
cleardevice();
// 绘制网格
paintGrid();
// 绘制蛇节点
paintSnake(snake, length);
// 休眠500ms
Sleep(500);
// 向右移动蛇节点
snakeMove(snake, length, eRight);
}
现阶段代码如下:
- #include <easyx.h>
- #include <stdio.h>
-
- #define NODE_WIDTH 40
-
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
-
- // 方向枚举
- enum direction
- {
- eUp,
- eDown,
- eLeft,
- eRight
- };
-
-
- // 蛇节点移动
- void snakeMove(node* snake, int length, int direction)
- {
- // 从尾结点开始,前一个节点覆盖后一个节点
- // 4 3 2 1 0 4 3 2 1 0
- // E D C B A ---> D E C B A
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
-
- // 根据方向,确定下一个头节点
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else // right
- {
- newHead.x++;
- }
-
- // 更新头节点
- // D E C B A ---> D E C B N
- snake[0] = newHead;
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
- // 蛇节点长度
- int length = 5;
-
- while (1)
- {
- // 清空整个窗体
- cleardevice();
- // 绘制网格
- paintGrid();
- // 绘制蛇节点
- paintSnake(snake, length);
- // 休眠500ms
- Sleep(500);
- // 向右移动蛇节点
- snakeMove(snake, length, eRight);
- }
-
-
- getchar();
- closegraph();
- return 0;
- }

现在,程序开始后蛇会向右移动,接下来,加入用键盘来控制蛇的移动方向的功能。
键盘交互功能如下:
按下w键,蛇向上移动
按下s键,蛇向下移动
按下a键,蛇向左移动
按下d键,蛇向右移动
在主函数中声明一个枚举变量d
,初始为向右移动。
- // 移动方向
- enum direction d = eRight;
将d
传递给snakeMove
函数的第三个参数。snakeMove
函数的第三个参数可以根据方向设置新蛇头的位置。若蛇需要更改移动方向,只要更改枚举变量d
即可。
- // 枚举变量d,控制蛇的移动方向
- snakeMove(snake, length, d);
与之前的键盘交互一样,使用_getch
与_kbhit
函数配合,可以获取键盘输入且不会导致程序阻塞。使用这两个函数别忘了包含头文件#include <conio.h>
。获取到键盘输入后,通过传入枚举变量指针pD
,修改变量枚举值。
- // 键盘输入改变direction
- void changeDirection(enum direction* pD)
- {
- // 检查输入缓存区中是否有数据
- if (_kbhit() != 0)
- {
- // _getch函数获取输入缓存区中的数据
- char c = _getch();
- // 判断输入并转向
- switch (c)
- {
- case 'w':
- // 向上移动
- *pD = eUp;
- break;
- case 's':
- // 向下移动
- *pD = eDown;
- break;
- case 'a':
- // 向左移动
- *pD = eLeft;
- break;
- case 'd':
- // 向右移动
- *pD = eRight;
- break;
- }
- }
- }

这里还需要注意一个问题,蛇不能后退,如果新的方向与原方向相反,那么按键无效。
- // 键盘输入改变direction
- void changeDirection(enum direction* pD)
- {
- // 检查输入缓存区中是否有数据
- if (_kbhit() != 0)
- {
- // _getch函数获取输入缓存区中的数据
- char c = _getch();
- // 判断输入并转向
- switch (c)
- {
- case 'w':
- // 向上移动
- if (*pD != eDown)
- *pD = eUp;
- break;
- case 's':
- // 向下移动
- if (*pD != eUp)
- *pD = eDown;
- break;
- case 'a':
- // 向左移动
- if (*pD != eRight)
- *pD = eLeft;
- break;
- case 'd':
- // 向右移动
- if (*pD != eLeft)
- *pD = eRight;
- break;
- }
- }
- }

主函数中,在snakeMove
函数前调用changeDirection
函数检查是否有键盘输入,若有输入且非回头方向的输入,则改变方向枚举变量d
。新的方向枚举变量d
传入snakeMove
函数后,即可使用新方向设置新蛇头的位置,实现蛇改变移动方向功能。
// 蛇节点坐标
node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
// 蛇节点长度
int length = 5;
enum direction d = eRight;
while (1)
{
cleardevice();
paintGrid();
paintSnake(snake, length);
Sleep(500);
// 获取键盘输入并将方向存储到变量d
changeDirection(&d);
// 根据变量d的方向移动蛇节点
snakeMove(snake, length, d);
}
现阶段代码:
- #include <easyx.h>
- #include <stdio.h>
- #include <conio.h>
-
- #define NODE_WIDTH 40
-
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
-
- // 方向枚举
- enum direction
- {
- eUp,
- eDown,
- eLeft,
- eRight
- };
-
-
- // 蛇节点移动
- void snakeMove(node* snake, int length, int direction)
- {
- // 从尾结点开始,前一个节点覆盖后一个节点
- // 4 3 2 1 0 4 3 2 1 0
- // E D C B A ---> D E C B A
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
-
- // 根据方向,确定下一个头节点
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else // right
- {
- newHead.x++;
- }
-
- // 更新头节点
- // D E C B A ---> D E C B N
- snake[0] = newHead;
- }
-
- // 键盘输入改变direction
- void changeDirection(enum direction* pD)
- {
- // 检查输入缓存区中是否有数据
- if (_kbhit() != 0)
- {
- // _getch函数获取输入缓存区中的数据
- char c = _getch();
- // 判断输入并转向
- switch (c)
- {
- case 'w':
- // 向上移动
- if (*pD != eDown)
- *pD = eUp;
- break;
- case 's':
- // 向下移动
- if (*pD != eUp)
- *pD = eDown;
- break;
- case 'a':
- // 向左移动
- if (*pD != eRight)
- *pD = eLeft;
- break;
- case 'd':
- // 向右移动
- if (*pD != eLeft)
- *pD = eRight;
- break;
- }
- }
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
- // 蛇节点长度
- int length = 5;
- enum direction d = eRight;
-
- while (1)
- {
- // 清空整个窗体
- cleardevice();
- // 绘制网格
- paintGrid();
- // 绘制蛇节点
- paintSnake(snake, length);
- // 休眠500ms
- Sleep(500);
- // 获取键盘输入并将方向存储到变量d
- changeDirection(&d);
- // 根据变量d的方向移动蛇节点
- snakeMove(snake, length, d);
- }
-
- getchar();
- closegraph();
- return 0;
- }

目前蛇的部分已经完成了,还差需要创建食物,让蛇吃到食物后长大。
主函数中声明一个node
类型的变量作为食物的节点。
node food;
食物的位置是随机生成的,但是有两个要求:
不能生成在窗体外
不能生成在蛇节点上
对于网格坐标来说,宽度有800/NODE_WIDTH
,即20格。高度有600 / NODE_WIDTH
,即15格。限制食物的x
坐标在区间[0, 19]
以内,y
坐标在区间[0, 14]
内。
- food.x = rand() % (800 / NODE_WIDTH); // 区间[0, 19]内
- food.y = rand() % (600 / NODE_WIDTH); // 区间[0, 14]内
对于第二个条件,只能遍历所有蛇节点,检查是否食物与蛇任何一个节点重合了。如果重合,那么重新随机生成一次食物,直到食物与所有蛇节点不重合为止。
- while (1)
- {
- food.x = rand() % (800 / NODE_WIDTH);
- food.y = rand() % (600 / NODE_WIDTH);
-
- int i;
- for (i = 0; i < length; i++)
- {
- if (snake[i].x == food.x && snake[i].y == food.y)
- {
- break;
- }
- }
- if (i < length)
- continue;
- else
- break;
- }

将生成食物封装成createFood
函数,函数传入两个参数,一个参数为蛇节点数组首元素指针,另一个参数为蛇节点个数。生成完成食物坐标后,返回装有食物坐标的food
结构。
- // 随机创建食物
- node createFood(node* snake, int length)
- {
- node food;
- while (1)
- {
- food.x = rand() % (800 / NODE_WIDTH);
- food.y = rand() % (600 / NODE_WIDTH);
-
- int i;
- for (i = 0; i < length; i++)
- {
- if (snake[i].x == food.x && snake[i].y == food.y)
- {
- break;
- }
- }
- if (i < length)
- continue;
- else
- break;
- }
- return food;
- }

根据createFood
函数返回的食物坐标,在窗体上绘制一个黄色矩形代表食物。将绘制食物的代码封装成函数paintFood
。
食物的左上角坐标为:【food.x * NODE_WIDTH, food.y * NODE_WIDTH】
食物的右下角坐标为:【(food.x + 1) * NODE_WIDTH, (food.y + 1) * NODE_WIDTH】
- // 绘制食物
- void paintFood(node food)
- {
- int left, top, right, bottom;
- left = food.x * NODE_WIDTH;
- top = food.y * NODE_WIDTH;
- right = (food.x + 1) * NODE_WIDTH;
- bottom = (food.y + 1) * NODE_WIDTH;
- setfillcolor(YELLOW);
- solidrectangle(left, top, right, bottom);
- setfillcolor(WHITE);
- }
主函数中,第一次生成食物在循环外。循环内部每次循环绘制一次食物。使用了随机数,别忘了使用当前时间作为随机数种子。另外,time
函数需要包含头文件#include <time.h>
。
// 随机生成食物
srand(unsigned int(time(NULL)));
node food = createFood(snake, length);
while (1)
{
cleardevice();
paintGrid();
paintSnake(snake, length);
// 绘制食物
paintFood(food);
Sleep(500);
changeDirection(&d);
snakeMove(snake, length, d);
}
现阶段代码:
- #include <easyx.h>
- #include <stdio.h>
- #include <conio.h>
- #include <time.h>
-
- #define NODE_WIDTH 40
-
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
-
- // 方向枚举
- enum direction
- {
- eUp,
- eDown,
- eLeft,
- eRight
- };
-
-
- // 蛇节点移动
- void snakeMove(node* snake, int length, int direction)
- {
- // 从尾结点开始,前一个节点覆盖后一个节点
- // 4 3 2 1 0 4 3 2 1 0
- // E D C B A ---> D E C B A
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
-
- // 根据方向,确定下一个头节点
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else // right
- {
- newHead.x++;
- }
-
- // 更新头节点
- // D E C B A ---> D E C B N
- snake[0] = newHead;
- }
-
- // 键盘输入改变direction
- void changeDirection(enum direction* pD)
- {
- // 检查输入缓存区中是否有数据
- if (_kbhit() != 0)
- {
- // _getch函数获取输入缓存区中的数据
- char c = _getch();
- // 判断输入并转向
- switch (c)
- {
- case 'w':
- // 向上移动
- if (*pD != eDown)
- *pD = eUp;
- break;
- case 's':
- // 向下移动
- if (*pD != eUp)
- *pD = eDown;
- break;
- case 'a':
- // 向左移动
- if (*pD != eRight)
- *pD = eLeft;
- break;
- case 'd':
- // 向右移动
- if (*pD != eLeft)
- *pD = eRight;
- break;
- }
- }
- }
-
- // 绘制食物
- /*
-
- (x * NODE_WIDTH, y * NODE_WIDTH)
- @-----------
- | |
- | |
- | |
- | |
- | |
- -----------@ ((x + 1) * NODE_WIDTH, (y + 1) * NODE_WIDTH)
-
- */
- void paintFood(node food)
- {
- int left, top, right, bottom;
- left = food.x * NODE_WIDTH;
- top = food.y * NODE_WIDTH;
- right = (food.x + 1) * NODE_WIDTH;
- bottom = (food.y + 1) * NODE_WIDTH;
- setfillcolor(YELLOW);
- solidrectangle(left, top, right, bottom);
- setfillcolor(WHITE);
- }
-
-
- // 随机创建食物
- node createFood(node* snake, int length)
- {
- node food;
- while (1)
- {
- food.x = rand() % (800 / NODE_WIDTH);
- food.y = rand() % (600 / NODE_WIDTH);
-
- int i;
- for (i = 0; i < length; i++)
- {
- if (snake[i].x == food.x && snake[i].y == food.y)
- {
- break;
- }
- }
- if (i < length)
- continue;
- else
- break;
- }
- return food;
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
- // 蛇节点长度
- int length = 5;
- enum direction d = eRight;
-
- // 食物
- srand(unsigned int(time(NULL)));
- node food = createFood(snake, length);
-
- while (1)
- {
- // 清空整个窗体
- cleardevice();
- // 绘制网格
- paintGrid();
- // 绘制蛇节点
- paintSnake(snake, length);
- // 绘制食物
- paintFood(food);
- // 休眠500ms
- Sleep(500);
- // 获取键盘输入并将方向存储到变量d
- changeDirection(&d);
- // 根据变量d的方向移动蛇节点
- snakeMove(snake, length, d);
- }
-
- getchar();
- closegraph();
- return 0;
- }

观察下图,蛇头为节点(11, 7)
。而蛇目前向右运动,下一步即将吃到在(12, 7)
位置的食物。
现在蛇头移动到了(12, 7)
的位置,并吃掉了食物。原蛇尾节点(6, 7)
本应该删除,但是这时蛇吃掉了食物,需要长大一节。再将蛇尾节点(6, 7)
加回来。
为了获得被删除的原蛇尾节点,snakeMove
函数需要返回原蛇尾节点,函数返回值从void
改为node
。函数中需要记录原蛇尾节点,并在最后返回原蛇尾节点。
- // 蛇身体移动
- node snakeMove(node* snake, int length, int direction)
- {
- // 记录尾节点
- node tail = snake[length - 1];
-
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else
- {
- newHead.x++;
- }
- snake[0] = newHead;
-
- // 返回尾节点
- return tail;
- }

在主函数的循环中,作新的蛇头节点是否与食物节点重合的判断。若蛇头节点与食物节点重合,那么蛇会长大一节。将snakeMove
函数返回的上一次尾结点添加到蛇尾后面,蛇节点长度加1。并且,食物被吃掉以后,应当重新生成新的食物,作为新的目标。
注意蛇不能无限生长而超过snake
数组的长度,当length
大于等于100时,吃到食物就不再增加长度了。
while (1)
{
cleardevice();
paintGrid();
paintSnake(snake, length);
paintFood(food);
Sleep(500);
changeDirection(&d);
node lastTail = snakeMove(snake, length, d);
// 新的蛇头节点是否与食物节点重合
if (snake[0].x == food.x && snake[0].y == food.y)
{
// 限制snake节点最大长度
if (length < 100)
{
// 已经吃到食物,长度+1
snake[length] = lastTail;
length++;
}
// 重新生成新的食物
food = createFood(snake, length);
}
}
现阶段代码:
- #include <easyx.h>
- #include <stdio.h>
- #include <conio.h>
- #include <time.h>
-
- #define NODE_WIDTH 40
-
- // 节点
- typedef struct {
- int x;
- int y;
- }node;
-
- // 绘制网格
- // 横线(0, y), (800, y) 0 <= y <= 600
- // 竖线(x, 0),(x, 600) 0 <= x <= 800
- void paintGrid()
- {
- // 横线
- for (int y = 0; y < 600; y += NODE_WIDTH)
- {
- line(0, y, 800, y);
- }
- // 竖线
- for (int x = 0; x < 800; x += NODE_WIDTH)
- {
- line(x, 0, x, 600);
- }
- }
-
- void paintSnake(node* snake, int n)
- {
- int left, top, right, bottom;
- for (int i = 0; i < n; i++)
- {
- // 左上角:【网格x坐标 * 网格宽度, 网格y坐标 * 网格宽度】
- left = snake[i].x * NODE_WIDTH;
- top = snake[i].y * NODE_WIDTH;
- // 右下角:【(网格x坐标 + 1) * 网格宽度, (网格y坐标 + 1) * 网格宽度】
- right = (snake[i].x + 1) * NODE_WIDTH;
- bottom = (snake[i].y + 1) * NODE_WIDTH;
- // 通过左上角与右下角坐标绘制矩形
- solidrectangle(left, top, right, bottom);
- }
- }
-
- // 方向枚举
- enum direction
- {
- eUp,
- eDown,
- eLeft,
- eRight
- };
-
-
- // 蛇身体移动
- node snakeMove(node* snake, int length, int direction)
- {
- //for (int i = 0; i < length; i++)
- // printf("(%d, %d)\n", snake[i].x, snake[i].y);
-
- // 记录尾节点
- node tail = snake[length - 1];
-
- // 从尾结点开始,前一个节点覆盖后一个节点
- // 0 1 2 3 4 0 1 2 3 4
- // E D C B A ---> E E D C B
- for (int i = length - 1; i > 0; i--)
- {
- snake[i] = snake[i - 1];
- }
-
- // 下一个头节点
- node newHead;
- newHead = snake[0];
- if (direction == eUp)
- {
- newHead.y--;
- }
- else if (direction == eDown)
- {
- newHead.y++;
- }
- else if (direction == eLeft)
- {
- newHead.x--;
- }
- else // right
- {
- newHead.x++;
- }
-
- // 更新头节点
- // E D C B A ---> F E D C B
- snake[0] = newHead;
-
- //for (int i = 0; i < length; i++)
- // printf("(%d, %d)\n", snake[i].x, snake[i].y);
-
- // 返回尾节点
- return tail;
- }
-
- // 键盘输入改变direction
- void changeDirection(enum direction* pD)
- {
- // 检查输入缓存区中是否有数据
- if (_kbhit() != 0)
- {
- // _getch函数获取输入缓存区中的数据
- char c = _getch();
- // 判断输入并转向
- switch (c)
- {
- case 'w':
- // 向上移动
- if (*pD != eDown)
- *pD = eUp;
- break;
- case 's':
- // 向下移动
- if (*pD != eUp)
- *pD = eDown;
- break;
- case 'a':
- // 向左移动
- if (*pD != eRight)
- *pD = eLeft;
- break;
- case 'd':
- // 向右移动
- if (*pD != eLeft)
- *pD = eRight;
- break;
- }
- }
- }
-
- // 绘制食物
- /*
-
- (x * NODE_WIDTH, y * NODE_WIDTH)
- @-----------
- | |
- | |
- | |
- | |
- | |
- -----------@ ((x + 1) * NODE_WIDTH, (y + 1) * NODE_WIDTH)
-
- */
- void paintFood(node food)
- {
- int left, top, right, bottom;
- left = food.x * NODE_WIDTH;
- top = food.y * NODE_WIDTH;
- right = (food.x + 1) * NODE_WIDTH;
- bottom = (food.y + 1) * NODE_WIDTH;
- setfillcolor(YELLOW);
- solidrectangle(left, top, right, bottom);
- setfillcolor(WHITE);
- }
-
-
- // 随机创建食物
- node createFood(node* snake, int length)
- {
- node food;
- while (1)
- {
- food.x = rand() % (800 / NODE_WIDTH);
- food.y = rand() % (600 / NODE_WIDTH);
-
- int i;
- for (i = 0; i < length; i++)
- {
- if (snake[i].x == food.x && snake[i].y == food.y)
- {
- break;
- }
- }
- if (i < length)
- continue;
- else
- break;
- }
- return food;
- }
-
- int main()
- {
- initgraph(800, 600);
- // 设置背景色
- setbkcolor(RGB(164, 225, 202));
- // 使用背景色清空窗体
- cleardevice();
-
- // 蛇节点坐标
- node snake[100] = { {5, 7}, {4, 7}, {3, 7}, {2, 7}, {1, 7} };
- // 蛇节点长度
- int length = 5;
- enum direction d = eRight;
-
- // 食物
- srand(unsigned int(time(NULL)));
- node food = createFood(snake, length);
-
- while (1)
- {
- // 清空整个窗体
- cleardevice();
- // 绘制网格
- paintGrid();
- // 绘制蛇节点
- paintSnake(snake, length);
- // 绘制食物
- paintFood(food);
- // 休眠500ms
- Sleep(500);
- // 获取键盘输入并将方向存储到变量d
- changeDirection(&d);
-
- node lastTail = snakeMove(snake, length, d);
- // 新的蛇头节点是否与食物节点重合
- if (snake[0].x == food.x && snake[0].y == food.y)
- {
- // 限制snake节点最大长度
- if (length < 100)
- {
- // 已经吃到食物, 长度+1
- snake[length] = lastTail;
- length++;
- }
- // 重新生成新的食物
- food = createFood(snake, length);
- }
- }
-
- getchar();
- closegraph();
- return 0;
- }

最后,我们需要判断游戏是否结束。游戏结束的条件为:
蛇头吃到墙壁
蛇头吃到蛇身
如果满足以上两个条件,则游戏结束,并复位所有设置,重新开始游戏。
游戏网格x坐标区间为[0, 800 / NODE_WIDTH)
,即[0, 20)
。
游戏网格y坐标区间为[0, 600 / NODE_WIDTH)
,即[0, 19)
。
蛇头snake[0].x
小于0或大于等于20,蛇头即吃到左边或右边的墙壁。
蛇头snake[0].y
小于0或大于等于15,蛇头即吃到上边或下边的墙壁。
遍历除了蛇头外的所有蛇节点坐标,若有节点坐标与蛇头坐标一致,则表明蛇头吃到了蛇身。
将上面两个结束条件封装成isGameOver
函数,若游戏结束则返回true
,否则返回false
。
- bool isGameOver(node *snake, int length)
- {
- // 是否撞墙
- if (snake[0].x < 0 || snake[0].x > 800 / NODE_WIDTH)
- return true;
-
- if (snake[0].y < 0 || snake[0].y > 600 / NODE_WIDTH)
- return true;
-
- // 是否吃到蛇身
- for (int i = 1; i < length; i++)
- {
- if (snake[0].x == snake[i].x && snake[0].y == snake[i].y)
- return true;
- }
- return false;
- }

游戏结束后,需要调用reset
函数,复位蛇节点坐标,蛇节点长度以及前进方向。
- void reset(node* snake, int *pLength, enum direction *d)
- {
- snake[0] = node{5, 7};
- snake[1] = node{ 4, 7 };
- snake[2] = node{ 3, 7 };
- snake[3] = node{ 2, 7 };
- snake[4] = node{ 1, 7 };
- *pLength = 5;
- *d = eRight;
- }
在主函数的循环中,添加游戏结束的判断。若游戏结束,复位各种设置。由于,蛇身坐标被复位,有可能与之前的食物坐标重合。因此,也应当重新生成食物。
完整源码请加群【881577770】获取!里面有一些资料可以帮助大家更好的学习,在学习C语言的过程中遇到任何的问题,都可以发出来一起讨论,大家都是学习C/C++的,或是转行,或是大学生,还有工作中想提升自己能力的前端党,如果你是正在学习C/C++的小伙伴可以加入学习。
- while (1)
- {
- cleardevice();
- paintGrid();
- paintSnake(snake, length);
- paintFood(food);
- Sleep(500);
- changeDirection(&d);
- node lastTail = snakeMove(snake, length, d);
- if (snake[0].x == food.x && snake[0].y == food.y)
- {
- if (length < 100)
- {
- snake[length] = lastTail;
- length++;
- }
- food = createFood(snake, length);
- }
-
- // 游戏是否结束
- if (isGameOver(snake, length) == true)
- {
- // 游戏结束,复位设置,重新生成食物
- reset(snake, &length, &d);
- food = createFood(snake, length);
- }
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。