贪吃蛇

代码解析 

 main() 函数

int main()
{
    InitSnake();
    while (1)
    {
        while (!kbhit())        //如果没有接收键盘
        {
            if (!EatFood())     //如果食物被吃掉,就画出新的食物来
            {
                CoorFood();
                DrawFood();
            }
            Break();            //先检查再移动
            MoveSnake();
            IntervalTime = Level() * 60;
            Sleep(IntervalTime);
        }
        GetKey();
    }
    getchar();  //卡屏
    return 0;
}

游戏从 InitSnake() 开始,初始化画板,加载封面图片以及音乐。初始化蛇也就是只有一个结点的蛇最开始的样子,同时初始化一个食物结点。kbhit() 函数用于接收键盘,当没有键盘信息传来时(键盘信息包括改变蛇的移动方向以及游戏暂停),蛇一直移动。移动之前先 Break() 函数判断是否撞到自己或者撞到墙,如果撞到就 GameOver(),没有撞到就执行移动操作。蛇两次移动之间间隔 IntervalTime 时间,Level() 函数提供游戏等级,蛇的长度越大,Level() 的值越低,移动的间隔时间越短,也就是蛇的移动速度越来越快,游戏难度越来越大。移动之前还要EatFood() 函数判断食物有没被吃掉,如果被吃掉了,就要重新计算新食物的生成坐标,并画出新的食物结点。

 InitSnake() 函数

void InitSnake()
{
    initgraph(640, 480, NOMINIMIZE);  //初始化绘图环境,创建画板

    //加载图片
    loadimage(NULL, L"E:\\贪吃蛇\\贪吃蛇\\pic.jpg", 640, 480);
    Sleep(1000);

    //加载音乐
    mciSendString(L"open 谓风.mp3  alias bk", 0, 0, 0);
    mciSendString(L"play bk repeat", 0, 0, 0);

    setbkcolor(RGB(220, 20, 150));   //用 RGB 宏合成颜色
    cleardevice();    //用背景色清空屏幕,并将当前点移至 (0, 0)

    //初始化一条小蛇
    setfillcolor(CYAN);  //设置填充颜色,蛇头是青色
    solidrectangle(0, 0, SNAKE_SIZE, SNAKE_SIZE);
    snake.szb[0].x = 0;
    snake.szb[0].y = 0;
    snake.n = 1;
    snake.c = Right;

    //初始化食物
    food.flag = true;
}

initgaph() 函数用于创建画板,函数原型

HWND initgraph(int width,int height,int flag = NULL);

指定画板的长宽以及绘图环境的样式,flag 的值可以为NOCLOSE(禁用画板的关闭按钮),NOMINIMIZE(禁用画板最小化按钮),SHOWCONSOLE(同时还显示控制台窗口)。函数返回创建的绘图窗口的句柄。

loadimage() 函数加载游戏封面,NULL表示图片将读取至绘图窗口。路径用字符串表示,用 \\ 可能是防止 \ 将后面的字符转义掉,长字符串前面加上 L 。后两个参数表示拉伸图片,这里将图片铺满整个绘图窗口。

休眠 1000 毫秒后加载音乐,填充画板,游戏开始。

加载音乐需要添加 #include 多媒体设备接口头文件,并添加 #pragma comment(lib,"winmm.lib") 库文件,音乐文件的操作指令 open 打开,play 播放,pause 暂停,resume 恢复,close 关闭。打开的时候 alias 给 谓风.mp3 起个别名叫 bk。mciSendString() 函数的后面三个参数都设置为0.。

用 RGB() 宏合成背景颜色,cleardevice() 函数用背景色填充画板。游戏开始了。

初始化只有一个结点的小蛇,蛇头是 CYAN ,后面的蛇身是 BLUE,初始化蛇结点的形状是填充矩形,蛇头的坐标是 (0,0),初始化蛇的长度,方向,以及食物最开始没有被吃掉。

画圆函数 circle() 的三个参数是圆心的 x,y坐标以及半径,矩形函数 rectangle() 函数的四个参数是左上角顶点的 x,y 坐标以及右下角顶点的 x,y 坐标。蛇的结点是矩形,保存结点坐标,在这里就是保存矩形左上角顶点的坐标。

结构体声明

enum DIRECT {
    up = 72,
    down = 80,
    Left = 75,   //使用 std 之后,要对left ,right 重命名
    Right = 77,
    space = 32
};


struct COOR {
    int x;
    int y;
} coor;



struct SNAKE {
    COOR szb[SNAKE_MAX_LENGTH];  //每一个蛇的结点的坐标
    int n;                       //蛇的当前长度
    DIRECT c;                    //蛇的方向
} snake;


struct FOOD {
    COOR fzb;   //食物坐标
    bool flag;  //true 表示食物没有被吃掉, false 表示食物被吃掉
} food;

DIRECT 枚举类型是为了将接收到的键盘信息(接收到的其实是 ASCII 码)转换成容易理解的符号常量。注意到后面进行文件处理的时候,要引入 std 名称空间,引入之后 left 和 right 在 cout<< 的时候,分别表示左对齐和右对齐,有了特殊的含义,所以这里要给枚举常量重新起个名字。

COOR 结构体保存结点坐标,也就是矩形左上方顶点的坐标。

SNAKE 结构体保存蛇的结点信息,这里定义了两个全局常量。

#define SNAKE_MAX_LENGTH  100     //蛇的最大长度
#define SNAKE_SIZE  10            //蛇的大小(宽度)

FOOD 结构体保存食物结点的相关信息。

Break() 函数

在蛇移动之前,先判断一下蛇会不会撞到墙或者撞到自己。撞到墙意味着蛇头坐标超出了画板的边界。因为蛇头坐标表示的是左上角顶点的坐标,所以比较的参数是 630 和 470 ,而不是 640 和 480。撞到自己的话,意思就是蛇头结点和某一个身体结点重合了。其实这个关系作为判断条件并不准确,两个矩形只要有边碰到就是撞上了,这时候两个矩形还没有完全重合呢!

void Break()
{
    //撞到墙
    if (snake.szb[0].x < 0 || snake.szb[0].x > 630 || snake.szb[0].y < 0 ||
            snake.szb[0].y > 470)
    {
        GameOver();
    }
    //撞到自己
    //因为某个 bug ,这里必须要从 2 开始
    for (int i = 2; i < snake.n; i++)
    {
        if (snake.szb[0].x == snake.szb[i].x && snake.szb[0].y == snake.szb[i].y)
        {
            GameOver();
        }
    }
}

MoveSnake() 函数

void MoveSnake()
{
    //蛇的后一个结点保存前一个结点的坐标,DrawSnake()之后就实现了每个结点坐标的更新
    //同时,更新之前的最后一个结点还保留在,所以更新后会多出一个结点,要把它删去
    for (int i = snake.n; i > 0; i--)
        //这里 i=snake.n 初始化,就会只有一个结点
    {
        snake.szb[i].x = snake.szb[i - 1].x;
        snake.szb[i].y = snake.szb[i - 1].y;
    }
    //对蛇的方向进行判断
    switch (snake.c)
    {
    case up:
        snake.szb[0].y -= SNAKE_SIZE;
        break;
    case down:
        snake.szb[0].y += SNAKE_SIZE;
        break;
    case Left:
        snake.szb[0].x -= SNAKE_SIZE;
        break;
    case Right:
        snake.szb[0].x += SNAKE_SIZE;
        break;
    }
    DrawSnake();  //移动完之后重新画蛇
}

For() 循环里面的 i 的初始条件,我还没有完全搞明白。

DrawSnake() 函数

void DrawSnake()
{
    for (int i = snake.n - 1; i > 0; i--)
    {
        setfillcolor(BLUE);   //这里是画蛇的身体,蓝色
        solidrectangle(snake.szb[i].x, snake.szb[i].y, snake.szb[i].x + SNAKE_SIZE,
                       snake.szb[i].y + SNAKE_SIZE);
    }
    //蛇头颜色不一样,单独拿出来画
    setfillcolor(CYAN);
    solidrectangle(snake.szb[0].x, snake.szb[0].y, snake.szb[0].x + SNAKE_SIZE,
                   snake.szb[0].y + SNAKE_SIZE);
    //隐藏尾巴
    setfillcolor(RGB(220, 20, 150));   //设置当前绘图前景色
    //用和当前背景色相同的绘图前景色来绘制多出的结点,就把它隐藏了
    solidrectangle(snake.szb[snake.n].x, snake.szb[snake.n].y,
                   snake.szb[snake.n].x + SNAKE_SIZE, snake.szb[snake.n].y + SNAKE_SIZE);
    //setcolor(WHITE);  //隐藏最后一个结点之后,别忘了恢复绘图色
}

蛇移动完之后,在新的每个蛇结点位置画上矩形,同时把多出来的那个尾巴结点删除。

食物函数

//画食物
void DrawFood()
{
    setfillcolor(GREEN);  //食物是绿色填充
    solidroundrect(food.fzb.x, food.fzb.y, food.fzb.x + SNAKE_SIZE,
                   food.fzb.y + SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE);
    food.flag = true; //新画的食物还没有被吃掉
}

//计算食物坐标
void CoorFood()
{
    srand(unsigned(time(NULL)));
    //这种求余方法是为了使食物位于 10 的整数倍的坐标上
    food.fzb.x = rand() % (640 / SNAKE_SIZE) * SNAKE_SIZE;
    food.fzb.y = rand() % (480 / SNAKE_SIZE) * SNAKE_SIZE;
}

当 EatFood() 函数返回食物被吃掉了,就先后调用 CoorFood() 函数和 DrawFood() 函数画出新的食物。注意,这里的求余方法非常巧妙。

EatFood() 函数

//吃食物函数,返回食物有没有被吃掉
bool EatFood()
{
    if (snake.szb[0].x == food.fzb.x && snake.szb[0].y == food.fzb.y)
    {
        snake.n++;
        //将食物结点删掉
        setfillcolor(RGB(220, 20, 150));
        solidroundrect(food.fzb.x, food.fzb.y, food.fzb.x + SNAKE_SIZE,
                       food.fzb.y + SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE);

        food.flag = false; //食物已经被吃掉
    }
    return food.flag;
}

吃掉食物,意思就是蛇头结点和食物结点重合了。

GetKey() 函数

//接收键盘
void GetKey()
{
    int key;
    key = getch();
    switch (key)
    {
    case up:   //这里应该是在枚举里面转换过了
        if (snake.c != down)
        {
            snake.c = up;
        }
        break;
    case down:
        if (snake.c != up)
        {
            snake.c = down;
        }
        break;
    case Left:
        if (snake.c != Right)
        {
            snake.c = Left;
        }
        break;
    case Right:
        if (snake.c != Left)
        {
            snake.c = Right;
        }
        break;
    //实现游戏暂停功能,按空格键继续游戏
    case space:
        do
        {
            mciSendString(L"pause bk", 0, 0, 0); //暂停音乐
            key = getch();
        } while (key != space);
        mciSendString(L"resume bk", 0, 0, 0);    //再次按下空格键恢复音乐
        break;
    }
}

这里注意 getchar() 和 getch() 的区别。getchar() 按下按键后,要按一下回车(Enter)才能有效果,而 getch() 是即时产生响应的。转换方向的时候,有个前提条件,就是蛇的当前移动方向不能和按下的方向键相反。按下 space 游戏暂停,一直在执行 do 里面的语句,即暂停音乐和接收按键。当再次按下 space 时,while() 里面条件成假,跳出循环,再次播放音乐,游戏重新开始。这里注意到 do  while() 结构是如果为真则持续执行,如果为假则跳出循环。

Level() 函数

//游戏难度等级,表现为蛇的移动速度越来越快
int Level()
{
    int level;
    if (snake.n < 15)
    {
        level = 5;
    }
    else if (snake.n < 30)
    {
        level = 4;
    }
    else if (snake.n < 40)
    {
        level = 3;
    }
    else if (snake.n < 50)
    {
        level = 2;
    }
    else
    {
        level = 1;
    }
    return level;
}

这个函数就是根据蛇的长度提供不同的难度等级。

GameOver() 函数

//游戏结束
void GameOver()
{
    cleardevice();

    //先获取当前历史最高分
    ifstream fin("历史最高分.txt");
    int tmp;
    fin >> tmp;

    //输出游戏结束的画面
    setcolor(YELLOW);
    settextstyle(50, 20, L"宋体");
    outtextxy(100, 100, L"亲爱的,游戏结束啦!");
    outtextxy(100, 200, L"您的得分是:");
    //输出数字的话,要先将数字格式化成字符串
    TCHAR s[5];
    _stprintf(s, _T("%d"), snake.n);
    outtextxy(340, 200, s);
    outtextxy(400, 200, L"分!");

    if (snake.n > tmp) //超过了最高分
    {
        outtextxy(100, 300, L"恭喜超过了当前最高分!");
	fstream file("历史最高分.txt", ios::out);  //清空文件内容
        ofstream fout("历史最高分.txt");
        fout << snake.n;
    }
    else   //没有超过最高分
    {
        outtextxy(100, 300, L"历史最高分是:");
        TCHAR t[5];
	_stprintf(t, _T("%d"), tmp);
	outtextxy(400, 300, t);
	outtextxy(460, 300, L"分!");
    }

    //关闭音乐
    mciSendString(L"close bk", 0, 0, 0);

    //手动关闭游戏窗口
    getchar();
    closegraph();
}

这个函数实现了历史最高分功能,该功能依赖于文件实现。引入 istream 和 ostream 这两个头文件,使用 ifstream 和 ofstream 这两个类创建 fin 和 fout 这两个对象。tmp 读取文件中的历史最高分,与结束时的蛇的长度比较。

settextstyle() 函数的三个参数是文字的高度,宽度,以及字体。

settextxy() 函数的三个参数是第一个字符的 x , y 坐标以及待输出的字符串。如果输出变量数字的话,要先格式化成字符串输出。

如果超过了最高分,就把文件数据清楚,把新的数据写进去。没有超过,就输出文件中的历史最高分。

最后,关闭音乐。

 

编码过程中出现过的错误

文件输入输出操作的头文件

std 对于 left 和 right 的影响

加载图片时路径中的  \\

GetKey() 函数里, switch 里面, case 里面的 if ,判断条件写错了

撞到墙时的判断条件写反了

 

目前仍不理解的地方

移动函数里面 for()里面的 i 的初始值

为什么开始会有两个结点,撞到自己的 for() 循环初始值要从 2 开始

 

未来改进的方向

加上重新开始的功能,这样好像要重新调用 main 函数? 可以将 main() 里面的内容抽出来放到一个 gaming() 函数里,main() 调用这个 gaming(),如果游戏结束, gaming() 就返回给 main() 一个标志,main() 接收到这个意思为游戏结束的标志,就会再次调用 gaming(),游戏重新开始。

将面向过程改成面向对象。

更换添加音乐的方式。在 VS2012 外面,点击 exe 文件运行,音乐无法播放。

用 EasyX 做出来的窗口,不能在游戏中任意缩放窗口大小。

可以将代码中的窗口长宽这两个常量抽出来定义成全局常量。

加上一个写有游戏中当前得分的小窗口。

写一个玩家 id 登陆游戏功能,保存不同玩家的历史最高分,可以进行排序。

利用 Qt 做GUI ,做一些对话框之类的。

 

 

你可能感兴趣的:(贪吃蛇)