这一节中,我们来做一款经典小游戏,贪吃蛇。先看看最终效果图

 

在开始之前,我们把窗体创建好。

创建一个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;
}

在主函数中放置一个死循环,循环体中依次执行以下步骤:

  1. 清空整个窗体

  2. 绘制网格

  3. 绘制蛇节点

  4. 休眠500ms

  5. 向右移动蛇节点

 

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;

食物的位置是随机生成的,但是有两个要求:

  1. 不能生成在窗体外

  2. 不能生成在蛇节点上

对于网格坐标来说,宽度有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.游戏结束

最后,我们需要判断游戏是否结束。游戏结束的条件为:

  1. 蛇头吃到墙壁

  2. 蛇头吃到蛇身

如果满足以上两个条件,则游戏结束,并复位所有设置,重新开始游戏。

游戏网格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);
    }
}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐