基础项目——扫雷(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…...

正则表达式中常见的贪婪词
1. * 含义:匹配前面的元素零次或者多次。示例:对于正则表达式 a*,在字符串 "aaaa" 中,它会匹配整个 "aaaa",因为它会尽可能多地匹配 a 字符。代码示例(Python):…...

CF 339A.Helpful Maths(Java实现)
题目分析 输入一串式子,输出从小到大排列的式子 思路分析 如上所说核心思路,但是我要使用笨方法,输入一串式子用split分割开,但是此时需要用到转义字符,即函数内参数不能直接使用“”,而是“\\”。分割开后…...

SQL 指南
SQL 指南 引言 SQL(Structured Query Language,结构化查询语言)是一种用于管理关系数据库系统的标准计算机语言。自1970年代问世以来,SQL已经成为了数据库管理和数据操作的事实标准。本文旨在为初学者和有经验的数据库用户提供一个全面的SQL指南,涵盖SQL的基础知识、高级…...

DDD架构实战第七讲总结:分层模型和代码组织
云架构师系列课程之DDD架构实战第七讲总结:分层模型和代码组织 一、引言 在前几讲中,我们介绍了领域驱动设计(DDD)的基本构造块和生命周期模型中的聚合。本讲将重点讨论如何将这些构造块和代码组织起来,探讨分层架构和六边形模型,以及如何组织代码结构。 二、工厂和资…...

Python “字典” 实战案例:5个项目开发实例
Python “字典” 实战案例:5个项目开发实例 内容摘要 本文包括 5 个使用 Python 字典的综合应用实例。具体是: 电影推荐系统配置文件解析器选票统计与排序电话黄页管理系统缓存系统(LRU 缓存) 以上每一个实例均有完整的程序代…...

(一)QT的简介与环境配置WIN11
目录 一、QT的概述 二、QT的下载 三、简单编程 常用快捷键 一、QT的概述 简介 Qt(发音:[kjuːt],类似“cute”)是一个跨平台的开发库,主要用于开发图形用户界面(GUI)应用程序,…...

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

vue2的$el.querySelector在vue3中怎么写
这个也属于直接操作 dom 了,不建议在项目中这样操作,不过我是在vue2升级vue3的时候遇到的,是以前同事写的代码,也没办法 先来看一下对比 在vue2中获取实例是直接通过 this.$refs.xxx 获取绑定属性 refxxx 的实例,并且…...

GPSd定时检测保活TCP GPS源
为了在 TCP GPS 源丢失连接时自动重新连接,可以编写一个监控脚本,定期检查 gpspipe 输出中的 TCP 源数据是否存在。如果检测到丢失,则使用 gpsdctl 或直接命令重新添加 TCP 源。 1、工具 检查并安装必要工具,本例需要使用 gpspi…...

IDEA中Maven使用的踩坑与最佳实践
文章目录 IDEA中Maven使用的踩坑与最佳实践一、环境配置类问题1. Maven环境配置2. IDEA中Maven配置建议 二、常见问题与解决方案1. 依赖下载失败2. 依赖冲突解决3. 编译问题修复 三、效率提升技巧1. IDEA Maven Helper插件使用2. 常用Maven命令配置3. 多模块项目配置4. 资源文件…...