基于QT的跨平台扫雷游戏

扫雷介绍

《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。

扫雷游戏区包括雷区、地雷计数器(位于左上角,记录剩余地雷数),计时器(位于右上角,记录游戏时间)和一个笑脸图标(点击用以重置整个扫雷界面),确定大小的矩形雷区中按照算法布置一定数量的地雷,玩家需要尽快找出雷区中的所有不是地雷的方块,而不许踩到地雷。

游戏的基本操作包括左键单击(Left Click)、左键双击(Chording)两种。其中左键用于打开安全的格子,推进游戏进度;左键双击标记地雷,以辅助判断;

左键单击:在判断出不是雷的方块上按下左键,可以打开该方块。如果方块上出现数字,则该数字表示其周围3×3区域中的地雷数(一般为8个格子,对于边块为5个格子,对于角块为3个格子。所以扫雷中最大的数字为8);如果方块上为空(相当于0),则可以递归地打开与空相邻的方块;如果不幸触雷,则游戏结束。

左键双击:在判断为地雷的方块上按下右键,可以标记地雷(显示为小红旗)。重复一次操作可取消标记。

图片资源

步骤

​ 步骤1:创建QT项目

终端输入:qtcreator或点击桌面qt图标。启动Qt创造器。

选择菜单"file/file or project",在"新建"对话框中依次选择"application"和"QT widgets Application", 并点击"choose..."

在"项目介绍和位置"中指定项目的名称为:UdpSender,并选择存储路径,Qt会在该路径下创建工程目录,点击"next"

在"Class information"中选择"QDialog"作为"Base class",并将"class name"设置为"Mine",点击"next" ,点击"finsh"。

步骤2:编写头文件

ifndef MINE_H
define MINE_H
include <QDialog>
include <QTimer>
include <QPushButton>
namespace Ui {
class Mine;
}
class Mine : public QDialog
{
    Q_OBJECT
public:
    explicit Mine(int x, int y,QWidget *parent = 0);
    ~Mine();
private:
    void init();//初始化数据
    void paintEvent(QPaintEvent *);//绘图事件
    void mousePressEvent(QMouseEvent *e);//鼠标点击事件
    void mouseReleaseEvent(QMouseEvent *);//鼠标释放事件
    void mouseDoubleClickEvent(QMouseEvent *e);//鼠标双击事件
    void sui(int x, int sy);//随机布雷
    void Ment();//统计雷周围的数字
    void Open(int x,int y);//递归展开
    void Explode();//点击到雷后,展开所有的雷
    int pan();//判断游戏是否结束
private slots:
    void on_timer();
    void on_mouse();
private:
    Ui::Mine *ui;
    int wx;//窗口的大小
    int hy;
    int mapx;//根据窗口的大小设置雷的行数(设置数组的大小)
    int mapy;//根据窗口的大小设置雷的列数
    int map[50][50];//虚拟地图
    int map2[50][50];//真实地图
    int size;//雷的数量
    int Ssize;//剩余雷的数量
    bool xiao;//判断是否点击笑脸
    bool One;//判断是否是第一次点击雷区
    QTimer timer;//定时器,计算游戏时长
    int Time;//游戏的时长
    QTimer mouseTimer;//定时器,用以判断是鼠标双击还是单击
    QPushButton* button;//按钮,用以关闭的窗口
};
#endif // MINE_H    

步骤3:初始化成员数据

初始化类成员编写init函数

void Mine::init(){
    Time = 0;//初始化时长为0
    xiao = false;//设置笑脸没有被点击
    timer.stop();//停止计算时长定时器
    mouseTimer.stop();//停止计算是否双击定时器
    One = true;//设置是第一次点击雷区
    wx = width();//获取窗口宽度
    hy = height();//获取窗口高度
    memset(map2,0,sizeof(map2));//将真实地图全部设置为0
    for(int x=0;x<50;x++){//将虚拟地图全部设置为9,待点击状态
        for(int y=0;y<50;y++){
            map[x][y]=9;
        }
    }
    //根据窗口的大小计算出雷区的行和列
    mapy = (wx-32)/25;
    if(mapx>50){
        mapx=50;
    }
    mapx = (hy-117)/25;
    if(mapy>50){
        mapy=50;
    }
    //计算出雷的个数,
    size = (mapx*mapy)/2-(mapx*mapy)/3;
    Ssize = size;//初始化剩余雷的个数和雷的个数相同
    //定时器连接槽函数
    connect(&timer,SIGNAL(timeout()),this,SLOT(on_timer()));
    connect(&mouseTimer,SIGNAL(timeout()),this,SLOT(on_mouse()));
    //设置按钮信息
    button = new QPushButton(this);
    button->resize(20,20);//设置按钮大小
    button->move(-1,-1);//设置按钮的坐标点
    button->setFlat(true);//设置按钮为透明
    button->setText("x");//设置按钮内容为X,模拟关闭按钮
    //连接槽函数,当点击按钮关闭窗口
    connect(button,SIGNAL(clicked()),this,SLOT(close()));    
}   

步骤4:初始构造函数

​ 4.1为了让程序在跨平台中适应不同的窗口的大小,所以改变构造函数为带参构造函数,在Mine.h和Mine.cpp中同时修改,并修改main.cpp中的对象调用。修改如下。

mine.h
​
•           explicit Mine(int x, int y,QWidget *parent = 0);//将构造函数设置为带参构造函数
•       mine.cpp
​
•           Mine::Mine(int x, int y, QWidget *parent) ://将构造函数设置为带参构造函数
             QDialog(parent),
•               ui(new Ui::Mine)
•           {
•                   ui->setupUi(this);  
​
•           }
•       main.cpp
​
•           int main(int argc, char *argv[])
•           {
•                   QApplication a(argc, argv);
                 Mine w(600,600);//指定窗口的大小为600X600的
                 w.show();
                 return a.exec();
•           }

 

4.2根据参数设置窗口的大小,因为每一个方格的大小范围为25X25,所以雷区的大小必须是25的倍数。根据用户传递过来的窗口大小,并计算出窗口的大小。并在构造函数调用初始化数据函数。

            Mine::Mine(int x, int y, QWidget *parent) :
             QDialog(parent),
             ui(new Ui::Mine)
            {
                 ui->setupUi(this);
                 x=((x-32)%25)<10?x-((x-32)%25):x+25-((x-32)%25);//计算出窗口的X大小
                 y=((y-117)%25)<10?y-((y-117)%25):y+25-((y-117)%25);//计算出窗口的Y大小
                 this->setFixedSize(x,y);//设置窗口的大小
                 init();//调用数据初始化函数,初始化数据
            }

步骤5:随机产生雷

根据计算出的雷数,随机在真实地图中产生雷。

        void Mine::sui(int sx,int sy){//sx,sy为用户第一次点击的位置,确保第一次点击不能点到雷
            for(int i=0;i<size;){
                int x = rand()%mapx;
                int y = rand()%mapy;
                if(map2[x][y]==0&&x!=sx&&y!=sy){
                    map2[x][y]=10;//数字10表示为雷
                    i++;
                }
            }
        }

步骤4:统计周围的数字

雷布置好后,求出雷周围8个方向的数字,为1-8。统计周围数字有两种方法

1遍历整个数组找到雷,让雷周围的8个方向非雷加1。

2遍历整个地图,找到非雷方格,判断周围有几个雷。非雷方格对应加几。

以下代码采用第一种,找到所有的雷,让雷的周围8个方格非雷加1。

        void Mine::Ment(){
            //为了方便找到周围八个点,将坐标差值记录到一个二维数组中
            int arr[8][2]={{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
            //遍历查找真实地图,找到雷
            for(int x=0;x<mapx;x++){
                for(int y=0;y<mapy;y++){
                    if(map2[x][y]==10){//找到雷
                        //找到雷后,根据差值找到雷周围的8个方向
                        for(int k=0;k<8;k++){
                            int nx=x+arr[k][0];
                            int ny=y+arr[k][1];
                            if(nx<0||nx>mapx||ny<0||ny>mapy){//判断是否越界
                                continue;
                            }
                            else if(map2[nx][ny]==10){//判断是否是雷
                                continue;
                            }
                            else{//没有越界也不是雷就加1,说明这个方格周围有一个雷
                                map2[nx][ny]++;
                            }
                        }
                    }
                }
            }
        }

步骤5:重写绘图事件

​ 为了让界面好看,模拟3D效果,需要将界面分为三个区域,第一个区域为上面图形周边的方格,是由线来绘制,根据线的属性不同绘制出3D效果。第二个区域为游戏区上方的数据区,只要显示剩余雷数,游戏时长以及笑脸图标。第三个区域为雷区,根据虚拟地图的二维数组绘制不同的图案,真实地图放置的为雷和数字,虚拟地图为用户所能看到的。

		void Mine::paintEvent(QPaintEvent *)
		{
    		QPainter pa(this);//声明绘图对象类
    		QPixmap pix(":/mine.png");//加载图片
    		//模拟3D效果,区分雷区,数据区
    		//画边框
    		//设置画笔样式为白色,10个宽度,连续
    		pa.setPen(QPen(Qt::white,10,Qt::SolidLine,Qt::RoundCap,Qt::RoundJoin));
   			pa.drawRect(0,0,wx,hy);
    		//画上面的正方形
    		pa.setPen(QPen(Qt::white, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    		pa.drawLine(15,80,wx-15,80);
    		pa.drawLine(wx-15,15,wx-15,80);
    		pa.setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            pa.drawLine(15,15,15,80);
            pa.drawLine(15,15,wx-15,15);
            //画下面的正方形
            pa.setPen(QPen(Qt::black, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            pa.drawLine(15,100,wx-15,100);
            pa.drawLine(15,100,15,hy-15);
            pa.setPen(QPen(Qt::white, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            pa.drawLine(15,hy-15,wx-15,hy-15);
            pa.drawLine(wx-15,100,wx-15,hy-15);
            //画上面中的红色
            pa.setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            pa.setBrush(QBrush(QColor(139,26,26)));
            pa.drawRect(25,25,80,45);
            pa.drawRect(wx-25-80,25,80,45);
            pa.setPen(QPen(Qt::red, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            pa.setFont(QFont("文泉驿等宽正黑",40,35));
            char buf[5]={0};
            sprintf(buf,"%03d",Ssize);
            pa.drawText(25,68,buf);
            sprintf(buf,"%03d",Time);
            pa.drawText(wx-25-80,68,buf);
            //画笑脸
            if(xiao==true){
                QPixmap img = pix.copy(15*25-1,0,24,24);
                pa.drawPixmap((wx-30)/2+2,37,24,24,img);
            }else{
                QPixmap img = pix.copy(14*25,0,24,24);
                pa.drawPixmap((wx-30)/2+2,37,24,24,img);
            }
            //画扫雷界面
            //0为点过的方格
            //1-8数字方格
            //9没有点过方格
            //10雷方格
            //11小红旗方格
            //12踩到雷方格,
            for(int x=0;x<mapx;x++){
                for(int y=0;y<mapy;y++){
                    QPixmap img=pix.copy(map[x][y]*25,0,25,25);
                    pa.drawPixmap(y*25+16,x*25+101,25,25,img);
                }
            }
        }

步骤6:重写鼠标事件

鼠标主要有鼠标按下,鼠标弹起和鼠标双击三种方式,QT的鼠标双击和鼠标单击事件有冲突,当点击鼠标双击时,鼠标单击也会触发,所有为了区分鼠标双击和鼠标单击,采用定时器,当在一定时间内两次点击鼠标则触发双击事件,当在一定时间内没有触发第二单击侧默认为单击事件

        //鼠标点击事件。当鼠标点击,开启鼠标定时器,当用户在200毫秒内第二次点击就启动鼠标双击,如果在没有第二次点击就启动鼠标定时器槽函数,处理单击事件
        void Mine::mousePressEvent(QMouseEvent *e)
        {
            mouseTimer.start(200);//开启鼠标定时器,必须大于200秒小于300秒,小于200秒就不起作用,大于300秒事件间隔太大,可能造成双击和单击混淆
            mousex = e->x();//获取鼠标点击的位置
            mousey = e->y();
        }
	    //鼠标释放。当点击笑脸时,笑脸变哭脸,当释放鼠标时笑脸变哭脸
        void Mine::mouseReleaseEvent(QMouseEvent *)
        {
            xiao = false;//设置哭脸为笑脸
            update();
        }
		//鼠标双击事件,当鼠标双击就对方格进行标记为小红旗用11表示小红旗
        void Mine::mouseDoubleClickEvent(QMouseEvent *e){
            mouseTimer.stop();//启动双击事件,停止鼠标定时器
            //判断用户点击的位置是否在雷区
            if((e->x()>16)&&(e->x()<wx-16)&&(e->y()>101)&&(e->y()<hy-16)){
                //二维数组的下标位置和绘图的坐标位置正好相反
                int lx = (e->y()-101)/25;//求出用户点击的位置对应的二维数组的下标,
                int ly = (e->x()-15)/25;
                if(map[lx][ly]==11){//已经是小红旗则取消小红旗
                    map[lx][ly]=9;
                    Ssize++;
                }else if(map[lx][ly]!=9){//如果不为空,则不用处理了
                    return;
                }
                else{//如果为空则标记为小红旗,相应的雷数减1,
                    Ssize--;
                    map[lx][ly]=11;
                }
            }
            //判断游戏是否结束,为1说明游戏结束了
            if(pan()==1){
                Explode();//显示所有的雷
                update();//更新界面
                timer.stop();//停止游戏时长定时器
                //询问用户是否重新开始
                if(QMessageBox::warning(NULL,"提示","恭喜你胜利了\n\t是否重新开始游戏",QMessageBox::Yes,QMessageBox::No)==QMessageBox::Yes){
                    init();
                }else{
                    close();
                }
            }
        }
		//鼠标但是处理函数,是鼠标定时器对应的槽函数
        void Mine::on_mouse(){
            mouseTimer.stop();
            //点击笑脸,变成哭脸并重置游戏
            if((mousex>((wx-30)/2))&&mousex<((wx-30)/2+25)){
                if(mousey>35&&mousey<60){
                    xiao = true;
                    init();
                    update();
                }
            }
            //点击雷区,
            if((mousex>16)&&(mousex<wx-16)&&(mousey>101)&&(mousey<hy-16)){
                int lx = (mousey-101)/25;//获取点击的位置对应的二维数组的下标
                int ly = (mousex-15)/25;
                if(One==true){//第一次点击之后游戏开始,确保第一次点击不会点击到雷,并且游戏时长也是从第一次点击开始
                    One = false;
                    timer.start(1000);
                    sui(lx,ly);
                    Ment();
                }//判断用户点击是否越界
                if(lx<0||lx>mapx||ly<0||ly>mapy){
                    return;
                }else if(map[lx][ly]!=9){//判断用户点击不为空
                    return;
                }else if(map2[lx][ly]==10){//踩道雷;
                    Explode();//实现所有的雷
                    map[lx][ly]=12;//用户点击显示红X雷
                    timer.stop();//停止定时器
                    //询问是否继续游戏
                    if(QMessageBox::warning(NULL,"提示","你踩道雷了\n\t是否重新开始游戏",QMessageBox::Yes,QMessageBox::No)==QMessageBox::Yes){
                        init();
                    }else{
                        close();
                    }
                }else if(map2[lx][ly]==0){//点击空白时,需要递归展开,遇到数字或者边界停止
                    map[lx][ly]=0;
                    Open(lx,ly);
                }else if(map2[lx][ly]>0&&map2[lx][ly]<9){//点击到数字直接将真实地图的给虚拟地图
                    map[lx][ly]=map2[lx][ly];
                }
            }
            //判断游戏是否胜利
            if(pan()==1){
                Explode();
                update();
                timer.stop();
                if(QMessageBox::warning(NULL,"提示","恭喜你胜利了\n\t是否重新开始游戏",QMessageBox::Yes,QMessageBox::No)==QMessageBox::Yes){
                    init();
                }else{
                    close();
                }
            }
            update();
        }

步骤7:递归展开函数

当用户点击的位置为空白时,则展开周围所有空白的位置,当周围的方格也是空白的就继续这个空白的位置继续展开,使用递归函数来实现,直到遇到数字或者边界则停止。为了更好的寻找到周围的八个点的方向,将当前点与周围八个点的差值用二维数组保存起来,循环即可。

        void Mine::Open(int x, int y){
            //记录周围八个点的差值,使用二维数组,y为0表示X的差值,y为1表示Y的差值
            int val[8][2]={{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
            for(int k=0;k<8;k++){//寻找用户点击的位置周围的八个点
                  int nx = val[k][0]+x;//获取周围的某个点
                  int ny = val[k][1]+y;
                  //判断这个点是否越界,越界则不用管
                  if(nx<0||nx>mapx||ny<0||ny>mapy){
                      continue;
                  }
                  //确定周围的这个点没有被展开过,如果已经展开也不用管
                  else if(map[nx][ny]!=9){
                      continue;
                  }
                  //如果周围的某个点依然是空白的,则继续以当前点为中心继续展开周围的八个点
                  else if(map2[nx][ny]==0){
                      map[nx][ny]=0;
                      Open(nx,ny);
                  }
                  //如果是数字则直接显示即可并停止这一方向的继续展开
                  else{
                      map[nx][ny]=map2[nx][ny];
                  }
              }
        }

步骤8:判断游戏是否胜利

当所有标记为小红旗的方格所对应的真实地图中是雷,并且剩余未点击的方格的数量正好是剩余的雷数就说明扫雷胜利了.在鼠标双击事件中,当标记一个小红旗,剩余雷数量就会减1。

        int Mine::pan(){
            int i =0;
            //判断剩余雷数量大于0时,小红旗所对应的真实地图中的雷匹配就说明胜利了
            if(Ssize>0){
                for(int x=0;x<mapx;x++){
                    for(int y=0;y<mapy;y++){
                        if(map[x][y]==11&&map2[x][y]!=10){//不匹配
                            return -1;
                        }else if(map[x][y]==9){//剩余没有点击的
                            i++;
                        }
                    }
                }
            }
            if(Ssize+i==size){//判断剩余未点击的方格和剩余雷数相同则说明胜利
                return 1;
            }
            return -1;
        }

步骤9:展开所有的雷

当用户点击猜到雷时,需要将所有的地雷展开,遍历真个真实地图,将真实地图中雷对应的虚拟地图也改成雷。

        void Mine::Explode(){
            for(int i = 0;i<mapx;i++){
                for(int j=0;j<mapy;j++){
                    if(map2[i][j]==10){
                        map[i][j]=13;
                    }
                }
            }
        }

步骤10:游戏时长计算

当用户第一次点击雷区,游戏时长开始计算,启动游戏时长计算定时器,定时器设置为1秒钟。当游戏时长为999时游戏雷还没有结束就说明用户输了。

        void Mine::on_timer(){
            if(Time>999){//当游戏时长为999秒时,游戏结束
                Explode();//展开所有的雷
                update();
                timer.stop();//游戏时长定时器停止
                //询问是否继续游戏
                if(QMessageBox::warning(NULL,"提示","你踩道雷了\n\t是否重新开始游戏",QMessageBox::Yes,QMessageBox::No)==QMessageBox::Yes){
                    init();
                }else{
                    close();
                }
            }
            Time++;//游戏时长加1
            update();
        }

源代码下载:https://download.csdn.net/download/u012670181/10957115

 

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值