10分钟教你用C++写一个贪吃蛇附带AI功能(史上最详细最入门的贪吃蛇教程)附源代码下载

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

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

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

效果图示

AI模式演示

image
image

整体规划+原理

image

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

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

01 初始化工作-游戏设置

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

//游戏设置相关模块,把函数都放到一个类里面了。函数定义为static静态成员,不生成实体也可以直接调用
class GameSetting
{
public:
    //游戏窗口的长宽
    static const int window_height = 40;
    static const int window_width = 80;
public:
    static void GameInit()
    {
        //设置游戏窗口大小
        char buffer[32];
        sprintf_s(buffer, "mode con cols=%d lines=%d",window_width, window_height);
        system(buffer);

        //隐藏光标
        HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_CURSOR_INFO CursorInfo;
        GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
        CursorInfo.bVisible = false; //隐藏控制台光标
        SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
        //初始化随机数种子
        srand((unsigned int)time(0));
    }
};

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

02 打印信息类

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

下面挑几个重点的来讲:

2.1 画地图边界

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

//画地图边界
static void DrawMap()
{
    system("cls");
    int i, j;
    for (i = 0; i < GameSetting::window_width; i++)
        cout << "#";
    cout << endl;
    for (i = 0; i < GameSetting::window_height-2; i++)
    {
        for (j = 0; j < GameSetting::window_width; j++)
        {
            if (i == 13 && j >= GameSetting::window_width - 29)
            {
                cout << "#";
                continue;
            }

            if (j == 0 || j == GameSetting::window_width - 29 || j == GameSetting::window_width-1)
            {
                cout << "#";
            }
            else
                cout << " ";
            
        }
        cout << endl;
    }
    for (i = 0; i < GameSetting::window_width; i++)
        cout << "#";
    
}

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

2.2 画出分数和模式

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

//画分数
static void DrawScore(int score)
{
    gotoxy(GameSetting::window_width - 22+14, 6);
    cout << "  ";
    gotoxy(GameSetting::window_width - 22+14, 4);
    cout << "  ";

    gotoxy(GameSetting::window_width - 22, 6);
    cout << "当前玩家分数: " << score << endl;
    gotoxy(GameSetting::window_width - 22, 4);
    cout << "当前游戏速度: " << 10 - speed / 25 << endl;

}

03 食物类

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

typedef struct
{
    int x;
    int y;
}COORDINATE;

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

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

3.1 随机生成食物

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

void RandomXY(vector<COORDINATE> & coord)
{
    m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
    m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
    unsigned int i;
    //原则上不允许食物出现在蛇的位置上,如果有,重新生成
    for (i = 0; i < coord.size(); i++)
    {
        //食物出现在蛇身的位置上。重新生成
        if (coord[i].x == m_coordinate.x && coord[i].y == m_coordinate.y)
        {
            m_coordinate.x = rand() % (GameSetting::window_width - 30) + 1;
            m_coordinate.y = rand() % (GameSetting::window_height - 2) + 1;
            i = 0;
        }
    }
}

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

3.2 画出食物

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

void DrawFood()
{
    setColor(12, 0);
    gotoxy(m_coordinate.x, m_coordinate.y);
    cout << "@";
    setColor(7, 0);
}

04 贪吃蛇类

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

4.1 成员变量

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

4.2 默认构造函数

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

        //移动方向向上
    m_direction = 1;
    m_is_alive = true;
    COORDINATE snake_head;
    //蛇头生成位置
    snake_head.x = GameSetting::window_width / 2 - 15;
    snake_head.y = GameSetting::window_height / 2;

    this->m_coordinate.push_back(snake_head);
    snake_head.y++;
    this->m_coordinate.push_back(snake_head);
    snake_head.y++;
    this->m_coordinate.push_back(snake_head); //初始蛇身长度三节

4.3 监听键盘

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

    //监听键盘
void listen_key_borad()
{
    char ch;

    if (_kbhit())                    //kbhit 非阻塞函数 
    {
        ch = _getch();    //使用 getch 函数获取键盘输入 
        switch (ch)
        {
        case 'w':
        case 'W':
            if (this->m_direction == DOWN)
                break;
            this->m_direction = UP;
            break;
        case 's':
        case 'S':
            if (this->m_direction == UP)
                break;
            this->m_direction = DOWN;
            break;
        case 'a':
        case 'A':
            if (this->m_direction == RIGHT)
                break;
            this->m_direction = LEFT;
            break;
        case 'd':
        case 'D':
            if (this->m_direction == LEFT)
                break;
            this->m_direction = RIGHT;
            break;
        case '+':
            if (speed >= 25)
            {
                speed -= 25;
            }
            break;
        case '-':
            if (speed < 250)
            {
                speed += 25;
            }
            break;
        }
    }
}

4.4 移动贪吃蛇

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

    //移动贪吃蛇
void move_snake()
{
    //监听键盘
    listen_key_borad();
    //蛇头
    COORDINATE head = m_coordinate[0];
    //direction方向:1 上  2 下  3 左  4 右
    switch (this->m_direction)
    {
    case UP:
        head.y--;
        break;
    case DOWN:
        head.y++;
        break;
    case LEFT:
        head.x--;
        break;
    case RIGHT:
        head.x++;
        break;
    }
    //插入移动后新的蛇头。是否删除蛇尾,在后续吃到食物判断那里做
    m_coordinate.insert(m_coordinate.begin(), head);
}

4.5 是否吃到食物

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

bool is_eat_food(Food & f)
{
    //获取食物坐标
    COORDINATE food_coordinate = f.GetFoodCoordinate();
    //吃到食物,食物重新生成,不删除蛇尾
    if (m_coordinate[HEAD].x == food_coordinate.x && m_coordinate[HEAD].y == food_coordinate.y)
    {
        f.RandomXY(m_coordinate);
        return true;
    }
    else
    {
        //没有吃到食物,删除蛇尾
        m_coordinate.erase(m_coordinate.end() - 1);
        return false;
    }
}

4.6判断蛇是否还存活

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

//判断贪吃蛇死了没
bool snake_is_alive()
{
    if (m_coordinate[HEAD].x <= 0 ||
        m_coordinate[HEAD].x >= GameSetting::window_width - 29 ||
        m_coordinate[HEAD].y <= 0 ||
        m_coordinate[HEAD].y >= GameSetting::window_height - 1)
    {
        //超出边界
        m_is_alive = false;
        return m_is_alive;
    }
    //和自己碰到一起
    for (unsigned int i = 1; i < m_coordinate.size(); i++)
    {
        if (m_coordinate[i].x == m_coordinate[HEAD].x && m_coordinate[i].y == m_coordinate[HEAD].y)
        {
            m_is_alive = false;
            return m_is_alive;
        }
    }
    m_is_alive = true;

    return m_is_alive;
}

4.7 画出贪吃蛇

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

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

4.8 清除屏幕上的贪吃蛇

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

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

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

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

GameSetting setting;
PrintInfo print_info;
Snake  snake;
//初始化游戏
setting.GameInit();
//游戏模式选择
print_info.DrawChoiceInfo();

char ch = _getch();
switch (ch)
{
case '1':
    snake.set_model(true);
    speed = 50;
    break;
case '2':
    snake.set_model(false);
    break;
default:
    gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
    cout << "输入错误,Bye!" << endl;
    cin.get();
    cin.get();
    return 0;
}
gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3);
system("pause");

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

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

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

//游戏死循环
while (true)
{
    //打印成绩
    print_info.DrawScore(snake.GetSnakeSize());
    //画出食物
    food.DrawFood();
    //清理蛇尾,每次画蛇前必做
    snake.ClearSnake();
    //判断是否吃到食物
    snake.is_eat_food(food);
    //根据用户模式选择不同的调度方式
    if (snake.GetModel() == true)
    {
        snake.move_snake();
    }
    else
    {
        snake.AI_find_path(food);
        snake.AI_move_snake();
    }
    //画蛇
    snake.draw_snake();
    //判断蛇是否还活着
    if (!snake.snake_is_alive())
    {
        print_info.GameOver(snake.GetSnakeSize());
        break;
    }
    //控制游戏速度
    Sleep(speed);
}

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

06 AI部分和完善

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

目录
相关文章
|
28天前
|
人工智能 JSON Java
【极速入门版】编程小白也能轻松上手Comate AI编程插件
【极速入门版】编程小白也能轻松上手Comate AI编程插件
28 0
|
28天前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
207 62
|
5天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI的奥秘:机器学习入门指南
【10月更文挑战第30天】本篇文章是一份初学者友好的机器学习入门指南,旨在帮助读者理解并开始实践机器学习。我们将介绍机器学习的基本概念,包括监督学习、无监督学习和强化学习等。我们还将提供一些实用的代码示例,以帮助读者更好地理解和应用这些概念。无论你是编程新手,还是有一定经验的开发者,这篇文章都将为你提供一个清晰的机器学习入门路径。
21 2
|
13天前
|
人工智能 API 决策智能
swarm Agent框架入门指南:构建与编排多智能体系统的利器 | AI应用开发
Swarm是OpenAI在2024年10月12日宣布开源的一个实验性质的多智能体编排框架。其核心目标是让智能体之间的协调和执行变得更轻量级、更容易控制和测试。Swarm框架的主要特性包括轻量化、易于使用和高度可定制性,非常适合处理大量独立的功能和指令。【10月更文挑战第15天】
96 6
|
14天前
|
存储 人工智能 Java
Neo4j从入门到精通:打造高效知识图谱数据库 | AI应用开发
在大数据和人工智能时代,知识图谱作为一种高效的数据表示和查询方式,逐渐受到广泛关注。本文从入门到精通,详细介绍知识图谱及其存储工具Neo4j,涵盖知识图谱的介绍、Neo4j的特点、安装步骤、使用方法(创建、查询)及Cypher查询语言的详细讲解。通过本文,读者将全面了解如何利用Neo4j处理复杂关系数据。【10月更文挑战第14天】
58 6
|
17天前
|
人工智能 自动驾驶 搜索推荐
【通义】AI视界|苹果AI本周正式上线,将引入四大功能
本文由【通义】自动生成,涵盖苹果AI上线、特斯拉被华尔街重新评估、谷歌开发控制计算机的AI、Meta与路透社合作及Waymo获56亿美元融资等科技动态。点击链接或扫描二维码获取更多信息。
|
16天前
|
人工智能 IDE 开发工具
C++中的AI编程助手添加
【10月更文挑战第16天】AI 对我们来说就是一个可靠的编程助手,给我们提供了实时的建议和解决方案,无论是快速修复错误、提升代码质量,或者查找关键文档和资源,AI 作为编程助手都能让你事半功倍。
|
20天前
|
算法 数据挖掘 Shell
「毅硕|生信教程」 micromamba:mamba的C++实现,超越conda
还在为生信软件的安装配置而烦恼?micromamba(micromamba是mamba包管理器的小型版本,采用C++实现,具有mamba的核心功能,且体积更小,可以脱离conda独立运行,更易于部署)帮你解决!
45 1
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
33 1