【游戏】C++结合EasyX写扫雷(时隔半年后重写)
- 上一次写扫雷
- 这一次
- 实现思路
- 设置全局变量
- Grid类
- Grid类的成员函数
- 启动画面
- 死循环监听鼠标事件
- 全部代码
- 其他
上一次写扫雷
大约半年之前的寒假期间,我接触了EasyX这个图形库,于是试着写了一个经典小游戏——扫雷
当时大概用了三四天时间,还发布了三篇博客来记录:
【小游戏】用C++结合EasyX制作扫雷1
【小游戏】用C++结合EasyX制作扫雷2
【小游戏】用C++结合EasyX制作扫雷3
这一次
基本上全都是自己重写,没怎么借鉴之前的代码(主要可能是因为看不懂之前自己写的代码了)
编写环境仍然是VS2022,记得把项目字符集改为多字符集
基本思路和之前差不多;引用的图片文件也是一样的(图片文件资源在GitHub储存库中有);另外就是添加了一点点花里胡哨的东西,比如说开始屏幕(Splash Screen)和从命令行传入参数(这个功能还有问题)进行设置等功能
写的过程中,感觉有很多东西忘记了,比如EasyX的一些基础;再就是感觉代码写得更有逻辑了(doge
仍然难免有一些Bug
实现思路
- 设置几个全局变量
- 定义Grid类
- Grid的属性
- 方法——show 用于显示格子
- 方法——showMinesNumAround 通过EasyX在对应位置显示出格子周围的雷的数量
- 方法——onLeftButtonClick 鼠标左键单击时做出的操作
- 方法——onRightButtonClick 鼠标右键单击时做出的操作
- 方法——findMinesAround 用遍历的方式找出格子周围雷的数量
- splashScreen函数实现启动画面
- main函数
- 接受来自命令行的参数
- 初始化画布
- 加载图像文件
- 布置雷盘
- 死循环监测鼠标信息
设置全局变量
(每个变量的作用都在注释中)
// 默认常量
int gridSize = 40; // 每个格子的大小(长和宽)
int lines = 5, rows = 5; // 行数和列数
int width = rows * gridSize, height = lines * gridSize; // 窗口宽度和高度
int minesNum = 10; // 雷的数量
int normalGridsNum = lines * rows - minesNum; // 除了雷以外的格子数量
IMAGE grid, gridClicked, mine, gridFlag; // 图像
Grid类
每个属性的作用在注释中都有。
有些成员函数在创建playGround后定义,因为playGround是Grid类型的。(详见全部代码)
// 格子类
class Grid {
public:
int posX, posY; // 该格子所处的位置(单位:像素)
bool isFlag = false, isClicked = false, isMine = false;
int posLine, posRow; // 该格子所处的行和列位置(单位:格子)
int minesNumAround = 0; // 该格子周围雷的数量
void show(int posLine,int posRow) {
//显示
this->posLine = posLine;
this->posRow = posRow;
posX = (this->posRow - 1) * gridSize;
posY = (this->posLine - 1) * gridSize;
putimage(posX, posY, &grid);
}
void showMinesNumAround() {
if (minesNumAround==0)
return;
else if (minesNumAround == 1)
settextcolor(BLUE);
else if (minesNumAround == 2)
settextcolor(GREEN);
else if (minesNumAround == 3)
settextcolor(YELLOW);
else if (minesNumAround == 4)
settextcolor(RGB(255, 135, 35));
else
settextcolor(RED);
char info = minesNumAround + 48;
outtextxy(posX+int(gridSize*0.4), posY+int(gridSize*0.1), _T(info));
}
void onLeftButtonClick();
void onRightButtonClick();
void findMinesAround();
};
Grid类的成员函数
- onLeftButtonClick函数:判断这个格子是否被左键单击过(通过Grid的属性isClicked),只有没被点击过才执行接下来的操作。点击后normalGridNum-1,isClicked设置为true(说明已经被左键单击过),然后找出周围雷的数量并显示,再把周围不是雷的格子翻开(让它们执行onLeftButtonClick函数)
- onRightButtonClick函数:判断格子是否被点击(右键单击和左键单击)过。如果有,就把isFlag设置为false(相当于取消旗子),然后设置图片为正常格子;如果没有,就把isFlag设置为true(插旗子),然后把图片设置为旗子的图片。
- findMinesNum函数:如果这个格子自身不是雷,就遍历周围的八个格子,并以minesNumAround属性作为计数器,得出周围格子的数量。
/*定义Grid类的几个函数*/
// 鼠标左键点击
void Grid::onLeftButtonClick() {
if (!isClicked) { // 如果没有被左键点击过
isClicked = true;
normalGridsNum--;
putimage(posX, posY, &gridClicked);
findMinesAround(); // 找雷数
showMinesNumAround(); // 显示出来周围雷数
//翻开周围(4个)不是雷的格子
vector<vector<int>> minesPosAround = { {posLine - 1,posRow},{posLine,posRow - 1},{posLine,posRow + 1},{posLine + 1,posRow} };
for (int i = 0; i < 4; i++) {
if (minesPosAround[i][0] > 0 && minesPosAround[i][0] <= lines && minesPosAround[i][1] > 0 && minesPosAround[i][1] <= rows) {
if (playGround[minesPosAround[i][0]][minesPosAround[i][1]].isMine == false) {
playGround[minesPosAround[i][0]][minesPosAround[i][1]].onLeftButtonClick();
}
}
}
}
}
// 鼠标右键点击
void Grid::onRightButtonClick() {
if (!isFlag&&!isClicked) { // 如果没有被右键点击过
isFlag = true;
putimage(posX, posY, &gridFlag);
}
else if(isFlag&&!isClicked) {
isFlag = false;
putimage(posX, posY, &grid);
}
}
// 寻找周围8个(或5个)格子中雷的数量
void Grid::findMinesAround() {
if (isMine) { // 如果该格子是雷,则返回-1
return;
}
else { // 如果该格子不是雷,继续
for (int i = posLine - 1; i < posLine + 1; i++) {
for (int j = posRow - 1; j < posRow + 1; j++) {
if (i != posLine && j != posRow && playGround[i][j].isMine) {
minesNumAround++;
}
}
}
}
}
启动画面
for循环不断重绘开始界面的图像,并且每次绘制前清空画布,每次绘制后停顿1毫秒,实现动画效果。BeginBatchDraw,FlushBatchDraw和EndBatchDraw是EasyX的绘图函数,可以使图像绘制更流畅,从而使动画更流畅
// 启动画面
void splashScreen() {
IMAGE splashImage;
loadimage(&splashImage, "images/mineIcon.png",width,height);
putimage(0, 0, &splashImage);
Sleep(1500);
BeginBatchDraw();
for (int i = 0; i <= height; i+=3) {
FlushBatchDraw();
cleardevice();
putimage(0, i, &splashImage);
Sleep(1);
}
EndBatchDraw();
cleardevice();
}
死循环监听鼠标事件
如果左键单击,双重for循环判断是点击的哪一个格子。然后判断这个格子是否是雷。如果是,直接判定为输;如果不是,执行onLeftButtonClick函数。
如果右键单击,双重for循环判断是点击的哪一个格子。执行onLeftButtonClick函数。
每次循环开始时判断normalGridNum,即正常格子的数量。如果正常格子数量为0,说明所有正常格子已经被翻开,判定为胜
/*循环监听鼠标事件*/
while (true) {
if (normalGridsNum == 0) {
// 显示提示信息
settextcolor(GREEN);
BeginBatchDraw();
for (int i = height; i >= 10; i -= 3) {
FlushBatchDraw();
cleardevice();
outtextxy(10, i, "你赢啦");
Sleep(1);
}
EndBatchDraw();
Sleep(2500);
return 0;
}
ExMessage msg = getmessage(EX_MOUSE);
if (msg.message == WM_LBUTTONDOWN) {
for (int i = 1; i <= lines; i++) {
for (int j = 1; j <= rows; j++) {
if (msg.x > playGround[i][j].posX&&msg.x< playGround[i][j].posX+gridSize&&msg.y>playGround[i][j].posY&&msg.y< playGround[i][j].posY+gridSize) {
// 判断点击的是那一个格子
if (playGround[i][j].isMine) { // 如果是雷
putimage(playGround[i][j].posX, playGround[i][j].posY, &mine);
for (int i = 0; i < minesNum; i++) {
putimage(mines[i].posLine, mines[i].posRow, &mine);
}
// 显示提示信息
settextcolor(RED);
BeginBatchDraw();
for (int i = height; i >= 10; i -= 3) {
FlushBatchDraw();
cleardevice();
outtextxy(10, i, "你输啦");
Sleep(1);
}
EndBatchDraw();
Sleep(2500); // 停顿两秒半
return 0;
}
playGround[i][j].onLeftButtonClick();
}
}
}
}
else if (msg.message == WM_RBUTTONDOWN) {
for (int i = 1; i <= lines; i++) {
for (int j = 1; j <= rows; j++) {
if (msg.x > playGround[i][j].posX && msg.x< playGround[i][j].posX + gridSize && msg.y>playGround[i][j].posY && msg.y < playGround[i][j].posY + gridSize) {
// 判断点击的是那一个格子
playGround[i][j].onRightButtonClick();
}
}
}
}
}
全部代码
#include<iostream>
#include<graphics.h>
#include<vector>
#include<string>
#include<tchar.h>
#include<ctime>
using namespace std;
// 默认常量
int gridSize = 40; // 每个格子的大小(长和宽)
int lines = 5, rows = 5; // 行数和列数
int width = rows * gridSize, height = lines * gridSize; // 窗口宽度和高度
int minesNum = 10; // 雷的数量
int normalGridsNum = lines * rows - minesNum; // 除了雷以外的格子数量
IMAGE grid, gridClicked, mine, gridFlag; // 图像
// 格子类
class Grid {
public:
int posX, posY; // 该格子所处的位置(单位:像素)
bool isFlag = false, isClicked = false, isMine = false;
int posLine, posRow; // 该格子所处的行和列位置(单位:格子)
int minesNumAround = 0; // 该格子周围雷的数量
void show(int posLine,int posRow) {
//显示
this->posLine = posLine;
this->posRow = posRow;
posX = (this->posRow - 1) * gridSize;
posY = (this->posLine - 1) * gridSize;
putimage(posX, posY, &grid);
}
void showMinesNumAround() {
if (minesNumAround==0)
return;
else if (minesNumAround == 1)
settextcolor(BLUE);
else if (minesNumAround == 2)
settextcolor(GREEN);
else if (minesNumAround == 3)
settextcolor(YELLOW);
else if (minesNumAround == 4)
settextcolor(RGB(255, 135, 35));
else
settextcolor(RED);
char info = minesNumAround + 48;
outtextxy(posX+int(gridSize*0.4), posY+int(gridSize*0.1), _T(info));
}
void onLeftButtonClick();
void onRightButtonClick();
void findMinesAround();
};
vector<vector<Grid>> playGround(lines+1,vector<Grid>(rows+1)); // 创建整个雷盘
vector<Grid> mines; // 定义一个vector储存是雷的格子
/*定义Grid类的几个函数*/
// 鼠标左键点击
void Grid::onLeftButtonClick() {
if (!isClicked) { // 如果没有被左键点击过
isClicked = true;
normalGridsNum--;
putimage(posX, posY, &gridClicked);
findMinesAround(); // 找雷数
showMinesNumAround(); // 显示出来周围雷数
//翻开周围(4个)不是雷的格子
vector<vector<int>> minesPosAround = { {posLine - 1,posRow},{posLine,posRow - 1},{posLine,posRow + 1},{posLine + 1,posRow} };
for (int i = 0; i < 4; i++) {
if (minesPosAround[i][0] > 0 && minesPosAround[i][0] <= lines && minesPosAround[i][1] > 0 && minesPosAround[i][1] <= rows) {
if (playGround[minesPosAround[i][0]][minesPosAround[i][1]].isMine == false) {
playGround[minesPosAround[i][0]][minesPosAround[i][1]].onLeftButtonClick();
}
}
}
}
}
// 鼠标右键点击
void Grid::onRightButtonClick() {
if (!isFlag&&!isClicked) { // 如果没有被右键点击过
isFlag = true;
putimage(posX, posY, &gridFlag);
}
else if(isFlag&&!isClicked) {
isFlag = false;
putimage(posX, posY, &grid);
}
}
// 寻找周围8个(或5个)格子中雷的数量
void Grid::findMinesAround() {
if (isMine) { // 如果该格子是雷,则返回-1
return;
}
else { // 如果该格子不是雷,继续
for (int i = posLine - 1; i < posLine + 1; i++) {
for (int j = posRow - 1; j < posRow + 1; j++) {
if (i != posLine && j != posRow && playGround[i][j].isMine) {
minesNumAround++;
}
}
}
}
}
// 启动画面
void splashScreen() {
IMAGE splashImage;
loadimage(&splashImage, "images/mineIcon.png",width,height);
putimage(0, 0, &splashImage);
Sleep(1500);
BeginBatchDraw();
for (int i = 0; i <= height; i+=3) {
FlushBatchDraw();
cleardevice();
putimage(0, i, &splashImage);
Sleep(1);
}
EndBatchDraw();
cleardevice();
}
//主函数
int main(int argc, char* argv[]) {
/*接收来自命令行的参数*/
if (argc == 2 && argv[1] == "-h") { // 输出帮助信息
cout << "帮助" << endl;
cout << "-s:每个格子的大小" << endl;
cout << "-l:雷盘的行数" << endl << "-r:雷盘的列数" << endl;
cout << "-n:雷的个数" << endl;
}
else if (argc > 2) {
for (int i = 1; i < argc-1; i++) {
if (argv[i] == "-s") {
gridSize = atoi(argv[i + 1]);
}
else if (argv[i] == "-l") {
lines = atoi(argv[i + 1]);
}
else if (argv[i] == "-r") {
rows = atoi(argv[i + 1]);
}
else if (argv[i] == "-n") {
minesNum = atoi(argv[i + 1]);
}
else {
cout << "参数错误" << endl;
}
}
}
/*初始化画布*/
initgraph(gridSize * rows, height);
loadimage(&grid, "images/grid.png", gridSize, gridSize);
loadimage(&gridClicked, "images/gridClicked.png", gridSize, gridSize);
loadimage(&mine, "images/mine.png", gridSize, gridSize);
loadimage(&gridFlag, "images/gridFlag.png", gridSize, gridSize);
setbkmode(TRANSPARENT);
settextstyle(int(gridSize*0.9), int(gridSize * 0.4), _T("微软雅黑"));
setbkcolor(WHITE);
cleardevice();
/*启动画面*/
splashScreen();
/*布置雷盘*/
// 显示格子
for (int i = 1; i <= lines; i++) {
for (int j = 1; j <= rows; j++) {
playGround[i][j].show(i, j);
Sleep(10);
}
}
// 随机布雷
srand(time(nullptr));
for (int i = 0; i < minesNum; i++) {
int x = rand() % rows + 1, y = rand() % lines + 1;
playGround[x][y].isMine = true;
mines.push_back(playGround[x][y]);
}
/*循环监听鼠标事件*/
while (true) {
if (normalGridsNum == 0) {
// 显示提示信息
settextcolor(GREEN);
BeginBatchDraw();
for (int i = height; i >= 10; i -= 3) {
FlushBatchDraw();
cleardevice();
outtextxy(10, i, "你赢啦");
Sleep(1);
}
EndBatchDraw();
Sleep(2500);
return 0;
}
ExMessage msg = getmessage(EX_MOUSE);
if (msg.message == WM_LBUTTONDOWN) {
for (int i = 1; i <= lines; i++) {
for (int j = 1; j <= rows; j++) {
if (msg.x > playGround[i][j].posX&&msg.x< playGround[i][j].posX+gridSize&&msg.y>playGround[i][j].posY&&msg.y< playGround[i][j].posY+gridSize) {
// 判断点击的是那一个格子
if (playGround[i][j].isMine) { // 如果是雷
putimage(playGround[i][j].posX, playGround[i][j].posY, &mine);
for (int i = 0; i < minesNum; i++) {
putimage(mines[i].posLine, mines[i].posRow, &mine);
}
// 显示提示信息
settextcolor(RED);
BeginBatchDraw();
for (int i = height; i >= 10; i -= 3) {
FlushBatchDraw();
cleardevice();
outtextxy(10, i, "你输啦");
Sleep(1);
}
EndBatchDraw();
Sleep(2500); // 停顿两秒半
return 0;
}
playGround[i][j].onLeftButtonClick();
}
}
}
}
else if (msg.message == WM_RBUTTONDOWN) {
for (int i = 1; i <= lines; i++) {
for (int j = 1; j <= rows; j++) {
if (msg.x > playGround[i][j].posX && msg.x< playGround[i][j].posX + gridSize && msg.y>playGround[i][j].posY && msg.y < playGround[i][j].posY + gridSize) {
// 判断点击的是那一个格子
playGround[i][j].onRightButtonClick();
}
}
}
}
}
/*关闭绘图窗口*/
closegraph();
return 0;
}
其他
上次的扫雷代码详见储存库,图像资源也在上面,这次的用的图像一样的。不过这次写代码时改了一下图片文件的名字。