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

news2025/1/27 8:58:12

目录

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

前言

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

一、环境配置

首先要下载 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);
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[操作系统] 深入进程地址空间

程序地址空间回顾 在C语言学习的时&#xff0c;对程序的函数、变量、代码等数据的存储有一个大致的轮廓。在语言层面上存储的地方叫做程序地址空间&#xff0c;不同类型的数据有着不同的存储地址。 下图为程序地址空间的存储分布和和特性&#xff1a; 使用以下代码来验证一下…

OpenCV:图像处理中的低通滤波

目录 简述 什么是低通滤波&#xff1f; 各种滤波器简介与实现 方盒滤波 均值滤波 中值滤波 高斯滤波 双边滤波 各种滤波的对比与应用场景 相关阅读 OpenCV基础&#xff1a;图像变换-CSDN博客 OpenCV&#xff1a;图像滤波、卷积与卷积核-CSDN博客 简述 低通滤波是一…

32、【OS】【Nuttx】OSTest分析(1):stdio测试(二)

背景 接上篇wiki 31、【OS】【Nuttx】OSTest分析&#xff08;1&#xff09;&#xff1a;stdio测试&#xff08;一&#xff09; 继续stdio测试的分析&#xff0c;上篇讲到标准IO端口初始化&#xff0c;单从测试内容来说其实很简单&#xff0c;没啥可分析的&#xff0c;但这几篇…

OpenAI掀桌子!免费版ChatGPT,提供o3-mini模型!

逆天免费用 今天凌晨&#xff0c;OpenAI联合创始人兼首席执行官Sam Altman宣布了一个大消息——免费版ChatGPT&#xff0c;将提供o3-mini模型&#xff01; 网页们纷纷不淡定了 看来OpenAI&#xff0c;这o3-mini还没正式上线呢&#xff0c;就免费开放使用了。 不过还是要感谢…

redis离线安装部署详解(包括一键启动)

像上文一样 因为在学习的过程中没有查到一个详细的离线部署方案 所以在自己学习之后想要自己写一个文章 希望可以帮助后续学习redis离线部署的朋友少走一线弯路 首先就是下载安装包 可以自己在本地下载再传到机器上&#xff08;通过xftp或lrzsz都可&#xff09; http://d…

图论汇总1

1.图论理论基础 图的基本概念 二维坐标中&#xff0c;两点可以连成线&#xff0c;多个点连成的线就构成了图。 当然图也可以就一个节点&#xff0c;甚至没有节点&#xff08;空图&#xff09; 图的种类 整体上一般分为 有向图 和 无向图。 有向图是指 图中边是有方向的&a…

小利特惠源码/生活缴费/电话费/油卡燃气/等充值业务类源码附带承兑系统

全新首发小利特惠/生活缴费/电话费/油卡燃气/等充值业务类源码附带U商承兑系统 安装教程如下 图片:

ESMC-600M蛋白质语言模型本地部署攻略

前言 之前介绍了ESMC-6B模型的网络接口调用方法&#xff0c;但申请token比较慢&#xff0c;有网友问能不能出一个本地部署ESMC小模型的攻略&#xff0c;遂有本文。 其实本地部署并不复杂&#xff0c;官方github上面也比较清楚了。 操作过程 环境配置&#xff1a;CUDA 12.1、…

Java 实现Excel转HTML、或HTML转Excel

Excel是一种电子表格格式&#xff0c;广泛用于数据处理和分析&#xff0c;而HTM则是一种用于创建网页的标记语言。虽然两者在用途上存在差异&#xff0c;但有时我们需要将数据从一种格式转换为另一种格式&#xff0c;以便更好地利用和展示数据。本文将介绍如何通过 Java 实现 E…

Ubuntu20.04 运行 PL-VIO

文章目录 运行后不知为何没有线特征 运行后不知为何没有线特征

centos操作系统上以service形式运行blackbox_exporter监控网页端口

文章目录 前言一、blackbox_exporter是什么二、使用步骤1.获取二进制文件2.准备部署脚本3.执行命令&#xff0c;进行部署4.prometheus中增加需要监控页面的job信息 三、查看部署结果四、配置到grafana中总结 前言 记录一下centos操作系统上以简单的service形式运行blackbox_ex…

Linux内核编程(二十一)USB驱动开发-键盘驱动

一、驱动类型 USB 驱动开发主要分为两种&#xff1a;主机侧的驱动程序和设备侧的驱动程序。一般我们编写的都是主机侧的USB驱动程序。 主机侧驱动程序用于控制插入到主机中的 USB 设备&#xff0c;而设备侧驱动程序则负责控制 USB 设备如何与主机通信。由于设备侧驱动程序通常与…

RV1126画面质量四:GOP改善画质

一&#xff0e; 什么是 GOP GOP 实际上就是两个 I 帧的间隔&#xff0c;比方说分辨率是 1920 * 1080 50 帧&#xff0c;假设 GOP 为 5&#xff0c;那就是大概 2s 插入一个 I 帧。我们再 回顾下&#xff0c;H264/H265 的帧结构。H264/H265 分别分为三种帧类型&#xff1a;I 帧、…

【2025年数学建模美赛F题】(顶刊论文绘图)模型代码+论文

全球网络犯罪与网络安全政策的多维度分析及效能评估 摘要1 Introduction1.1 Problem Background1.2Restatement of the Problem1.3 Literature Review1.4 Our Work 2 Assumptions and Justifications数据完整性与可靠性假设&#xff1a;法律政策独立性假设&#xff1a;人口统计…

Vivado生成X1或X4位宽mcs文件并固化到flash

1.生成mcs文件 01.在vivado里的菜单栏选择"tools"工具栏 02.在"tools"里选择"生成内存配置文件" 03.配置参数 按照FPGA板上的flash型号进行选型&#xff0c;相关配置步骤可参考下图。 注意&#xff1a;Flash数据传输位宽如果需要选择X4位宽&am…

idea plugin插件开发——入门级教程(IntelliJ IDEA Plugin)

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;idea plugin插件开发——入门级教程&#xff08;IntelliJ IDEA Plugin&#xff09;-CSDN博客 目录 前言 官方 官方文档 代码示例 开发前必读 Intellij、Gradle、JDK 版本关系 plu…

Linux的常用指令的用法

目录 Linux下基本指令 whoami ls指令&#xff1a; 文件&#xff1a; touch clear pwd cd mkdir rmdir指令 && rm 指令 man指令 cp mv cat more less head tail 管道和重定向 1. 重定向&#xff08;Redirection&#xff09; 2. 管道&#xff08;Pipes&a…

docker 简要笔记

文章目录 一、前提内容1、docker 环境准备2、docker-compose 环境准备3、流程说明 二、打包 docker 镜像1、基础镜像2、国内镜像源3、基础的dockerfile4、打包镜像 四、构建运行1、docker 部分2、docker-compose 部分2.1、构建docker-compose.yml2.1.1、同目录构建2.1.2、利用镜…

Windows 与 Linux 文件权限的对比与转换

Windows和Linux在文件权限管理方面存在显著差异。了解它们的对比和转换方法对于跨平台操作和管理文件非常重要。以下是详细的对比和转换方法&#xff1a; 一、Windows 文件权限 1. 权限类型 Windows使用基于用户和组的权限模型&#xff0c;常见的权限类型包括&#xff1a; 读…

FireFox | Google Chrome | Microsoft Edge 禁用更新 final版

之前的方式要么失效&#xff0c;要么对设备有要求&#xff0c;这次梳理一下对设备、环境几乎没有要求的通用方式&#xff0c;universal & final 版。 1.Firefox 方式 FireFox火狐浏览器企业策略禁止更新_火狐浏览器禁止更新-CSDN博客 这应该是目前最好用的方式。火狐也…