C语言C++图形库---贪吃蛇大作战【附源码】
这一节中,我们来做一款经典小游戏,贪吃蛇。先看看最终效果图
在开始之前,我们把窗体创建好。
创建一个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;
}
1. 定位网格
将整个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
将绘制网格的代码封装成函数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;
}
2. 绘制蛇节点
设定初始状态下,蛇有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;
}
3. 移动蛇节点
现在,我们希望让蛇向右运动,蛇头节点为(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;
}
4. 控制移动方向
现在,程序开始后蛇会向右移动,接下来,加入用键盘来控制蛇的移动方向的功能。
键盘交互功能如下:
-
按下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;
}
5. 创建食物
目前蛇的部分已经完成了,还差需要创建食物,让蛇吃到食物后长大。
主函数中声明一个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;
}
6. 吃掉食物并长大
观察下图,蛇头为节点(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;
}
7.游戏结束
最后,我们需要判断游戏是否结束。游戏结束的条件为:
-
蛇头吃到墙壁
-
蛇头吃到蛇身
如果满足以上两个条件,则游戏结束,并复位所有设置,重新开始游戏。
游戏网格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);
}
}
更多推荐
所有评论(0)