C++编写贪吃蛇小游戏快速入门

刚学完C++。一时兴起,就花几天时间手动做了个贪吃蛇,后来觉得不过瘾,于是又加入了AI功能。希望大家Enjoy It.

效果图示

AI模式演示

image
image

整体规划+原理

image

大体上可以分为图上所示的几个类。不过……怎么看都有点强行面向对象的味道在里面。。[哭笑][哭笑][哭笑]。不管了……代码写得可能有点凌乱,下面我会为大家一一讲解。

整个程序设计的原理就是:主函数死循环,不断刷新打印贪吃蛇和食物。这样每循环一次,就类似电影里面的一帧,最终显示的效果就是蛇会动起来。

01 初始化工作-游戏设置

游戏设置和相关初始化放在了一个类里面,并进行了静态声明。主要设置了游戏窗口的长和款。并在GameInit()函数里面设置了窗口大小,隐藏光标,初始化随机数种子等。代码如下:

  1. 1//游戏设置相关模块,把函数都放到一个类里面了。函数定义为static静态成员,不生成实体也可以直接调用
    2class GameSetting
    3{
    4public:
    5    //游戏窗口的长宽
    6    static const int window_height = 40;
    7    static const int window_width = 80;
    8public:
    9    static void GameInit()
    10    {
    11        //设置游戏窗口大小
    12        char buffer[32];
    13        sprintf_s(buffer, "mode con cols=%d lines=%d",window_width, window_height);
    14        system(buffer);
    15
    16        //隐藏光标
    17        HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    18        CONSOLE_CURSOR_INFO CursorInfo;
    19        GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
    20        CursorInfo.bVisible = false; //隐藏控制台光标
    21        SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
    22        //初始化随机数种子
    23        srand((unsigned int)time(0));
    24    }
    25};

用到了几个相关的Windows API,本文不做过多介绍,大家百度即可。

02 打印信息类

该类主要是用来打印一些游戏相关信息的。该类大体如下:

image

下面挑几个重点的来讲:

2.1 画地图边界

这个函数主要是根据上面所给的游戏窗口长宽来打印地图边界的。其中还划分了几个区域,主要用来放不同的信息的。

  1. 1//画地图边界
    2static void DrawMap()
    3{
    4    system("cls");
    5    int i, j;
    6    for (= 0; i < GameSetting::window_width; i++)
    7        cout << "#";
    8    cout << endl;
    9    for (= 0; i < GameSetting::window_height-2; i++)
    10    {
    11        for (= 0; j < GameSetting::window_width; j++)
    12        {
    13            if (== 13 && j >= GameSetting::window_width - 29)
    14            {
    15                cout << "#";
    16                continue;
    17            }
    18
    19            if (== 0 || j == GameSetting::window_width - 29 || j == GameSetting::window_width-1)
    20            {
    21                cout << "#";
    22            }
    23            else
    24                cout << " ";
    25
    26        }
    27        cout << endl;
    28    }
    29    for (= 0; i < GameSetting::window_width; i++)
    30        cout << "#";
    31
    32}

划分区域如下图,#就是边框了:

image

2.2 画出分数和模式

该函数主要是在右上角画出成绩和游戏模式的,在绘制之前会进行刷新处理。先清除,再重新打印。用到了一个gotoxy()函数。这个函数主要是移动光标到(x, y)坐标处的。关于(x, y)的位置,根据实际情况调整即可。

  1. 1//画分数
    2static void DrawScore(int score)
    3{
    4    gotoxy(GameSetting::window_width - 22+14, 6);
    5    cout << "  ";
    6    gotoxy(GameSetting::window_width - 22+14, 4);
    7    cout << "  ";
    8
    9    gotoxy(GameSetting::window_width - 22, 6);
    10    cout << "当前玩家分数: " << score << endl;
    11    gotoxy(GameSetting::window_width - 22, 4);
    12    cout << "当前游戏速度: " << 10 - speed / 25 << endl;
    13
    14}

03 食物类

食物类定义了食物的坐标,随机生成规则,和画出食物等一系列操作。其中食物坐标我们用了一个结构体:

  1. 1typedef struct
    2{
    3    int x;
    4    int y;
    5}COORDINATE;

该结构体两个成员,分别保存坐标的(x, y)。蛇身的坐标也会用到这个结构体。
有关食物类的大体如下:

image

下面我们还是挑几个重点来讲。

3.1 随机生成食物

随机生成食物,原则上不允许食物出现在蛇身的位置上,如果有。我们重新生成。注意地图的范围,就是区域左边一块。实际情况根据自身的地图范围来调整食物坐标的范围,注意不要越界。用rand()函数获得随机坐标。代码如下:

  1. 1void RandomXY(vector<COORDINATE> & coord)
    2{
    3    m_coordinate.= rand() % (GameSetting::window_width - 30) + 1;
    4    m_coordinate.= rand() % (GameSetting::window_height - 2) + 1;
    5    unsigned int i;
    6    //原则上不允许食物出现在蛇的位置上,如果有,重新生成
    7    for (= 0; i < coord.size(); i++)
    8    {
    9        //食物出现在蛇身的位置上。重新生成
    10        if (coord[i].== m_coordinate.&& coord[i].== m_coordinate.y)
    11        {
    12            m_coordinate.= rand() % (GameSetting::window_width - 30) + 1;
    13            m_coordinate.= rand() % (GameSetting::window_height - 2) + 1;
    14            i = 0;
    15        }
    16    }
    17}

然后,在构造函数里面传入蛇身的坐标。即可生成食物。

3.2 画出食物

画出食物比较简单了,gotoxy到随机生成的坐标之后,cout就行。我们在这还设置了一个食物颜色为红色。代码如下:

  1. 1void DrawFood()
    2{
    3    setColor(12, 0);
    4    gotoxy(m_coordinate.x, m_coordinate.y);
    5    cout << "@";
    6    setColor(7, 0);
    7}

04 贪吃蛇类

定义贪吃蛇的移动,打印,吃食物等等。这节课我们暂时不讨论AI功能,先把手动操作的贪吃蛇做了跑起来,下节课再做AI功能的介绍。该类大体如下:

image

4.1 成员变量

成员变量m_direction记录每次移动的方向。m_is_alive记录贪吃蛇是否还活着。m_coordinate则是贪吃蛇身体坐标的记录。贪吃蛇是一节一节的,整条蛇必然是由许多节组成的。因此用了一个vector来存储蛇身,每节类型是COORDINATE结构体的。

4.2 默认构造函数

默认构造函数Snake()里面主要是做了初始贪吃蛇的生成,以及移动方向的定义等。初始的蛇为3节。在中间位置,向上移动。代码如下:

  1. 1        //移动方向向上
    2    m_direction = 1;
    3    m_is_alive = true;
    4    COORDINATE snake_head;
    5    //蛇头生成位置
    6    snake_head.= GameSetting::window_width / 2 - 15;
    7    snake_head.= GameSetting::window_height / 2;
    8
    9    this->m_coordinate.push_back(snake_head);
    10    snake_head.y++;
    11    this->m_coordinate.push_back(snake_head);
    12    snake_head.y++;
    13    this->m_coordinate.push_back(snake_head); //初始蛇身长度三节

4.3 监听键盘

监听键盘用了C里面的一个库函数。_kbhit()非阻塞函数,可以不断监听键盘的情况从而不产生阻塞。有键盘按下的时候,就获取按下的键盘是哪个。然后做出相应的变化,其实是方向的调整。需要注意的是,当我们的蛇往上走的时候,按下方向的键,我们是不做处理的。其它方向一样。还有一个调整游戏速度的,speed是休眠时间,speed越小,速度越快。反之速度越慢。

  1. 1    //监听键盘
    2void listen_key_borad()
    3{
    4    char ch;
    5
    6    if (_kbhit())                    //kbhit 非阻塞函数 
    7    {
    8        ch = _getch();    //使用 getch 函数获取键盘输入 
    9        switch (ch)
    10        {
    11        case 'w':
    12        case 'W':
    13            if (this->m_direction == DOWN)
    14                break;
    15            this->m_direction = UP;
    16            break;
    17        case 's':
    18        case 'S':
    19            if (this->m_direction == UP)
    20                break;
    21            this->m_direction = DOWN;
    22            break;
    23        case 'a':
    24        case 'A':
    25            if (this->m_direction == RIGHT)
    26                break;
    27            this->m_direction = LEFT;
    28            break;
    29        case 'd':
    30        case 'D':
    31            if (this->m_direction == LEFT)
    32                break;
    33            this->m_direction = RIGHT;
    34            break;
    35        case '+':
    36            if (speed >= 25)
    37            {
    38                speed -= 25;
    39            }
    40            break;
    41        case '-':
    42            if (speed < 250)
    43            {
    44                speed += 25;
    45            }
    46            break;
    47        }
    48    }
    49}

4.4 移动贪吃蛇

移动贪吃蛇,我们用了一个方向变量,在监听键盘的时候获取移动的方向,然后在根据方向移动贪吃蛇的蛇头。这里的移动我们是这样处理的,首先,贪吃蛇每移动一次,需要改变的只有蛇头和蛇尾两节。我们只需要把新的蛇头插进去,最后再画出来就可以了。至于蛇尾,如果我们不删除蛇尾的话,蛇会不断变长的。因此我们的做法是:吃到食物的时候插入蛇头而不删除蛇尾,没有吃到食物的时候插入蛇头同时删除蛇尾。这样就完美搞定了。

  1. 1    //移动贪吃蛇
    2void move_snake()
    3{
    4    //监听键盘
    5    listen_key_borad();
    6    //蛇头
    7    COORDINATE head = m_coordinate[0];
    8    //direction方向:1 上  2 下  3 左  4 右
    9    switch (this->m_direction)
    10    {
    11    case UP:
    12        head.y--;
    13        break;
    14    case DOWN:
    15        head.y++;
    16        break;
    17    case LEFT:
    18        head.x--;
    19        break;
    20    case RIGHT:
    21        head.x++;
    22        break;
    23    }
    24    //插入移动后新的蛇头。是否删除蛇尾,在后续吃到食物判断那里做
    25    m_coordinate.insert(m_coordinate.begin(), head);
    26}

4.5 是否吃到食物

判断是否吃到食物,就是看看蛇头的坐标等不等于食物的坐标。如果等于,就重新生成食物,不删除蛇尾,蛇变长一节。不等于,就删除蛇尾,蛇长不变。

  1. 1bool is_eat_food(Food & f)
    2{
    3    //获取食物坐标
    4    COORDINATE food_coordinate = f.GetFoodCoordinate();
    5    //吃到食物,食物重新生成,不删除蛇尾
    6    if (m_coordinate[HEAD].== food_coordinate.&& m_coordinate[HEAD].== food_coordinate.y)
    7    {
    8        f.RandomXY(m_coordinate);
    9        return true;
    10    }
    11    else
    12    {
    13        //没有吃到食物,删除蛇尾
    14        m_coordinate.erase(m_coordinate.end() - 1);
    15        return false;
    16    }
    17}

4.6判断蛇是否还存活

判断蛇是否GG,主要是看是否超出边界,是否碰到自己身体其他部分。

  1. 1//判断贪吃蛇死了没
    2bool snake_is_alive()
    3{
    4    if (m_coordinate[HEAD].<= 0 ||
    5        m_coordinate[HEAD].>= GameSetting::window_width - 29 ||
    6        m_coordinate[HEAD].<= 0 ||
    7        m_coordinate[HEAD].>= GameSetting::window_height - 1)
    8    {
    9        //超出边界
    10        m_is_alive = false;
    11        return m_is_alive;
    12    }
    13    //和自己碰到一起
    14    for (unsigned int i = 1; i < m_coordinate.size(); i++)
    15    {
    16        if (m_coordinate[i].== m_coordinate[HEAD].&& m_coordinate[i].== m_coordinate[HEAD].y)
    17        {
    18            m_is_alive = false;
    19            return m_is_alive;
    20        }
    21    }
    22    m_is_alive = true;
    23
    24    return m_is_alive;
    25}

4.7 画出贪吃蛇

画出贪吃蛇比较简单,gotoxy到身体的每一节,然后cout就行。在此之前设置了颜色为浅绿色。

  1. 1//画出贪吃蛇
    2void draw_snake()
    3{
    4    //设置颜色为浅绿色
    5    setColor(10, 0);
    6    for (unsigned int i = 0; i < this->m_coordinate.size(); i++)
    7    {
    8        gotoxy(m_coordinate[i].x, m_coordinate[i].y);
    9        cout << "*";
    10    }
    11    //恢复原来的颜色
    12    setColor(7, 0);
    13}

4.8 清除屏幕上的贪吃蛇

我们是死循环不断刷新打印贪吃蛇的,因此每移动一次,必然会在屏幕上留下上一次贪吃蛇的痕迹。因此我们每次在画蛇之前,不是添足,而是清理一下上次遗留的蛇身。我们知道,蛇每次移动,变的只有蛇头和蛇尾,因此该函数我们只需要清理蛇尾就行。gotoxy到蛇尾的坐标,cout<<" ";就行。

  1. 1gotoxy(m_coordinate[this->m_coordinate.size()-1].x, m_coordinate[this->m_coordinate.size() - 1].y);
    2cout << " ";

05 主函数,组装我们的游戏

我们的游戏在主函数里面进行组装。然后开始运行。
首先我们做游戏相关的初始化和模式选择。

  1. 1GameSetting setting;
    2PrintInfo print_info;
    3Snake  snake;
    4//初始化游戏
    5setting.GameInit();
    6//游戏模式选择
    7print_info.DrawChoiceInfo();
    8
    9char ch = _getch();
    10switch (ch)
    11{
    12case '1':
    13    snake.set_model(true);
    14    speed = 50;
    15    break;
    16case '2':
    17    snake.set_model(false);
    18    break;
    19default:
    20    gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
    21    cout << "输入错误,Bye!" << endl;
    22    cin.get();
    23    cin.get();
    24    return 0;
    25}
    26gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
    27system("pause");

然后就是画地图边框,打印游戏相关信息和说明。生成食物了。

  1. 1//画地图
    2print_info.DrawMap();
    3print_info.DrawGameInfo(snake.GetModel());
    4//生成食物
    5Food food(snake.m_coordinate);

最后就是游戏死循环,在死循环里面,我们需要不断移动蛇,画蛇,判断蛇的状态,判断食物的状态,是否吃到食物等等。具体代码:

  1. 1//游戏死循环
    2while (true)
    3{
    4    //打印成绩
    5    print_info.DrawScore(snake.GetSnakeSize());
    6    //画出食物
    7    food.DrawFood();
    8    //清理蛇尾,每次画蛇前必做
    9    snake.ClearSnake();
    10    //判断是否吃到食物
    11    snake.is_eat_food(food);
    12    //根据用户模式选择不同的调度方式
    13    if (snake.GetModel() == true)
    14    {
    15        snake.move_snake();
    16    }
    17    else
    18    {
    19        snake.AI_find_path(food);
    20        snake.AI_move_snake();
    21    }
    22    //画蛇
    23    snake.draw_snake();
    24    //判断蛇是否还活着
    25    if (!snake.snake_is_alive())
    26    {
    27        print_info.GameOver(snake.GetSnakeSize());
    28        break;
    29    }
    30    //控制游戏速度
    31    Sleep(speed);
    32}

最终,我们的代码就可以Run起来了。具体效果放在开头了。界面算不上好看,但是整个程序向大家展示了最基本最核心的功能和代码,大家可以在这个基础上开发自己喜欢的各种美丽的界面哦。

06 AI部分和完善

代码是画了几天间间断断写出来的,水平不算很高,代码也写得乱七八糟的。不过代码会在后期不断优化,尽量做到精简优美。至于AI功能,等下一篇博文写吧。

代码获取

欲获取代码,请关注我们的微信公众号【程序猿声】,在后台回复:aisnake。即可下载。

微信公众号

推荐文章:10分钟教你用Python做个打飞机小游戏超详细教程

推荐文章:10分钟教你用python下载和拼接微信好友头像图片

推荐文章:10分钟教你用python一行代码搞点大新闻

推荐文章:10分钟教你用python打造贪吃蛇超详细教程

【C/C++】10分钟教你用C++写一个贪吃蛇附带AI功能(附源代码详解和下载)的更多相关文章

  1. 【数据结构】10分钟教你用栈求解迷宫老鼠问题超详细教程附C++源代码

    问题描述 给定一张迷宫地图和一个迷宫入口,然后进入迷宫探索找到一个出口.如下图所示: 该图是一个矩形区域,有一个入口和出口.迷宫内部包含不能穿越的墙壁或者障碍物.这些障碍物沿着行和列放置,与迷宫的边界 ...

  2. 10分钟 教你学会Linux/Unix下的vi文本编辑器

    10分钟 教你学会Linux/Unix下的vi文本编辑器 vi编辑器是Unix/Linux系统管理员必须学会使用的编辑器.看了不少关于vi的资料,终于得到这个总结.不敢独享,和你们共享. 首先,记住v ...

  3. 【python】10分钟教你用python打造贪吃蛇超详细教程

    10分钟教你用python打造贪吃蛇超详细教程 在家闲着没妹子约, 刚好最近又学了一下python,听说pygame挺好玩的.今天就在家研究一下, 弄了个贪吃蛇出来.希望大家喜欢. 先看程序效果: 0 ...

  4. 10分钟教你用Python打造天气机器人+关键字自动回复+定时发送

    01 前言 Hello,各位小伙伴.自上次我们介绍了Python实现天气预报的功能以后,那个小程序还有诸多不完善的地方,今天,我们再次来完善一下我们的小程序.比如我们想给机器人发“天气”等关键字,它就 ...

  5. 10分钟教你用Python打造微信天气预报机器人

    01 前言 最近武汉的天气越来越恶劣了.动不动就下雨,所以,拥有一款好的天气预报工具,对于我们大学生来说,还真是挺重要的了.好了,自己动手,丰衣足食,我们来用Python打造一个天气预报的微信机器人吧 ...

  6. 10分钟教你用Python玩转微信之好友性别比例统计分析

    01 前言+效果展示 想必,微信对于大家来说,是再熟悉不过的了.那么,大家想不想探索一下微信上的各种奥秘呢?今天,我们一起来简单分析一下微信上的好友性别比例吧~废话不多说,开始干活. 结果如下: 02 ...

  7. 10分钟教你用Python玩转微信之抓取好友个性签名制作词云

    01 前言+展示 各位小伙伴我又来啦.今天带大家玩点好玩的东西,用Python抓取我们的微信好友个性签名,然后制作词云.怎样,有趣吧~好了,下面开始干活.我知道你们还是想先看看效果的. 后台登录: 词 ...

  8. 10分钟教你用VS2017将代码上传到GitHub

    前言 关于微软的Visual Studio系列,真可谓是宇宙最强IDE了.不过,像小编这样的菜鸟级别也用不到几个功能.今天给大家介绍一个比较实用的功能吧,把Visual Studio 2017里面写好 ...

  9. 【python】10分钟教你用python如何正确把妹

    前言 今天没妹子约,刚好研究一下.如何用神奇的python打造一个把妹神器吧.看完这个,你们就能走向人生巅峰,迎娶白富美啦. 我知道你们想看看效果 image 当然啦,这只是测试版的效果,真正的版本可 ...

随机推荐

  1. ArcGIS js api三种查询功能

    转自https://blog.csdn.net/lovecarpenter/article/details/52669777

  2. IDEA错误的忽略了智能补全代码,导致正确的代码自动提示不出来的问题

    标题说起来有点绕,当今大部分IDE都提供 Alt+Enter 呼出自动补全菜单的功能,IDEA也不例外,今天手残了一下,具体问题如下: 1. 通常我们键入一个自定义类时IDEA会自动提示为红色,表示缺 ...

  3. C++ std::vector<bool>

    std::vector template < class T, class Alloc = allocator<T> > class vector; // generic te ...

  4. linux zip解压缩中文乱码

    这里提供两个解决方案: 1.python处理下:https://gist.github.com/wangjiezhe/7841a350983a147b6d7e 2.java的zip4j:http:// ...

  5. [C++] * Basic and Class

    C++ 目  录 1 开始学习C++ 4 1.1 C++的头文件 4 1.2 命名空间 4 1.3 更严格的类型转化 4 1.4 new和delete 4 1.5 内联函数 4 1.6 引用 5 1. ...

  6. TCP、UDP、HTTP、SOCKET之间的区别与联系-乾颐堂CCIE

    IP:网络层协议: TCP和UDP:传输层协议: HTTP:应用层协议: SOCKET:TCP/IP网络的API. TCP/IP代表传输控制协议/网际协议,指的是一系列协议. TCP和UDP使用IP协 ...

  7. C++获取字符串长度数

    strlen,获取到的是字节数,中文占两个字节. 如何获取字符数,无论中文英文,标点符号,都按一个字符计算呢?这里提供其中的一个方法.那就是通过MultiByteToWideChar函数,将CStri ...

  8. 基于CacheManager组件的缓存产品配置

    一.Couchbase 使用CacheManager组件,在配置Couchbase缓存支持时,由于对配置节cache handle命名规则要求不了解,费了点时间查了源码才明白. section配置节 ...

  9. 彻底修改Eclipse的默认编码

    引用各位前辈经验得到彻底修改eclipse默认编码的方法. 单在eclipse里设置编码方式非常复杂且容易遗漏,全部修改后,有些代码生成模板内的${encode}变量仍为原编码方案,经过查阅许多资料得 ...

  10. python学习之内部执行流程,内部执行流程,编码(一)

    python的执行流程: 加载内存--->词法分析--->语法分析--->编译--->转换字节码---->转换成机器码---->供给CPU调度 python的编码: ...