基础项目——扫雷(c++)

目录

  • 前言
  • 一、环境配置
  • 二、基础框架
  • 三、关闭事件
  • 四、资源加载
  • 五、初始地图
  • 六、常量定义
  • 七、地图随机
  • 八、点击排雷
  • 九、格子类化
  • 十、 地图类化
  • 十一、 接口优化
  • 十二、 文件拆分
  • 十三、游戏重开

前言

各位小伙伴们,这期我们一起学习出贪吃蛇以外另一个基础的项目——扫雷,这个项目需要我们自己到网站下载一些需要用的库,所以和贪吃蛇比起相对来说要复杂一点,这期涉及到面向对象的知识,还有文件拆分。

一、环境配置

首先要下载 SFML:https://www.sfml-dev.org/download/sfml/2.6.1/,解压到本地放代码的文件夹。
在这里插入图片描述
因为我们需要#include <SFML/Graphics.hpp>,这个头文件,创建好一个项目,我们在配置它的环境。点击项目,右键属性,选择这里的 C/C++,选择【附加包含目录】,选择 SFML 的 include 目录,点击确定。
在这里插入图片描述
然后点击 附加库 -> 常规,附加库目录,选择 SFML 的 lib 目录:
在这里插入图片描述
再点击下面的【输入】,选择右边的【附加依赖项】,把这些内容拷贝进去:
(这里需要注意,对于 sfml-xxx-d.lib 是适用于 debug 模式的,sfml-xxx.lib 是适用于 release 模式的)
sfml-graphics-d.lib
sfml-window-d.lib
sfml-system-d.lib
sfml-audio-d.lib
opengl32.lib
freetype.lib
winmm.lib
gdi32.lib
在这里插入图片描述
最后点击应用,就配置完毕了。
然后再把 SFML-2.6.0\bin 目录下的 动态链接库文件 dll 都拷贝到项目目录下。
在这里插入图片描述
我们写个 main 函数来测试一下。

#include <SFML/Graphics.hpp>

int main() {
    sf::RenderWindow app(sf::VideoMode(816, 576), "MineSweeper");
    while (app.isOpen()) {

    }
    return 0;
}

在这里插入图片描述

二、基础框架

1、命名空间
sf 是这个库的命名空间,基本上所有的接口都是从这里取的,利用两个冒号来获取相应的函数或者类,如果不想写这段前缀呢,我们可以和 std 一样,在代码前面写上这么一句话:

using namespace sf;

这样一来,这些前缀就都可以去掉了。
2、窗口创建

RenderWindow win(VideoMode(816, 576), "MineSweeper");

这段代码呢,就是实例化了一个窗口对象,RenderWindow 是一个类,win 是一个对象名,这一段就是有参构造函数的传参,我们可以 F12 进去看它的定义。
这里的 VideoMode 也是一个类,这两个参数是 VideoMode 构造函数的传参,分别代表了窗口的宽高。
3、字符集
VideoMode 中构造函数的第二个传参,是一个字符串,代表了窗口的标题,然后我们实现一个 while 循环。

while (win.isOpen()) {

}

并且循环条件是 win.isOpen() 为 True ,这个函数的含义就是当窗口 win 被打开的时候就返回真,那么一旦关闭,就会返回假,这时候程序就会结束掉,我们来运行一下。
我们看到窗口左上角有个标题,这时候我希望这个标题是中文的,可以改下这个字符串。
在这里插入图片描述

三、关闭事件

#include <SFML/Graphics.hpp>
#include <iostream>
using namespace sf;

int main() {
    RenderWindow win(VideoMode(816, 576), L"扫雷");

    while (win.isOpen()) {
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
        }
    }
    return 0;
}

四、资源加载

接下来我们准备好这么一张图片,如果不会画图,可以直接用我的这张图,用画图工具打开以后,大概是宽为 1152,高为 96 的图片,每个小格子的大小是 96 x 96。
在这里插入图片描述
1、纹理对象
首先我们创建一个纹理对象,并且把这张图加载到内存中,纹理是游戏开发中一个比较重要的概念,可以理解成贴图, 2D游戏中,不同的对象,让人能够产生不同的视觉效果,就是利用不同的纹理实现的。

Texture t;
t.loadFromFile("mine.png");

2、精灵
然后我们再实例化一个精灵,并且把刚才准备好的纹理对象,作为初始化参数,传给它。精灵可以这么去理解,我拿到一个纹理的某一个矩形区域,然后可以对它进行平移、缩放、旋转 等等变换,然后绘制到屏幕上的这么一个东西,我们叫它精灵。

Sprite s(t);

在原先这张纹理贴图上,(96, 0) 的坐标上,取出一个 (96, 96) 的矩形,并且设置坐标为 (16, 16),然后把它的缩放值设置为原来的 1/2 ,这样就变成了一个 48 x 48 的矩形,然后调用 draw 接口绘制到 win 对象上面去,这时候其实屏幕上还没有东西,直到调用 display 以后,才会真正把它绘制到窗口上。
可以这么去理解,draw 调用完,实际上还没有真正的绘制到窗口上,只是把要画的内容,画到了一张画布上面,display 调用完,才会最终把这张画布的内容,一次性绘制到你的窗口上。

 s.setTextureRect(IntRect(96, 0, 96, 96));
    s.setPosition(16, 16);
    s.setScale(Vector2f(0.5, 0.5));
    win.draw(s);
    win.display();

在这里插入图片描述
接下来我们来写这么一段话:

int r = rand() % 12;
s.setTextureRect(IntRect(96 * r, 0, 96, 96));

随机一个 0 到 11 的数字,然后让它乘上 96 ,去改变这个纹理矩形左上角的 x 坐标,来看看效果,你会发现每一帧,都在改变图片,而实际上 0 到 11 就代表了扫雷这个游戏中,每个会用到的格子。

五、初始地图

定义一个 showGrid 的二维数组,是一个 15 列 x 10 行 的地图,代表实际显示出来的地图元素,一开始都为 10, 10 就是这个图片,代表的是一个未知的元素。

int showGrid[16][11];
for (int i = 1; i <= 15; ++i) {
    for (int j = 1; j <= 10; ++j) {
        showGrid[i][j] = 10;
    }
}

然后在绘制的时候,遍历每一个地图元素,处理精灵的纹理、位置以及缩放,并且绘制到 win 这个对象上。

for (int i = 1; i <= 15; ++i) {
    for (int j = 1; j <= 10; ++j) {
        s.setTextureRect(IntRect(96 * showGrid[i][j], 0, 96, 96));
        s.setPosition(i * 48, j * 48);
        s.setScale(Vector2f(0.5, 0.5));
        win.draw(s);
    }
}

最终一次性展现到窗口上,运行。这样我们就得到了一张初始的地图。

在这里插入图片描述

六、常量定义

这个时候我们发现,有太多数字了,这个我们叫它们 magic number,很难看,而且维护起来极其麻烦,所以我们想办法把数字变成常量。
首先引入第一个常量:

const int ORI_GRID_SIZE = 96;

它代表了在这张图片中,每个格子的像素大小,是 96。

const int GRID_SIZE = 48;

而 GRID_SIZE 呢,则代表显示到窗口的时候,每个格子实际的像素大小。
然后定义 MAP_COL 和 MAP_ROW ,分别代表这个扫雷地图,有多少列多少行:

const int MAP_COL = 15;
const int MAP_ROW = 10;

然后把之前 15 和 10 的地方,都替换掉(注意下面有个 10 是不能替换,因为含义不同):

int showGrid[MAP_COL+1][MAP_ROW+1];
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        showGrid[i][j] = 10;      // 这个10可不是 MAP_ROW
    }
}

遍历每个格子,进行渲染:

s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
for (int i = 1; i <= MAP_COL; ++i) {
    for (int j = 1; j <= MAP_ROW; ++j) {
        s.setTextureRect(IntRect(ORI_GRID_SIZE * showGrid[i][j], 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
        s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
        win.draw(s);
    }
}

而对于整个窗口大小,可以是 格子数 乘上 (列数 + 2),左边加一列格子,右边加一列格子,上下也是一样的,所以窗口大小可以定义成这样的常量:

const int WIN_W = GRID_SIZE * (1 + MAP_COL + 1);
const int WIN_H = GRID_SIZE * (1 + MAP_ROW + 1);

最后,showGrid 里面还有一个 10,这个我们可以用枚举来实现:

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

七、地图随机

showGrid 代表的是显示出来的格子类型,所以再定义一个 grid,代表真实的格子类型,并且利用随机函数,1/6 的概率是炸弹,5/6的概率是空。

 GridType grid[MAP_COL + 1][MAP_ROW + 1];
    GridType showGrid[MAP_COL+1][MAP_ROW+1];
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            showGrid[i][j] = GridType::GT_HIDE;
            if (rand() % 6 == 0) {
                grid[i][j] = GridType::GT_BOMB;
            }
            else {
                grid[i][j] = GridType::GT_EMPTY;
            }
        }
    }

定义周围的八个方向

const int DIR[8][2] = {
    {-1, -1}, {-1, 0}, {-1, 1},
    {0, -1}, {0, 1},
    {1, -1}, {1, 0}, {1, 1},
};

并且统计每个非炸弹的格子的周围八个方向,进行计数,从而改变当前格子的类型

 for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j] == GridType::GT_EMPTY) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj] == GridType::GT_BOMB) {
                        ++cnt;
                    }
                }
                grid[i][j] = (GridType)cnt;
            }
        }
    }

然后只需要在显示之前,把所有的实际格子内容,赋值给对应的显示格子,就相当于摊牌了。运行一下看看效果。

showGrid[i][j] = grid[i][j];

在这里插入图片描述

八、点击排雷

获取鼠标点击到的格子位置

Vector2i pos = Mouse::getPosition(win);
int x = pos.x / GRID_SIZE;
int y = pos.y / GRID_SIZE;

并且处理鼠标左键 和 鼠标右键 的 按下事件

if (e.type == Event::MouseButtonPressed) {
    if (e.key.code == Mouse::Left) {
        showGrid[x][y] = grid[x][y];
    }
    else if (e.key.code == Mouse::Right) {
        showGrid[x][y] = GridType::GT_FLAG;
    }
}

最后,如果当前的格子被确认是雷,那么所有格子都公开,游戏结束:

 if( showGrid[x][y] == GridType::GT_BOMB) 
                showGrid[i][j] = grid[i][j];

九、格子类化

接下来我们采用面向对象的思想,来改造下这个代码,首先是一个格子,目前用了两个数据来存储,一个是实际的格子类型,一个是显示的格子类型,我现在可以把它封装到一个类里面,定义两个私有成员变量。
分别用 m_realGridType 和 m_showGridType 来表示。
然后实现它们的 set 和 get 成员函数。并且把相关代码也进行替换。

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType realGType) {
        m_showGridType = realGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_showGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

改造下初始化地图的代码:

 Grid grid[MAP_COL + 1][MAP_ROW + 1];
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) {
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            }
        }
    }

改造下鼠标按键的代码:

  if (e.key.code == Mouse::Left) {
                grid[x][y].ShowGrid();
            }
            else if (e.key.code == Mouse::Right) {
                grid[x][y].SetShowGridType(GridType::GT_FLAG);
            }

改造下渲染的代码:

  if( grid[x][y].IsShowBomb()) 
                grid[i][j].ShowGrid();
            s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));

十、 地图类化

除了把格子用类来实现,整个地图也可以用类来实现。

class Map {
public:
    void init() {
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                grid[i][j].SetShowGridType(GridType::GT_HIDE);
                if (rand() % 6 == 0) {
                    grid[i][j].SetRealGridType(GridType::GT_BOMB);
                }
                else {
                    grid[i][j].SetRealGridType(GridType::GT_EMPTY);
                }
            }
        }
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[i][j].IsEmpty()) {
                    int cnt = 0;
                    for (int k = 0; k < 8; ++k) {
                        int ti = i + DIR[k][0];
                        int tj = j + DIR[k][1];
                        if (grid[ti][tj].IsRealBomb()) {
                            ++cnt;
                        }
                    }
                    if (cnt > 0) {
                        grid[i][j].SetRealGridType((GridType)cnt);
                    }
                }
            }
        }
    }

    void handleMouseEvent(Event& e, int x, int y) {
        if (e.type == Event::MouseButtonPressed) {
            if (e.key.code == Mouse::Left) {
                grid[x][y].ShowGrid();
            }
            else if (e.key.code == Mouse::Right) {
                grid[x][y].SetShowGridType(GridType::GT_FLAG);
            }
        }
    }

    void draw(RenderWindow& win, Sprite& s, int x, int y) {
        s.setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
        for (int i = 1; i <= MAP_COL; ++i) {
            for (int j = 1; j <= MAP_ROW; ++j) {
                if (grid[x][y].IsShowBomb())
                    grid[i][j].ShowGrid();
                s.setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
                s.setPosition(i * GRID_SIZE, j * GRID_SIZE);
                win.draw(s);
            }
        }
    }

private:
    Grid grid[MAP_COL + 1][MAP_ROW + 1];
};
int main() {
    RenderWindow win(VideoMode(WIN_W, WIN_H), L"扫雷");
    Texture t;
    t.loadFromFile("mine.png");
    Sprite s(t);
    Map mp;
    mp.init();
    while (win.isOpen()) {
        Vector2i pos = Mouse::getPosition(win);
        int x = pos.x / GRID_SIZE;
        int y = pos.y / GRID_SIZE;
        Event e;
        while (win.pollEvent(e)) {
            if (e.type == Event::Closed) {
                std::cout << "按下关闭按钮" << std::endl;
                win.close();
            }
            mp.handleMouseEvent(e, x, y);
        }
        mp.draw(win, s, x, y);
        win.display();
    }
    return 0;
}

十一、 接口优化

接下来我们来看,这个地图类对外提供的接口,只有三个了。一个是初始化,一个是处理事件,一个是渲染,并且渲染接口每次都把 窗口 和 精灵 传进去,实际上是没有必要的,因为在这个大循环里,这两个对象,是不变的。
所以这两个对象,实际上,可以作为 Map 类的成员变量,当然这里必须用指针。如果不传指针,就会通过拷贝构造函数,生成一个新的对象,这不是我们想要的,我需要它还是原来那个对象。

private:
    RenderWindow*    win;
    Sprite*          sprite;
    Grid             grid[MAP_COL + 1][MAP_ROW + 1];
};

然后修改 Map 类的初始化函数,如下:

void init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;
    ...
}


mp.init(&win, &s);

这里记住要传指针,所以把 win 和 s 的地址传进去就好了。最后修改 draw 函数(因为是指针,所以所有的 . 变成 -> 就好了):

void draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[x][y].IsShowBomb()) {
                grid[i][j].ShowGrid();
            }
            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}

sprite 作为 RenderWindow 的成员函数 draw 的参数,是一个对象,所以采用 * 进行解引用。

十二、 文件拆分

这个时候我们发现这个文件行数太多了,所以我们想办法把类拆到其它文件去。首先,先把 Grid 类的内容拆出去,新建一个 Grid.h 文件,实现如下:

#pragma once

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid() {
        m_realGridType = GridType::GT_EMPTY;
        m_showGridType = GridType::GT_EMPTY;
    }
    void SetRealGridType(GridType realGType) {
        m_realGridType = realGType;
    }
    void SetShowGridType(GridType realGType) {
        m_showGridType = realGType;
    }
    GridType GetShowGridType() {
        return m_showGridType;
    }
    void ShowGrid() {
        m_showGridType = m_realGridType;
    }
    bool IsEmpty() const {
        return m_realGridType == GridType::GT_EMPTY;
    }
    bool IsRealBomb() const {
        return m_realGridType == GridType::GT_BOMB;
    }
    bool IsShowBomb() const {
        return m_showGridType == GridType::GT_BOMB;
    }
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

然后在 main.cpp 里面,写上这么一句话:
#include “Grid.h”
这时候,我们希望 .h 文件里面不要有函数的实现,只保留声明。然后在源文件里,新建一个 Grid.cpp 文件,把函数的实现写在这个文件里。

#pragma once

enum GridType {
    GT_EMPTY = 0,
    GT_COUNT_1 = 1,
    GT_COUNT_2 = 2,
    GT_COUNT_3 = 3,
    GT_COUNT_4 = 4,
    GT_COUNT_5 = 5,
    GT_COUNT_6 = 6,
    GT_COUNT_7 = 7,
    GT_COUNT_8 = 8,
    GT_BOMB = 9,
    GT_HIDE = 10,
    GT_FLAG = 11
};

class Grid {
public:
    Grid();
    void SetRealGridType(GridType realGType);
    void SetShowGridType(GridType realGType);
    GridType GetShowGridType();
    void ShowGrid();
    bool IsEmpty() const;
    bool IsRealBomb() const;
    bool IsShowBomb() const;
private:
    GridType m_realGridType;
    GridType m_showGridType;
};

Grid.cpp 如下:

#include "Grid.h"

Grid::Grid() {
    m_realGridType = GridType::GT_EMPTY;
    m_showGridType = GridType::GT_EMPTY;
}

void Grid::SetRealGridType(GridType realGType) {
    m_realGridType = realGType;
}
void Grid::SetShowGridType(GridType realGType) {
    m_showGridType = realGType;
}
GridType Grid::GetShowGridType() {
    return m_showGridType;
}
void Grid::ShowGrid() {
    m_showGridType = m_realGridType;
}
bool Grid::IsEmpty() const {
    return m_realGridType == GridType::GT_EMPTY;
}
bool Grid::IsRealBomb() const {
    return m_realGridType == GridType::GT_BOMB;
}
bool Grid::IsShowBomb() const {
    return m_showGridType == GridType::GT_BOMB;
}

同样,在实现一个 Map.h 和 Map.cpp。

#pragma once

#include <SFML/Graphics.hpp>
#include "Grid.h"
using namespace sf;

class Map {
public:
    void init(RenderWindow* win, Sprite* sprite);
    void handleMouseEvent(Event& e, int x, int y);
    void draw(int x, int y);

private:
    RenderWindow* win;
    Sprite*    sprite;
    Grid      grid[MAP_COL + 1][MAP_ROW + 1];
};

#include "Map.h"
#include "Grid.h"


void Map::init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;

    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) {
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            }
        }
    }
}

void Map::handleMouseEvent(Event& e, int x, int y) {
    if (e.type == Event::MouseButtonPressed) {
        if (e.key.code == Mouse::Left) {
            grid[x][y].ShowGrid();
        }
        else if (e.key.code == Mouse::Right) {
            grid[x][y].SetShowGridType(GridType::GT_FLAG);
        }
    }
}

void Map::draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[x][y].IsShowBomb()) {
                grid[i][j].ShowGrid();
            }
            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}

十三、游戏重开

然后一个游戏结束以后,我们希望它能够重开,在 Map 中,引入一个私有成员变量 isRunning,并且引入一个 initGame 的函数,围绕 isRuning 进行逻辑修改。
一旦按到一个雷,那么 isRuning 就从 true 变成 false,然后左键按下的时候,根据 isRunning 是 true 还是 false 做不同的处理,如果为 true,则保留原先的逻辑;如果为 false,则重新初始化游戏,开始新的一局。

#include "Map.h"
#include "Grid.h"

void Map::init(RenderWindow* win, Sprite* sprite) {
    this->win = win;
    this->sprite = sprite;    
    initGame();
}

void Map::initGame() {
    this->isRunning = true;
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            grid[i][j].SetShowGridType(GridType::GT_HIDE);
            if (rand() % 6 == 0) {
                grid[i][j].SetRealGridType(GridType::GT_BOMB);
            }
            else {
                grid[i][j].SetRealGridType(GridType::GT_EMPTY);
            }
        }
    }
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (grid[i][j].IsEmpty()) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int ti = i + DIR[k][0];
                    int tj = j + DIR[k][1];
                    if (grid[ti][tj].IsRealBomb()) {
                        ++cnt;
                    }
                }
                if (cnt > 0) {
                    grid[i][j].SetRealGridType((GridType)cnt);
                }
            }
        }
    }
}

void Map::handleMouseEvent(Event& e, int x, int y) {
    if (e.type == Event::MouseButtonPressed) {
        if (e.key.code == Mouse::Left) {
            if (isRunning) {
                grid[x][y].ShowGrid();
                if (grid[x][y].IsShowBomb()) {
                    isRunning = false;
                }
            }
            else {
                initGame();
            }
        }
        else if (e.key.code == Mouse::Right) {
            grid[x][y].SetShowGridType(GridType::GT_FLAG);
        }
    }
}

void Map::draw(int x, int y) {
    sprite->setScale(Vector2f(GRID_SIZE * 1.0 / ORI_GRID_SIZE, GRID_SIZE * 1.0 / ORI_GRID_SIZE));
    for (int i = 1; i <= MAP_COL; ++i) {
        for (int j = 1; j <= MAP_ROW; ++j) {
            if (!isRunning) {
                grid[i][j].ShowGrid();
            }
            sprite->setTextureRect(IntRect(ORI_GRID_SIZE * grid[i][j].GetShowGridType(), 0, ORI_GRID_SIZE, ORI_GRID_SIZE));
            sprite->setPosition(i * GRID_SIZE, j * GRID_SIZE);
            win->draw(*sprite);
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/959681.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【动态规划】落花人独立,微雨燕双飞 - 8. 01背包问题

本篇博客给大家带来的是01背包问题之动态规划解法技巧. &#x1f40e;文章专栏: 动态规划 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; 要开心要快乐顺便…

游戏steam_api64.dll文件缺失怎么办?无法找到指定的模块的解决方法

在使用Steam平台运行游戏时&#xff0c;有时会遇到“steam_api64.dll文件缺失&#xff0c;无法找到指定的模块”的错误提示。这个问题通常是由于该文件被误删、病毒感染、系统更新不兼容或游戏安装不完整等原因造成的。以下是一些有效的解决方法&#xff0c;帮助你解决steam_ap…

Linux学习笔记——网络管理命令

一、网络基础知识 TCP/IP四层模型 以太网地址&#xff08;MAC地址&#xff09;&#xff1a; 段16进制数据 IP地址&#xff1a; 子网掩码&#xff1a; 二、接口管命令 ip命令&#xff1a;字符终端&#xff0c;立即生效&#xff0c;重启配置会丢失 nmcli命令&#xff1a;字符…

在 Windows 系统上,将 Ubuntu 从 C 盘 迁移到 D 盘

在 Windows 系统上&#xff0c;如果你使用的是 WSL&#xff08;Windows Subsystem for Linux&#xff09;并安装了 Ubuntu&#xff0c;你可以将 Ubuntu 从 C 盘 迁移到 D 盘。迁移过程涉及导出当前的 Ubuntu 发行版&#xff0c;然后将其导入到 D 盘的目标目录。以下是详细的步骤…

simulink入门学习01

文章目录 1.基本学习方法2.图形环境--模块和参数3.激活菜单---添加到模型3.1输入选项3.2添加到模型3.3更改运算3.4验证要求 4.乘以特定值--Gain模块4.1引入gain模块4.2更改增益参数4.3接入系统4.4大胆尝试 1.基本学习方法 今天突然想要学习这个simulink的相关知识&#xff0c;…

Linux的基本指令(上)

1.ls指令 语法&#xff1a;ls [选项] [目录或文件] 功能&#xff1a;对于⽬录&#xff0c;该命令列出该⽬录下的所有⼦⽬录与⽂件。对于⽂件&#xff0c;将列出⽂件名以及其他信息。 常用选项&#xff1a; -a 列出⽬录下的所有⽂件&#xff0c;包括以 . 开头的隐含⽂件。 -d 将…

一文详解Filter类源码和应用

背景 在日常开发中&#xff0c;经常会有需要统一对请求做一些处理&#xff0c;常见的比如记录日志、权限安全控制、响应处理等。此时&#xff0c;ServletApi中的Filter类&#xff0c;就可以很方便的实现上述效果。 Filter类 是一个接口&#xff0c;属于 Java Servlet API 的一部…

【算法】数论基础——唯一分解定理(算术基本定理)python

目录 定义进入正题热身训练实战演练扩展衍生判断一个数是否为完全平方数举一反三总结 定义 唯一分解定理&#xff1a;也叫做算数基本定理: 任意一个大于1的整数N&#xff0c;都可以唯一分解为若干个质数的乘积 换句话说&#xff0c;任何大于1的整数n可以表示为&#xff1a; 例如…

互联网医院成品|互联网医院开发|互联网医院搭建

在数字化医疗蓬勃发展的当下&#xff0c;互联网医院系统已成为医疗服务体系中至关重要的组成部分。它打破了传统医疗服务在时间和空间上的限制&#xff0c;为患者提供了更加便捷、高效的医疗服务。而一套完善的互联网医院系统&#xff0c;有几个功能是不能缺少的。 在线问诊功能…

Go的内存逃逸

Go的内存逃逸 内存逃逸是 Go 语言中一个重要的概念&#xff0c;指的是本应分配在栈上的变量被分配到了堆上。栈上的变量在函数结束后会自动回收&#xff0c;而堆上的变量需要通过垃圾回收&#xff08;GC&#xff09;来管理&#xff0c;因此内存逃逸会增加 GC 的压力&#xff0…

填充每个节点的下一个右侧节点指针力扣--116,117

目录 题目 思路 代码 题目 116 给定一个 完美二叉树 &#xff0c;其所有叶子节点都在同一层&#xff0c;每个父节点都有两个子节点。二叉树定义如下&#xff1a; struct Node { int val; Node *left; Node *right; Node *next; } 填充它的每个 next 指针&#xff0c…

钉钉群机器人设置——python版本

钉钉群机器人设置——python版本 应用场景钉钉界面操作程序开发效果展示 应用场景 由于工作需要&#xff0c;很多项目执行程序后出现报错信息无法第一时间收到&#xff0c;因此实时预警对于监控程序还是有必要。&#xff08;仅个人观点&#xff09; 参考文档及博客&#xff1a…

初步认识操作系统(Operator System)

目录 一、概念二、设计OS的目的三、定位四、操作系统上下的分级五、如何理解 "管理"六、总结 一、概念 任何计算机系统都包含一个基本的程序集合&#xff0c;称为操作系统(OS)。操作系统包括&#xff1a; 内核&#xff08;进程管理&#xff0c;内存管理&#xff0c…

文明6mod发布并开源:更多的蛮族营地扫荡收益mod

更多的蛮族营地扫荡收益mod&#xff08;More_Barbarian_Camp_RAID_luke&#xff09; 效果为&#xff1a; 更多的蛮族营地扫荡收益&#xff0c;增加到100金币&#xff0c;适用于野蛮氏族模式 原版本的扫荡收益非常鸡肋~&#xff01; mod下载链接&#xff1a; https://downlo…

社区养老服务平台的设计与实现(代码+数据库+LW)

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#…

websocket实现

由于安卓资源管理器展示的路径不尽相同,各种软件保存文件的位置也不一定一样.对于普通用户上传文件时,查找文件可能是一个麻烦的事情.后来想到了一个办法,使用pc端进行辅助上传. 文章目录 实现思路1.0 实现定义web与客户端通信数据类型和数据格式web端websocket实现web端对客户…

(一)HTTP协议 :请求与响应

前言 爬虫需要基础知识&#xff0c;HTTP协议只是个开始&#xff0c;除此之外还有很多&#xff0c;我们慢慢来记录。 今天的HTTP协议&#xff0c;会有助于我们更好的了解网络。 一、什么是HTTP协议 &#xff08;1&#xff09;定义 HTTP&#xff08;超文本传输协议&#xff…

arcgis短整型变为长整型的处理方式

1.用QGIS的重构字段工具进行修改&#xff0c;亲测比arcgis的更改字段工具有用 2.更换低版本的arcgis10.2.2&#xff0c;亲测10.5和10.6都有这个毛病&#xff0c;虽然官方文档里面说的是10.6.1及以上 Arcgis10.2.2百度链接&#xff1a;https://pan.baidu.com/s/1HYTwgnBJsBug…

从音频到 PDF:AI 全流程打造完美英文绘本教案

今天把英文绘本的自学教案自动生成流程完成了&#xff0c;我分享一下整个实现思路&#xff0c;让你也轻松搞定英文绘本教案的产出&#xff0c;让孩子的学习之路更加顺畅。  从音频到 PDF&#xff1a;AI 全流程打造完美英文绘本教案 一、音频转文本&#xff1a;AI 助力第一步 …

C++ Qt练习项目 日期时间数据

个人学习笔记 代码仓库 GitCode - 全球开发者的开源社区,开源代码托管平台 新建项目 设计UI 实现组件功能 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }…