6.贪吃蛇实践

  1. 6.贪吃蛇实践
    1. 6.1 知识点
      1. 原型图
        1. 开始场景
        2. 游戏场景
        3. 结束场景
      2. UML类图
        1. 总览类图
        2. 场景系统类图
      3. 游戏场景类图
      4. 游戏主入口、游戏类和游戏帧更新接口
        1. 将实现的类图
        2. 创建游戏帧更新接口
        3. 创建场景枚举
        4. 导入自定义控制台,包括一些辅助方法
        5. 游戏类包括初始化,开始游戏和切换场景方法,开始游戏时调用当前场景的主循环
        6. 在主入口启动游戏
      5. 多场景切换以及具体实现
        1. 将实现的类图
        2. 开始场景和结束场景有共同之处,可以抽出来一个类
        3. 开始场景类
        4. 结束场景类
        5. 游戏场景类
      6. 绘制接口,游戏对象类,位置结构体
        1. 将实现的类图
        2. 绘制接口
        3. 游戏对象类,实现绘制接口
        4. 位置结构体,代表位置,重载运算符
      7. 食物类、墙壁类和地图墙壁类
        1. 将实现的类图
        2. 食物类,默认不会在墙上产生,如果随机到蛇重合递归重新随机
        3. 墙壁类
        4. 地图墙壁类,拥有墙壁集合
      8. 蛇相关
        1. 蛇身体类型枚举
        2. 蛇身类
        3. 蛇类
    2. 6.2 知识点代码
      1. 贪吃蛇实践.cpp
      2. ISceneUpdate.h
      3. E_SceneType.h
      4. CustomConsole.h和CustomConsole.cpp
      5. Game.h和Game.cpp
      6. BeginOrEndBaseScene.h和BeginOrEndBaseScene.cpp
      7. BeginScene.h和BeginScene.cpp
      8. EndScene.h和EndScene.cpp
      9. GameScene.h和GameScene.cpp
      10. IDraw.h
      11. GameObject.h
      12. Position.h和Position.cpp
      13. Food.h和Food.cpp
      14. Wall.h和Wall.cpp
      15. Map.h和Map.cpp
      16. E_SnakeBody_Type.h和E_SnakeBody_Type.cpp
      17. SnakeBody.h和SnakeBody.cpp
      18. Snake.h和Snake.cpp

6.贪吃蛇实践


6.1 知识点

原型图

开始场景

游戏场景

结束场景

UML类图

总览类图

场景系统类图

游戏场景类图

游戏主入口、游戏类和游戏帧更新接口

将实现的类图

创建游戏帧更新接口

#pragma once
//场景更新相关的接口
class ISceneUpdate
{
public:
    virtual void Update() = 0;
    virtual ~ISceneUpdate() {}
};

创建场景枚举

#pragma once
enum class E_SceneType
{
    //开始
    Begin,
    //游戏
    Game,
    //结束
    End,
};

导入自定义控制台,包括一些辅助方法

#pragma once
#include <windows.h>
#include <random>
using namespace std;

//设置光标位置函数
void setCursorPosition(int x, int y);
//设置控制台大小函数
void setConsoleSize(int width, int height);
//设置文本颜色函数
void setTextColor(WORD color);
//设置光标显示隐藏
void setCursorVisibility(bool visible);
//关闭控制台
void closeConsole();
//获取随机数
int getRandom(int min, int max);
#include "CustomConsole.h"

//设置光标位置
void setCursorPosition(int x, int y) {
    // 获取当前的标准输出句柄
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置光标位置的坐标
    COORD cursorPosition;
    cursorPosition.X = x;  // 横坐标(列)
    cursorPosition.Y = y;  // 纵坐标(行)

    // 调用 Windows API 函数设置光标位置
    SetConsoleCursorPosition(hConsole, cursorPosition);
}

//设置控制台大小
void setConsoleSize(int width, int height) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置控制台屏幕缓冲区大小
    COORD bufferSize;
    bufferSize.X = width + 2;
    bufferSize.Y = height + 1;
    SetConsoleScreenBufferSize(hConsole, bufferSize);

    // 设置控制台窗口大小
    SMALL_RECT windowSize;
    windowSize.Left = 0;
    windowSize.Top = 0;
    windowSize.Right = width;
    windowSize.Bottom = height;
    SetConsoleWindowInfo(hConsole, TRUE, &windowSize);
}

//设置文本颜色
void setTextColor(WORD color) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTextAttribute(hConsole, color);
}
//设置光标显隐
void setCursorVisibility(bool visible) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;

    // 获取当前光标信息
    GetConsoleCursorInfo(hConsole, &cursorInfo);
    cursorInfo.bVisible = visible;  // 设置光标是否可见
    SetConsoleCursorInfo(hConsole, &cursorInfo);
}

//关闭控制台
void closeConsole() {
    HWND hConsole = GetConsoleWindow();  // 获取控制台窗口句柄
    if (hConsole != NULL) {
        PostMessage(hConsole, WM_CLOSE, 0, 0);  // 发送关闭消息
    }
}

int getRandom(int min, int max)
{
    // 创建随机数生成器
    random_device rd;  // 获得随机数种子
    mt19937 gen(rd()); // 使用 Mersenne Twister 生成器
    uniform_int_distribution<> dis(min, max); // 定义均匀分布 [0, 99]

    // 生成一个随机数
    int randomNumber = dis(gen);
    return randomNumber;
}

游戏类包括初始化,开始游戏和切换场景方法,开始游戏时调用当前场景的主循环

#pragma once
#include "ISceneUpdate.h"
#include "E_SceneType.h"
class Game
{
public:
    static const int w = 80;
    static const int h = 20;
    //管理当前场景的 接口 之后 用它来存储 游戏、开始、结束场景对象
    //之所以把它改成静态 是因为静态方法中要使用它
    static ISceneUpdate* nowScene;

    Game();
    ~Game();
    //开始游戏主循环
    void Start();
    //切换到哪个场景
    static void ChangeScene(E_SceneType type);
};
#include "Game.h"
#include "CustomConsole.h"
#include "GameScene.h"
#include "BeginScene.h"
#include "EndScene.h"

ISceneUpdate* Game::nowScene = nullptr;

Game::Game()
{
    //初始化控制台相关
    //隐藏光标
    setCursorVisibility(false);
    //设置窗口大小
    setConsoleSize(w, h);

    //一开始游戏 初始化时 就应该前往 开始场景
    ChangeScene(E_SceneType::Begin);
}

Game::~Game()
{
    if (nowScene != nullptr)
    {
        delete nowScene;
        nowScene = nullptr;
    }
}

void Game::Start()
{
    while (true)
    {
        //游戏主循环其实就是去更新游戏当前场景
        //只要场景不为空 我们就更新他们里面的逻辑
        if (nowScene != nullptr)
            nowScene->Update();
    }
}

void Game::ChangeScene(E_SceneType type)
{
    //切场景之前 应该擦除当前控制台行所有内容
    system("cls");

    //释放之前的场景
    if (nowScene != nullptr)
    {
        delete nowScene;
        nowScene = nullptr;
    }

    switch (type)
    {
        case E_SceneType::Begin:
            nowScene = new BeginScene();
            break;
        case E_SceneType::Game:
            nowScene = new GameScene();
            break;
        case E_SceneType::End:
            nowScene = new EndScene();
            break;
        default:
            break;
    }
}

在主入口启动游戏

#include <iostream>
#include "Game.h"
int main()
{
    Game* game = new Game();
    game->Start();  
}

多场景切换以及具体实现

将实现的类图

开始场景和结束场景有共同之处,可以抽出来一个类

#include "BeginOrEndBaseScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include "E_Color.h"
#include "conio.h"
void BeginOrEndBaseScene::Update()
{
    //开始和结束场景的 游戏逻辑
    //1.显示标题
    setTextColor(White);
    setCursorPosition(Game::w / 2 - strTitle.size() / 2, 5);
    cout << strTitle;
    //2.显示下方的选项
    setCursorPosition(Game::w / 2 - strOne.size() / 2, 8);
    setTextColor(nowSelIndex == 0 ? Red : White);//根据当前是否选中设置颜色
    cout << strOne;

    setCursorPosition(Game::w / 2 - 4, 10);
    setTextColor(nowSelIndex == 1 ? Red : White);//根据当前是否选中设置颜色
    cout << "结束游戏";
    
    //3.检测输入
    int input = _getch();
    switch (input)
    {
        case 'W':
        case 'w':
            --nowSelIndex;
            if (nowSelIndex < 0)
                nowSelIndex = 0;
            break;
        case 'S':
        case 's':
            ++nowSelIndex;
            if (nowSelIndex > 1)
                nowSelIndex = 1;
            break;
        case 'J':
        case 'j':
            EnterJDoSomthing();
            break;
    }
}
#pragma once
#include "ISceneUpdate.h"
#include <iostream>
using namespace std;
class BeginOrEndBaseScene :
    public ISceneUpdate
{
public:
    //当前选择的选项
    int nowSelIndex = 0;
    //标题
    string strTitle;
    //第一个选项
    string strOne;

    void Update() override;
    //按J键处理的逻辑 交给子类去重写即可
    virtual void EnterJDoSomthing() = 0;
};

开始场景类

#pragma once
#include "BeginOrEndBaseScene.h"
class BeginScene : public BeginOrEndBaseScene
{
public:
    BeginScene();
    void EnterJDoSomthing() override;
};
#include "BeginScene.h"
#include "CustomConsole.h"
#include "Game.h"
BeginScene::BeginScene()
{
    strTitle = "贪食蛇";
    strOne = "开始游戏";
}

void BeginScene::EnterJDoSomthing()
{
    //切换到游戏场景
    if (nowSelIndex == 0)
        Game::ChangeScene(E_SceneType::Game);
    else//如果没有选择第一个选项 那一定是退出游戏的操作
        closeConsole();
}

结束场景类

#pragma once
#include "BeginOrEndBaseScene.h"
class EndScene :
    public BeginOrEndBaseScene
{
public:
    EndScene();
    void EnterJDoSomthing() override;
};
#include "EndScene.h"
#include "Game.h"
#include "CustomConsole.h"
EndScene::EndScene()
{
    strTitle = "结束游戏";
    strOne = "回到开始界面";
}

void EndScene::EnterJDoSomthing()
{
    //切换到开始场景
    if (nowSelIndex == 0)
        Game::ChangeScene(E_SceneType::Begin);
    else//如果没有选择第一个选项 那一定是退出游戏的操作
        closeConsole();
}

游戏场景类

#pragma once
#include "ISceneUpdate.h"
#include "Map.h"
#include "Snake.h"
#include "Food.h"
class GameScene :
    public ISceneUpdate
{
public:
    Map* map;
    Snake* snake;
    Food* food;
    int updateIndex = 0;
    GameScene();
    ~GameScene();
    void Update() override;
};
#include "GameScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include <iostream>
#include <conio.h>
using namespace std;

GameScene::GameScene()
{
    map = new Map();
    snake = new Snake(40, 10);
    food = new Food(snake);
}

GameScene::~GameScene()
{
    if (map != nullptr)
    {
        delete map;
        map = nullptr;
    }

    if (snake != nullptr)
    {
        delete snake;
        snake = nullptr;
    }

    if (food != nullptr)
    {
        delete food;
        food = nullptr;
    }
}

void GameScene::Update()
{
    if (updateIndex % 9999 == 0)
    {
        /*setCursorPosition(0, 0);
        cout << "游戏场景";*/
        map->Draw();
        food->Draw();

        //先让蛇动起来 
        snake->Move();
        //再去绘制
        snake->Draw();

        //移动过后 检测蛇是否撞墙或者身体
        //来判断是否结束
        if (snake->CheckEnd(map))
        {
            //结束相关逻辑
            Game::ChangeScene(E_SceneType::End);
            return;
        }
        //蛇移动结束后 判断是否和食物重合(是否吃到了食物)
        snake->CheckEatFood(food);

        updateIndex = 1;
    }
    ++updateIndex;

    //我们的这个输入检测 希望马上就执行 不应该延时执行
    if (_kbhit())
    {
        int input = _getch();
        switch (input)
        {
            case 'W':
            case 'w':
                snake->ChangeDir(E_MoveDir::Up);
                break;
            case 'S':
            case 's':
                snake->ChangeDir(E_MoveDir::Down);
                break;
            case 'A':
            case 'a':
                snake->ChangeDir(E_MoveDir::Left);
                break;
            case 'D':
            case 'd':
                snake->ChangeDir(E_MoveDir::Right);
                break;
        }
    }
}

绘制接口,游戏对象类,位置结构体

将实现的类图

绘制接口

#pragma once
//绘制接口 提供绘制的行为抽象
class IDraw
{
public:
    virtual void Draw() = 0;
    virtual ~IDraw() {}
};

游戏对象类,实现绘制接口

#pragma once
#include "IDraw.h"
#include "Position.h"
#include <iostream>
using namespace std;
class GameObject :
    public IDraw
{
public:
    Position pos = Position(0,0);
};

位置结构体,代表位置,重载运算符

#pragma once
struct Position
{
public:
    int x;
    int y;
    Position(int x, int y) :x(x), y(y)
    {

    }

    //贪食蛇项目中 肯定存在位置的比较 判断是否是重合
    bool operator ==(const Position& p) const;
    bool operator !=(const Position& p) const;

};
#include "Position.h"

bool Position::operator==(const Position& p) const
{
    if (this->x == p.x && this->y == p.y)
        return true;
    return false;
}

bool Position::operator!=(const Position& p) const
{
    if (this->x == p.x && this->y == p.y)
        return false;
    return true;
}

食物类、墙壁类和地图墙壁类

将实现的类图

食物类,默认不会在墙上产生,如果随机到蛇重合递归重新随机

#pragma once
#include "GameObject.h"
#include "Snake.h"
class Food :
    public GameObject
{
public:
    Food(Snake* snake);
    void Draw() override;

    //随机位置 之后有蛇对象了 才好去写
    //因为随机位置时 需要得到蛇的头、身体位置 用来判断不能重合
    void RandomPos(Snake* snake);
};
#include "Food.h"
#include "CustomConsole.h"
#include "E_Color.h"
#include "Game.h"
Food::Food(Snake* snake)
{
    RandomPos(snake);
}

void Food::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(Blue);
    cout << "¤";
}

void Food::RandomPos(Snake* snake)
{
    //x方向的随机数 排除了 墙体的随机数
    int x = getRandom(1, Game::w / 2 - 2) * 2;
    //y方向的随机数 排除了 墙体的随机数
    int y = getRandom(1, Game::h - 2);
    //设置食物位置
    pos.x = x;
    pos.y = y;
    //传入食物的位置 和蛇判断是否重合 如果重合 那么应该重新随机位置
    //我们通过递归函数 来得到一个一定不会和蛇重合的位置
    if (snake->CheckSamePos(pos))
        RandomPos(snake);
}

墙壁类

#pragma once
#include "GameObject.h"
class Wall:public GameObject
{
public:
    Wall();
    Wall(int x, int y);
    void Draw() override;
};
#include "Wall.h"
#include "CustomConsole.h"
#include "E_Color.h"
Wall::Wall()
{
    this->pos.x = 0;
    this->pos.y = 0;
}

Wall::Wall(int x, int y)
{
    this->pos.x = x;
    this->pos.y = y;
}

void Wall::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(Red);
    cout << "■";
}

地图墙壁类,拥有墙壁集合

#pragma once
#include "IDraw.h"
#include "Wall.h"
class Map :
    public IDraw
{
public:
    Wall* walls;//数组指针
    int size;//存储数组容量的成员
    Map();
    ~Map();
    void Draw() override;
};
#include "Map.h"
#include "Game.h"
Map::Map()
{
    //初始化墙壁数组
    size = Game::w + (Game::h - 2) * 2;
    //初始化输入 容量是算好的
    walls = new Wall[size];
    int index = 0;
    for (int i = 0; i < Game::w; i+=2)
    {
        walls[index].pos.x = i;
        walls[index].pos.y = 0;
        ++index;
    }

    for (int i = 0; i < Game::w; i += 2)
    {
        walls[index].pos.x = i;
        walls[index].pos.y = Game::h - 1;
        ++index;
    }

    for (int i = 1; i < Game::h - 1; i++)
    {
        walls[index].pos.x = 0;
        walls[index].pos.y = i;
        ++index;
    }

    for (int i = 1; i < Game::h - 1; i++)
    {
        walls[index].pos.x = Game::w - 2;
        walls[index].pos.y = i;
        ++index;
    }
}

Map::~Map()
{
    //释放所有墙壁
    if (walls != nullptr)
    {
        delete[] walls;
        walls = nullptr;
    }
}

void Map::Draw()
{
    //绘制所有墙壁
    for (int i = 0; i < size; i++)
    {
        walls[i].Draw();
    }
}

蛇相关

蛇身体类型枚举

#pragma once
enum class E_SnakeBody_Type
{
    //头类型
    Head,
    //身体类型
    Body,
};

蛇身类

#pragma once
#include "GameObject.h"
#include "E_SnakeBody_Type.h"
class SnakeBody :
    public GameObject
{
public:
    E_SnakeBody_Type type;
    SnakeBody(E_SnakeBody_Type type, int x, int y);
    void Draw() override;
};
#include "SnakeBody.h"
#include "CustomConsole.h"
#include "E_Color.h"
SnakeBody::SnakeBody(E_SnakeBody_Type type, int x, int y)
{
    this->type = type;
    this->pos.x = x;
    this->pos.y = y;
}

void SnakeBody::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(type == E_SnakeBody_Type::Head ? Green : White);
    cout << (type == E_SnakeBody_Type::Head ? "●" : "◎");
}

蛇类

#pragma once
#include "IDraw.h"
#include "SnakeBody.h"
#include "Map.h"

class Food;

//蛇移动方向枚举
enum class E_MoveDir
{
    Up,//上
    Down,//下
    Left,//左
    Right,//右
};

class Snake :
    public IDraw
{
public:
    //由于蛇是动态成长的,不应该用new的方式去声明数组
    //因为new的方式 一开始相当于就创建了对应个数个 身体对象
    //我们应该吃了食物再去动态创建对象
    SnakeBody* bodys[200] = {};
    int nowNum = 0;
    E_MoveDir nowMoveDir = E_MoveDir::Right;

    Snake(int x, int y);
    ~Snake();
    void Draw() override;
    void Move();
    void ChangeDir(E_MoveDir dir);
    bool CheckEnd(Map* map);
    //判断外面传入的一个位置 是否和自己的头和身体重合
    bool CheckSamePos(Position& pos);
    void CheckEatFood(Food* food);
private:
    void AddBody();

};
#include "Snake.h"
#include "CustomConsole.h"
#include "Food.h"
Snake::Snake(int x, int y)
{
    //首先应该有对应的蛇头
    bodys[0] = new SnakeBody(E_SnakeBody_Type::Head, x, y);
    //通过一个索引去记录蛇有多长 不能通过数组的长度 
    //因为数组默认一来就很长
    nowNum = 1;
}

Snake::~Snake()
{
    for (int i = 0; i < nowNum; i++)
    {
        delete bodys[i]; 
        bodys[i] = nullptr;
    }
}

void Snake::Draw()
{
    for (int i = 0; i < nowNum; i++)
    {
        bodys[i]->Draw();
    }
}

void Snake::Move()
{
    //我们可以在改变蛇位置之前 利用蛇之前的位置
    //去擦除之前绘制的内容(用空格将之前绘制的图像覆盖了)

    //为了考虑之后蛇有身体 所以我们应该获取身体的最后一截 去擦除它
    //最后一截身体 就是数组中存储的最后一个身体
    SnakeBody* lastBody = bodys[nowNum - 1];
    setCursorPosition(lastBody->pos.x, lastBody->pos.y);
    cout << "  ";

    for (int i = nowNum - 1; i > 0; i--)
    {
        bodys[i]->pos.x = bodys[i - 1]->pos.x;
        bodys[i]->pos.y = bodys[i - 1]->pos.y;
    }

    switch (nowMoveDir)
    {
        case E_MoveDir::Up:
            --bodys[0]->pos.y;
            break;
        case E_MoveDir::Down:
            ++bodys[0]->pos.y;
            break;
        case E_MoveDir::Left:
            bodys[0]->pos.x -= 2;
            break;
        case E_MoveDir::Right:
            bodys[0]->pos.x += 2;
            break;
        default:
            break;
    }
}

//改变当前前进方向的方法
void Snake::ChangeDir(E_MoveDir dir)
{
    //如果蛇有身体 就不能够直接 左转右 右转左 上转下 下转上 
    //如果转向和上次一样 也不用再转
    if (dir == nowMoveDir ||
        nowNum > 1 &&
        (nowMoveDir == E_MoveDir::Right && dir == E_MoveDir::Left ||
         nowMoveDir == E_MoveDir::Left && dir == E_MoveDir::Right ||
         nowMoveDir == E_MoveDir::Up && dir == E_MoveDir::Down ||
        nowMoveDir == E_MoveDir::Down && dir == E_MoveDir::Up))
    {
        //如果是特殊情况 就直接return 不要执行转向逻辑
        return;
    }

    //改变当前的移动方向为传入的方向
    this->nowMoveDir = dir;
}

bool Snake::CheckEnd(Map* map)
{
    //撞墙
    for (int i = 0; i < map->size; i++)
    {
        if (bodys[0]->pos == map->walls[i].pos) 
            return true;
    }

    //撞身体
    for (int i = 1; i < nowNum; i++)
    {
        if (bodys[0]->pos == bodys[i]->pos)
            return true;
    }

    return false;
}

bool Snake::CheckSamePos(Position& pos)
{
    //判断蛇身体 是否有和传入位置重合的 如果有 就直接返回true
    //认为有重合
    for (int i = 0; i < nowNum; i++)
    {
        if (pos == bodys[i]->pos)
            return true;
    }

    return false;
}

void Snake::CheckEatFood(Food* food)
{
    if (bodys[0]->pos == food->pos)
    {
        //吃到食物的逻辑处理
        //1.随机食物位置
        food->RandomPos(this);
        //2.长身体
        AddBody();
    }
}

void Snake::AddBody()
{
    //当前最后一截身体
    SnakeBody* frontBody = bodys[nowNum - 1];
    //新键一个身体 让其位置和最后一个身体位置一致
    bodys[nowNum] = new SnakeBody(E_SnakeBody_Type::Body, frontBody->pos.x, frontBody->pos.y);
    //蛇身体涨了 计数就应该增加
    ++nowNum;
}

6.2 知识点代码

贪吃蛇实践.cpp

#include <iostream>
#include "Game.h"
int main()
{
    Game* game = new Game();
    game->Start();  
}

ISceneUpdate.h

#pragma once
//场景更新相关的接口
class ISceneUpdate
{
public:
    virtual void Update() = 0;
    virtual ~ISceneUpdate() {}
};

E_SceneType.h

#pragma once
enum class E_SceneType
{
    //开始
    Begin,
    //游戏
    Game,
    //结束
    End,
};

CustomConsole.h和CustomConsole.cpp

#pragma once
#include <windows.h>
#include <random>
using namespace std;

//设置光标位置函数
void setCursorPosition(int x, int y);
//设置控制台大小函数
void setConsoleSize(int width, int height);
//设置文本颜色函数
void setTextColor(WORD color);
//设置光标显示隐藏
void setCursorVisibility(bool visible);
//关闭控制台
void closeConsole();
//获取随机数
int getRandom(int min, int max);
#include "CustomConsole.h"

//设置光标位置
void setCursorPosition(int x, int y) {
    // 获取当前的标准输出句柄
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置光标位置的坐标
    COORD cursorPosition;
    cursorPosition.X = x;  // 横坐标(列)
    cursorPosition.Y = y;  // 纵坐标(行)

    // 调用 Windows API 函数设置光标位置
    SetConsoleCursorPosition(hConsole, cursorPosition);
}

//设置控制台大小
void setConsoleSize(int width, int height) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置控制台屏幕缓冲区大小
    COORD bufferSize;
    bufferSize.X = width + 2;
    bufferSize.Y = height + 1;
    SetConsoleScreenBufferSize(hConsole, bufferSize);

    // 设置控制台窗口大小
    SMALL_RECT windowSize;
    windowSize.Left = 0;
    windowSize.Top = 0;
    windowSize.Right = width;
    windowSize.Bottom = height;
    SetConsoleWindowInfo(hConsole, TRUE, &windowSize);
}

//设置文本颜色
void setTextColor(WORD color) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTextAttribute(hConsole, color);
}
//设置光标显隐
void setCursorVisibility(bool visible) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;

    // 获取当前光标信息
    GetConsoleCursorInfo(hConsole, &cursorInfo);
    cursorInfo.bVisible = visible;  // 设置光标是否可见
    SetConsoleCursorInfo(hConsole, &cursorInfo);
}

//关闭控制台
void closeConsole() {
    HWND hConsole = GetConsoleWindow();  // 获取控制台窗口句柄
    if (hConsole != NULL) {
        PostMessage(hConsole, WM_CLOSE, 0, 0);  // 发送关闭消息
    }
}

int getRandom(int min, int max)
{
    // 创建随机数生成器
    random_device rd;  // 获得随机数种子
    mt19937 gen(rd()); // 使用 Mersenne Twister 生成器
    uniform_int_distribution<> dis(min, max); // 定义均匀分布 [0, 99]

    // 生成一个随机数
    int randomNumber = dis(gen);
    return randomNumber;
}

Game.h和Game.cpp

#pragma once
#include "ISceneUpdate.h"
#include "E_SceneType.h"
class Game
{
public:
    static const int w = 80;
    static const int h = 20;
    //管理当前场景的 接口 之后 用它来存储 游戏、开始、结束场景对象
    //之所以把它改成静态 是因为静态方法中要使用它
    static ISceneUpdate* nowScene;

    Game();
    ~Game();
    //开始游戏主循环
    void Start();
    //切换到哪个场景
    static void ChangeScene(E_SceneType type);
};
#include "Game.h"
#include "CustomConsole.h"
#include "GameScene.h"
#include "BeginScene.h"
#include "EndScene.h"

ISceneUpdate* Game::nowScene = nullptr;

Game::Game()
{
    //初始化控制台相关
    //隐藏光标
    setCursorVisibility(false);
    //设置窗口大小
    setConsoleSize(w, h);

    //一开始游戏 初始化时 就应该前往 开始场景
    ChangeScene(E_SceneType::Begin);
}

Game::~Game()
{
    if (nowScene != nullptr)
    {
        delete nowScene;
        nowScene = nullptr;
    }
}

void Game::Start()
{
    while (true)
    {
        //游戏主循环其实就是去更新游戏当前场景
        //只要场景不为空 我们就更新他们里面的逻辑
        if (nowScene != nullptr)
            nowScene->Update();
    }
}

void Game::ChangeScene(E_SceneType type)
{
    //切场景之前 应该擦除当前控制台行所有内容
    system("cls");

    //释放之前的场景
    if (nowScene != nullptr)
    {
        delete nowScene;
        nowScene = nullptr;
    }

    switch (type)
    {
        case E_SceneType::Begin:
            nowScene = new BeginScene();
            break;
        case E_SceneType::Game:
            nowScene = new GameScene();
            break;
        case E_SceneType::End:
            nowScene = new EndScene();
            break;
        default:
            break;
    }
}

BeginOrEndBaseScene.h和BeginOrEndBaseScene.cpp

#include "BeginOrEndBaseScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include "E_Color.h"
#include "conio.h"
void BeginOrEndBaseScene::Update()
{
    //开始和结束场景的 游戏逻辑
    //1.显示标题
    setTextColor(White);
    setCursorPosition(Game::w / 2 - strTitle.size() / 2, 5);
    cout << strTitle;
    //2.显示下方的选项
    setCursorPosition(Game::w / 2 - strOne.size() / 2, 8);
    setTextColor(nowSelIndex == 0 ? Red : White);//根据当前是否选中设置颜色
    cout << strOne;

    setCursorPosition(Game::w / 2 - 4, 10);
    setTextColor(nowSelIndex == 1 ? Red : White);//根据当前是否选中设置颜色
    cout << "结束游戏";
    
    //3.检测输入
    int input = _getch();
    switch (input)
    {
        case 'W':
        case 'w':
            --nowSelIndex;
            if (nowSelIndex < 0)
                nowSelIndex = 0;
            break;
        case 'S':
        case 's':
            ++nowSelIndex;
            if (nowSelIndex > 1)
                nowSelIndex = 1;
            break;
        case 'J':
        case 'j':
            EnterJDoSomthing();
            break;
    }
}
#pragma once
#include "ISceneUpdate.h"
#include <iostream>
using namespace std;
class BeginOrEndBaseScene :
    public ISceneUpdate
{
public:
    //当前选择的选项
    int nowSelIndex = 0;
    //标题
    string strTitle;
    //第一个选项
    string strOne;

    void Update() override;
    //按J键处理的逻辑 交给子类去重写即可
    virtual void EnterJDoSomthing() = 0;
};

BeginScene.h和BeginScene.cpp

#pragma once
#include "BeginOrEndBaseScene.h"
class BeginScene : public BeginOrEndBaseScene
{
public:
    BeginScene();
    void EnterJDoSomthing() override;
};
#include "BeginScene.h"
#include "CustomConsole.h"
#include "Game.h"
BeginScene::BeginScene()
{
    strTitle = "贪食蛇";
    strOne = "开始游戏";
}

void BeginScene::EnterJDoSomthing()
{
    //切换到游戏场景
    if (nowSelIndex == 0)
        Game::ChangeScene(E_SceneType::Game);
    else//如果没有选择第一个选项 那一定是退出游戏的操作
        closeConsole();
}

EndScene.h和EndScene.cpp

#pragma once
#include "BeginOrEndBaseScene.h"
class EndScene :
    public BeginOrEndBaseScene
{
public:
    EndScene();
    void EnterJDoSomthing() override;
};
#include "EndScene.h"
#include "Game.h"
#include "CustomConsole.h"
EndScene::EndScene()
{
    strTitle = "结束游戏";
    strOne = "回到开始界面";
}

void EndScene::EnterJDoSomthing()
{
    //切换到开始场景
    if (nowSelIndex == 0)
        Game::ChangeScene(E_SceneType::Begin);
    else//如果没有选择第一个选项 那一定是退出游戏的操作
        closeConsole();
}

GameScene.h和GameScene.cpp

#pragma once
#include "ISceneUpdate.h"
#include "Map.h"
#include "Snake.h"
#include "Food.h"
class GameScene :
    public ISceneUpdate
{
public:
    Map* map;
    Snake* snake;
    Food* food;
    int updateIndex = 0;
    GameScene();
    ~GameScene();
    void Update() override;
};
#include "GameScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include <iostream>
#include <conio.h>
using namespace std;

GameScene::GameScene()
{
    map = new Map();
    snake = new Snake(40, 10);
    food = new Food(snake);
}

GameScene::~GameScene()
{
    if (map != nullptr)
    {
        delete map;
        map = nullptr;
    }

    if (snake != nullptr)
    {
        delete snake;
        snake = nullptr;
    }

    if (food != nullptr)
    {
        delete food;
        food = nullptr;
    }
}

void GameScene::Update()
{
    if (updateIndex % 9999 == 0)
    {
        /*setCursorPosition(0, 0);
        cout << "游戏场景";*/
        map->Draw();
        food->Draw();

        //先让蛇动起来 
        snake->Move();
        //再去绘制
        snake->Draw();

        //移动过后 检测蛇是否撞墙或者身体
        //来判断是否结束
        if (snake->CheckEnd(map))
        {
            //结束相关逻辑
            Game::ChangeScene(E_SceneType::End);
            return;
        }
        //蛇移动结束后 判断是否和食物重合(是否吃到了食物)
        snake->CheckEatFood(food);

        updateIndex = 1;
    }
    ++updateIndex;

    //我们的这个输入检测 希望马上就执行 不应该延时执行
    if (_kbhit())
    {
        int input = _getch();
        switch (input)
        {
            case 'W':
            case 'w':
                snake->ChangeDir(E_MoveDir::Up);
                break;
            case 'S':
            case 's':
                snake->ChangeDir(E_MoveDir::Down);
                break;
            case 'A':
            case 'a':
                snake->ChangeDir(E_MoveDir::Left);
                break;
            case 'D':
            case 'd':
                snake->ChangeDir(E_MoveDir::Right);
                break;
        }
    }
}

IDraw.h

#pragma once
//绘制接口 提供绘制的行为抽象
class IDraw
{
public:
    virtual void Draw() = 0;
    virtual ~IDraw() {}
};

GameObject.h

#pragma once
#include "IDraw.h"
#include "Position.h"
#include <iostream>
using namespace std;
class GameObject :
    public IDraw
{
public:
    Position pos = Position(0,0);
};

Position.h和Position.cpp

#pragma once
struct Position
{
public:
    int x;
    int y;
    Position(int x, int y) :x(x), y(y)
    {

    }

    //贪食蛇项目中 肯定存在位置的比较 判断是否是重合
    bool operator ==(const Position& p) const;
    bool operator !=(const Position& p) const;

};
#include "Position.h"

bool Position::operator==(const Position& p) const
{
    if (this->x == p.x && this->y == p.y)
        return true;
    return false;
}

bool Position::operator!=(const Position& p) const
{
    if (this->x == p.x && this->y == p.y)
        return false;
    return true;
}

Food.h和Food.cpp

#pragma once
#include "GameObject.h"
#include "Snake.h"
class Food :
    public GameObject
{
public:
    Food(Snake* snake);
    void Draw() override;

    //随机位置 之后有蛇对象了 才好去写
    //因为随机位置时 需要得到蛇的头、身体位置 用来判断不能重合
    void RandomPos(Snake* snake);
};
#include "Food.h"
#include "CustomConsole.h"
#include "E_Color.h"
#include "Game.h"
Food::Food(Snake* snake)
{
    RandomPos(snake);
}

void Food::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(Blue);
    cout << "¤";
}

void Food::RandomPos(Snake* snake)
{
    //x方向的随机数 排除了 墙体的随机数
    int x = getRandom(1, Game::w / 2 - 2) * 2;
    //y方向的随机数 排除了 墙体的随机数
    int y = getRandom(1, Game::h - 2);
    //设置食物位置
    pos.x = x;
    pos.y = y;
    //传入食物的位置 和蛇判断是否重合 如果重合 那么应该重新随机位置
    //我们通过递归函数 来得到一个一定不会和蛇重合的位置
    if (snake->CheckSamePos(pos))
        RandomPos(snake);
}

Wall.h和Wall.cpp

#pragma once
#include "GameObject.h"
class Wall:public GameObject
{
public:
    Wall();
    Wall(int x, int y);
    void Draw() override;
};
#include "Wall.h"
#include "CustomConsole.h"
#include "E_Color.h"
Wall::Wall()
{
    this->pos.x = 0;
    this->pos.y = 0;
}

Wall::Wall(int x, int y)
{
    this->pos.x = x;
    this->pos.y = y;
}

void Wall::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(Red);
    cout << "■";
}

Map.h和Map.cpp

#pragma once
#include "IDraw.h"
#include "Wall.h"
class Map :
    public IDraw
{
public:
    Wall* walls;//数组指针
    int size;//存储数组容量的成员
    Map();
    ~Map();
    void Draw() override;
};
#include "Map.h"
#include "Game.h"
Map::Map()
{
    //初始化墙壁数组
    size = Game::w + (Game::h - 2) * 2;
    //初始化输入 容量是算好的
    walls = new Wall[size];
    int index = 0;
    for (int i = 0; i < Game::w; i+=2)
    {
        walls[index].pos.x = i;
        walls[index].pos.y = 0;
        ++index;
    }

    for (int i = 0; i < Game::w; i += 2)
    {
        walls[index].pos.x = i;
        walls[index].pos.y = Game::h - 1;
        ++index;
    }

    for (int i = 1; i < Game::h - 1; i++)
    {
        walls[index].pos.x = 0;
        walls[index].pos.y = i;
        ++index;
    }

    for (int i = 1; i < Game::h - 1; i++)
    {
        walls[index].pos.x = Game::w - 2;
        walls[index].pos.y = i;
        ++index;
    }
}

Map::~Map()
{
    //释放所有墙壁
    if (walls != nullptr)
    {
        delete[] walls;
        walls = nullptr;
    }
}

void Map::Draw()
{
    //绘制所有墙壁
    for (int i = 0; i < size; i++)
    {
        walls[i].Draw();
    }
}

E_SnakeBody_Type.h和E_SnakeBody_Type.cpp

#pragma once
enum class E_SnakeBody_Type
{
    //头类型
    Head,
    //身体类型
    Body,
};

SnakeBody.h和SnakeBody.cpp

#pragma once
#include "GameObject.h"
#include "E_SnakeBody_Type.h"
class SnakeBody :
    public GameObject
{
public:
    E_SnakeBody_Type type;
    SnakeBody(E_SnakeBody_Type type, int x, int y);
    void Draw() override;
};
#include "SnakeBody.h"
#include "CustomConsole.h"
#include "E_Color.h"
SnakeBody::SnakeBody(E_SnakeBody_Type type, int x, int y)
{
    this->type = type;
    this->pos.x = x;
    this->pos.y = y;
}

void SnakeBody::Draw()
{
    setCursorPosition(pos.x, pos.y);
    setTextColor(type == E_SnakeBody_Type::Head ? Green : White);
    cout << (type == E_SnakeBody_Type::Head ? "●" : "◎");
}

Snake.h和Snake.cpp

#pragma once
#include "IDraw.h"
#include "SnakeBody.h"
#include "Map.h"

class Food;

//蛇移动方向枚举
enum class E_MoveDir
{
    Up,//上
    Down,//下
    Left,//左
    Right,//右
};

class Snake :
    public IDraw
{
public:
    //由于蛇是动态成长的,不应该用new的方式去声明数组
    //因为new的方式 一开始相当于就创建了对应个数个 身体对象
    //我们应该吃了食物再去动态创建对象
    SnakeBody* bodys[200] = {};
    int nowNum = 0;
    E_MoveDir nowMoveDir = E_MoveDir::Right;

    Snake(int x, int y);
    ~Snake();
    void Draw() override;
    void Move();
    void ChangeDir(E_MoveDir dir);
    bool CheckEnd(Map* map);
    //判断外面传入的一个位置 是否和自己的头和身体重合
    bool CheckSamePos(Position& pos);
    void CheckEatFood(Food* food);
private:
    void AddBody();

};
#include "Snake.h"
#include "CustomConsole.h"
#include "Food.h"
Snake::Snake(int x, int y)
{
    //首先应该有对应的蛇头
    bodys[0] = new SnakeBody(E_SnakeBody_Type::Head, x, y);
    //通过一个索引去记录蛇有多长 不能通过数组的长度 
    //因为数组默认一来就很长
    nowNum = 1;
}

Snake::~Snake()
{
    for (int i = 0; i < nowNum; i++)
    {
        delete bodys[i]; 
        bodys[i] = nullptr;
    }
}

void Snake::Draw()
{
    for (int i = 0; i < nowNum; i++)
    {
        bodys[i]->Draw();
    }
}

void Snake::Move()
{
    //我们可以在改变蛇位置之前 利用蛇之前的位置
    //去擦除之前绘制的内容(用空格将之前绘制的图像覆盖了)

    //为了考虑之后蛇有身体 所以我们应该获取身体的最后一截 去擦除它
    //最后一截身体 就是数组中存储的最后一个身体
    SnakeBody* lastBody = bodys[nowNum - 1];
    setCursorPosition(lastBody->pos.x, lastBody->pos.y);
    cout << "  ";

    for (int i = nowNum - 1; i > 0; i--)
    {
        bodys[i]->pos.x = bodys[i - 1]->pos.x;
        bodys[i]->pos.y = bodys[i - 1]->pos.y;
    }

    switch (nowMoveDir)
    {
        case E_MoveDir::Up:
            --bodys[0]->pos.y;
            break;
        case E_MoveDir::Down:
            ++bodys[0]->pos.y;
            break;
        case E_MoveDir::Left:
            bodys[0]->pos.x -= 2;
            break;
        case E_MoveDir::Right:
            bodys[0]->pos.x += 2;
            break;
        default:
            break;
    }
}

//改变当前前进方向的方法
void Snake::ChangeDir(E_MoveDir dir)
{
    //如果蛇有身体 就不能够直接 左转右 右转左 上转下 下转上 
    //如果转向和上次一样 也不用再转
    if (dir == nowMoveDir ||
        nowNum > 1 &&
        (nowMoveDir == E_MoveDir::Right && dir == E_MoveDir::Left ||
         nowMoveDir == E_MoveDir::Left && dir == E_MoveDir::Right ||
         nowMoveDir == E_MoveDir::Up && dir == E_MoveDir::Down ||
        nowMoveDir == E_MoveDir::Down && dir == E_MoveDir::Up))
    {
        //如果是特殊情况 就直接return 不要执行转向逻辑
        return;
    }

    //改变当前的移动方向为传入的方向
    this->nowMoveDir = dir;
}

bool Snake::CheckEnd(Map* map)
{
    //撞墙
    for (int i = 0; i < map->size; i++)
    {
        if (bodys[0]->pos == map->walls[i].pos) 
            return true;
    }

    //撞身体
    for (int i = 1; i < nowNum; i++)
    {
        if (bodys[0]->pos == bodys[i]->pos)
            return true;
    }

    return false;
}

bool Snake::CheckSamePos(Position& pos)
{
    //判断蛇身体 是否有和传入位置重合的 如果有 就直接返回true
    //认为有重合
    for (int i = 0; i < nowNum; i++)
    {
        if (pos == bodys[i]->pos)
            return true;
    }

    return false;
}

void Snake::CheckEatFood(Food* food)
{
    if (bodys[0]->pos == food->pos)
    {
        //吃到食物的逻辑处理
        //1.随机食物位置
        food->RandomPos(this);
        //2.长身体
        AddBody();
    }
}

void Snake::AddBody()
{
    //当前最后一截身体
    SnakeBody* frontBody = bodys[nowNum - 1];
    //新键一个身体 让其位置和最后一个身体位置一致
    bodys[nowNum] = new SnakeBody(E_SnakeBody_Type::Body, frontBody->pos.x, frontBody->pos.y);
    //蛇身体涨了 计数就应该增加
    ++nowNum;
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏