基础项目——扫雷(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 onceenum 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 onceenum 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);}}
}
相关文章:
基础项目——扫雷(c++)
目录 前言一、环境配置二、基础框架三、关闭事件四、资源加载五、初始地图六、常量定义七、地图随机八、点击排雷九、格子类化十、 地图类化十一、 接口优化十二、 文件拆分十三、游戏重开 前言 各位小伙伴们,这期我们一起学习出贪吃蛇以外另一个基础的项目——扫雷…...
docker安装elk6.7.1-搜集java日志
docker安装elk6.7.1-搜集java日志 如果对运维课程感兴趣,可以在b站上、A站或csdn上搜索我的账号: 运维实战课程,可以关注我,学习更多免费的运维实战技术视频 0.规划 192.168.171.130 tomcat日志filebeat 192.168.171.131 …...
自然语言处理(NLP)入门:基础概念与应用场景
什么是自然语言处理(NLP)? 自然语言处理(Natural Language Processing, NLP)是人工智能(AI)的一个重要分支,研究如何让计算机理解、生成、分析和与人类语言进行交互。换句话说&…...
AI News(1/21/2025):OpenAI 安全疏忽:ChatGPT漏洞引发DDoS风险/OpenAI 代理工具即将发布
1、OpenAI 的安全疏忽:ChatGPT API 漏洞引发DDoS风险 德国安全研究员 Benjamin Flesch 发现了一个严重的安全漏洞:攻击者可以通过向 ChatGPT API 发送一个 HTTP 请求,利用 ChatGPT 的爬虫对目标网站发起 DDoS 攻击。该漏洞源于 OpenAI 在处理…...
Linux——包源管理工具
一、概要 Linux下的包/源管理命令:主要任务就是完成在Linux环境下的安装/卸载/维护软件。 1.rpm 是最基础的rpm包的安装命令,需要提前下载相关安装包和依赖包。 2.yum/dnf (最好用)是基于rpm包的自动安装命令,可以自动…...
C++解决走迷宫问题:DFS、BFS算法应用
文章目录 思路:DFSBFSBFS和DFS的特点BFS 与 DFS 的区别BFS 的优点BFS 时间复杂度深度优先搜索(DFS)的优点深度优先搜索(DFS)的时间复杂度解释:空间复杂度总结:例如下面的迷宫: // 迷宫的表示:0表示可以走,1表示障碍 vector<vector<int>> maze = {{0, 0,…...
机器学习09-Pytorch功能拆解
机器学习09-Pytorch功能拆解 我个人是Java程序员,关于Python代码的使用过程中的相关代码事项,在此进行记录 文章目录 机器学习09-Pytorch功能拆解1-核心逻辑脉络2-个人备注3-Pytorch软件包拆解1-Python有参和无参构造构造方法的基本语法示例解释注意事项…...
BLE透传方案,IoT短距无线通信的“中坚力量”
在物联网(IoT)短距无线通信生态系统中,低功耗蓝牙(BLE)数据透传是一种无需任何网络或基础设施即可完成双向通信的技术。其主要通过简单操作串口的方式进行无线数据传输,最高能满足2Mbps的数据传输速率&…...
Linux 中的poll、select和epoll有什么区别?
poll 和 select 是Linux 系统中用于多路复用 I/O 的系统调用,它们允许一个程序同时监视多个文件描述符,以便在任何一个文件描述符准备好进行 I/O 操作时得到通知。 一、select select 是一种较早的 I/O 多路复用机制,具有以下特点ÿ…...
单片机-STM32 WIFI模块--ESP8266 (十二)
1.WIFI模块--ESP8266 名字由来: Wi-Fi这个术语被人们普遍误以为是指无线保真(Wireless Fidelity),并且即便是Wi-Fi联盟本身也经常在新闻稿和文件中使用“Wireless Fidelity”这个词,Wi-Fi还出现在ITAA的一个论文中。…...
linux日志排查相关命令
实时查看日志 tail -f -n 100 文件名 -f:实时查看 -n:查看多少行 直接查看日志文件 .log文件 cat 文件名 .gz文件 zgcat 文件名 在日志文件搜索指定内容 .log文件 grep -A 3 “呀1” 文件名 -A:向后查看 3:向后查看行数 “呀1”:搜…...
每日一题-二叉搜索树与双向链表
将二叉搜索树转化为排序双向链表 问题描述 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表,要求空间复杂度为 O(1),时间复杂度为 O(n),并且不能创建新的结点,只能调整树中结点的指针指向。 数据范围 …...
【多视图学习】Self-Weighted Contrastive Fusion for Deep Multi-View Clustering
Self-Weighted Contrastive Fusion for Deep Multi-View Clustering 用于深度多视图聚类的自加权对比融合 TMM 2024 代码链接 论文链接 0.摘要 多视图聚类可以从多个视图中探索共识信息,在过去二十年中越来越受到关注。然而,现有的工作面临两个主要挑…...
ASK-HAR:多尺度特征提取的深度学习模型
一、探索多尺度特征提取方法 在近年来,随着智能家居智能系统和传感技术的快速发展,人类活动识别(HAR)技术已经成为一个备受瞩目的研究领域。HAR技术的核心在于通过各种跟踪设备和测量手段,如传感器和摄像头࿰…...
C语言:数据的存储
本文重点: 1. 数据类型详细介绍 2. 整形在内存中的存储:原码、反码、补码 3. 大小端字节序介绍及判断 4. 浮点型在内存中的存储解析 数据类型结构的介绍: 类型的基本归类: 整型家族 浮点家族 构造类型: 指针类型&…...
深入理解动态规划(dp)--(提前要对dfs有了解)
前言:对于动态规划:该算法思维是在dfs基础上演化发展来的,所以我不想讲的是看到一个题怎样直接用动态规划来解决,而是说先用dfs搜索,一步步优化,这个过程叫做动态规划。(该文章教你怎样一步步的…...
单片机基础模块学习——数码管(二)
一、数码管模块代码 这部分包括将数码管想要显示的字符转换成对应段码的函数,另外还包括数码管显示函数 值得注意的是对于小数点和不显示部分的处理方式 由于小数点没有单独占一位,所以这里用到了两个变量i,j用于跳过小数点导致的占据其他字符显示在数…...
【大数据】机器学习----------强化学习机器学习阶段尾声
一、强化学习的基本概念 注: 圈图与折线图引用知乎博主斜杠青年 1. 任务与奖赏 任务:强化学习的目标是让智能体(agent)在一个环境(environment)中采取一系列行动(actions)以完成一个…...
flink写parquet解决timestamp时间格式字段问题
背景 Apache Parquet 是一种开源的列式数据文件格式,旨在实现高效的数据存储和检索。它提供高性能压缩和编码方案(encoding schemes)来批量处理复杂数据,并且受到许多编程语言和分析工具的支持。 在我们通过flink写入parquet文件的时候,会遇到timestamp时间格式写入的问题。…...
redis实现lamp架构缓存
redis服务器环境下mysql实现lamp架构缓存 ip角色环境192.168.242.49缓存服务器Redis2.2.7192.168.242.50mysql服务器mysql192.168.242.51web端php ***默认已安装好redis,mysql 三台服务器时间同步(非常重要) # 下载ntpdate yum -y install…...
wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
