目录
4.1 场景基本设置
选择关卡构造函数如下:
- //设置窗口固定大小
- this->setFixedSize(320,588);
- //设置图标
- this->setWindowIcon(QPixmap(":/res/Coin0001.png"));
- //设置标题
- this->setWindowTitle("选择关卡");
-
- //创建菜单栏
- QMenuBar * bar = this->menuBar();
- this->setMenuBar(bar);
- //创建开始菜单
- QMenu * startMenu = bar->addMenu("开始");
- //创建按钮菜单项
- QAction * quitAction = startMenu->addAction("退出");
- //点击退出 退出游戏
- connect(quitAction,&QAction::triggered,[=](){this->close();});
运行效果如图:

- void ChooseLevelScene::paintEvent(QPaintEvent *)
- {
- QPainter painter(this);
- QPixmap pix;
- pix.load(":/res/OtherSceneBg.png");
- painter.drawPixmap(0,0,this->width(),this->height(),pix);
-
- //加载标题
- pix.load(":/res/Title.png");
- painter.drawPixmap( (this->width() - pix.width())*0.5,30,pix.width(),pix.height(),pix);
- }
- //返回按钮
- MyPushButton * closeBtn = new MyPushButton(":/res/BackButton.png",":/res/BackButtonSelected.png");
- closeBtn->setParent(this);
- closeBtn->move(this->width()-closeBtn->width(),this->height()-closeBtn->height());
返回按钮是有正常显示图片和点击后显示图片的两种模式,所以我们需要重写MyPushButton中的 MousePressEvent和MouseReleaseEvent。
- //鼠标事件
- void MyPushButton::mousePressEvent(QMouseEvent *e)
- {
- if(pressedImgPath != "") //选中路径不为空,显示选中图片
- {
- QPixmap pixmap;
- bool ret = pixmap.load(pressedImgPath);
- if(!ret)
- {
- qDebug() << pressedImgPath << "加载图片失败!";
- }
-
- this->setFixedSize( pixmap.width(), pixmap.height() );
- this->setStyleSheet("QPushButton{border:0px;}");
- this->setIcon(pixmap);
- this->setIconSize(QSize(pixmap.width(),pixmap.height()));
- }
- //交给父类执行按下事件
- return QPushButton::mousePressEvent(e);
- }
- void MyPushButton::mouseReleaseEvent(QMouseEvent *e)
- {
- if(normalImgPath != "") //选中路径不为空,显示选中图片
- {
- QPixmap pixmap;
- bool ret = pixmap.load(normalImgPath);
- if(!ret)
- {
- qDebug() << normalImgPath << "加载图片失败!";
- }
- this->setFixedSize( pixmap.width(), pixmap.height() );
- this->setStyleSheet("QPushButton{border:0px;}");
- this->setIcon(pixmap);
- this->setIconSize(QSize(pixmap.width(),pixmap.height()));
- }
- //交给父类执行 释放事件
- return QPushButton::mouseReleaseEvent(e);
- }
在这里我们点击返回后,延时0.5后隐藏自身,并且发送自定义信号,告诉外界自身已经选择了返回按钮。
- //返回按钮功能实现
- connect(closeBtn,&MyPushButton::clicked,[=](){
- QTimer::singleShot(500, this,[=](){
- this->hide();
- //触发自定义信号,关闭自身,该信号写到 signals下做声明
- emit this->chooseSceneBack();
- }
- );
- });
在主场景MainScene中 点击开始按钮显示选择关卡的同时,监听选择关卡的返回按钮消息
- //监听选择场景的返回按钮 connect(chooseScene,&ChooseLevelScene::chooseSceneBack,[=](){
- this->show();
- });
测试主场景与选择关卡场景的切换功能。
- //创建关卡按钮
- for(int i = 0 ; i < 20;i++)
- {
- MyPushButton * menuBtn = new MyPushButton(":/res/LevelIcon.png");
- menuBtn->setParent(this);
- menuBtn->move(25 + (i%4)*70 , 130+ (i/4)*70);
-
- //按钮上显示的文字
- QLabel * label = new QLabel;
- label->setParent(this);
- label->setFixedSize(menuBtn->width(),menuBtn->height());
- label->setText(QString::number(i+1));
- label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); //设置居中
- label->move(25 + (i%4)*70 , 130+ (i/4)*70);
- label->setAttribute(Qt::WA_TransparentForMouseEvents,true); //鼠标事件穿透
- }
运行效果如果:

点击关卡按钮后,会进入游戏的核心场景,也就是翻金币的场景,首先先创建出该场景的.h和.cpp文件
创建PlayScene

点击选择关卡按钮后会跳入到该场景
建立点击按钮,跳转场景的信号槽连接
在ChooseLevelScene.h 中声明
PlayScene *pScene = NULL;
- //监听选择关卡按钮的信号槽
- connect(menuBtn,&MyPushButton::clicked,[=](){
- // qDebug() << "select: " << i;
- if(pScene == NULL) //游戏场景最好不用复用,直接移除掉创建新的场景
- {
- this->hide();
- pScene = new PlayScene(i+1); //将选择的关卡号 传入给PlayerScene
- pScene->show();
- }
- });
这里pScene = new PlayScene(i+1); 将用户所选的关卡号发送给pScene,也就是翻金币场景,当然PlayScene 要提供重载的有参构造版本,来接受这个参数。
PlayScene.h中 声明成员变量,用于记录当前用户选择的关卡。
- //成员变量 记录关卡索引
- int levalIndex;
PlayScene.cpp中 初始化该场景配置。
- PlayScene::PlayScene(int index)
- {
- //qDebug() << "当前关卡为"<< index;
- this->levalIndex = index;
- //设置窗口固定大小
- this->setFixedSize(320,588);
- //设置图标
- this->setWindowIcon(QPixmap(":/res/Coin0001.png"));
- //设置标题
- this->setWindowTitle("翻金币");
-
- //创建菜单栏
- QMenuBar * bar = this->menuBar();
- this->setMenuBar(bar);
- //创建开始菜单
- QMenu * startMenu = bar->addMenu("开始");
- //创建按钮菜单项
- QAction * quitAction = startMenu->addAction("退出");
- //点击退出 退出游戏
- connect(quitAction,&QAction::triggered,[=](){this->close();});
- }
- void PlayScene::paintEvent(QPaintEvent *)
- {
- //加载背景
- QPainter painter(this);
- QPixmap pix;
- pix.load(":/res/PlayLevelSceneBg.png");
- painter.drawPixmap(0,0,this->width(),this->height(),pix);
-
- //加载标题
- pix.load(":/res/Title.png");
- pix = pix.scaled(pix.width()*0.5,pix.height()*0.5);
- painter.drawPixmap( 10,30,pix.width(),pix.height(),pix);
- }
- //返回按钮
- MyPushButton * closeBtn = new MyPushButton(":/res/BackButton.png",":/res/BackButtonSelected.png");
- closeBtn->setParent(this);
- closeBtn->move(this->width()-closeBtn->width(),this->height()-closeBtn->height());
-
- //返回按钮功能实现
- connect(closeBtn,&MyPushButton::clicked,[=](){
- QTimer::singleShot(500, this,[=](){
- this->hide();
- //触发自定义信号,关闭自身,该信号写到 signals下做声明
- emit this->chooseSceneBack();
- }
- );
- });
5.4 在ChooseScene选择关卡场景中,监听PlayScene的返回信号。
- connect(pScene,&PlayScene::chooseSceneBack,[=](){
- this->show();
- delete pScene;
- pScene = NULL;
- });

- //当前关卡标题
- QLabel * label = new QLabel;
- label->setParent(this);
- QFont font;
- font.setFamily("华文新魏");
- font.setPointSize(20);
- label->setFont(font);
- QString str = QString("Leavel: %1").arg(this->levalIndex);
- label->setText(str);
- label->setGeometry(QRect(30, this->height() - 50,120, 50)); //设置大小和位置
假设我们选择了第15关卡,运行效果如果:

- //创建金币的背景图片
- for(int i = 0 ; i < 4;i++)
- {
- for(int j = 0 ; j < 4; j++)
- {
- //绘制背景图片
- QLabel* label = new QLabel;
- label->setGeometry(0,0,50,50);
- label->setPixmap(QPixmap(":/res/BoardNode.png"));
- label->setParent(this);
- label->move(57 + i*50,200+j*50);
- }
- }
运行效果如图:

我们知道,金币是本游戏的核心对象,并且在游戏中可以利用二维数组进行维护,拥有支持点击,翻转特效等特殊性,因此不妨将金币单独封装到一个类中,完成金币所需的所有功能。

并修改MyCoin的基类为QPushButton
在资源图片中,我们可以看到,金币翻转的效果原理是多张图片切换而形成的,而以下八张图片中,第一张与最后一张比较特殊,因此我们在给用户看的时候,无非是金币Coin0001或者是银币 Coin0008这两种图。
因此我们在创建一个金币对象时候,应该提供一个参数,代表着传入的是金币资源路径还是银币资源路径,根据路径我们创建不同样式的图案。

在MyCoin.h中声明:
MyCoin(QString butImg); //代表图片路径
在MyCoin.cpp中进行实现
- MyCoin::MyCoin(QString butImg)
- {
-
- QPixmap pixmap;
- bool ret = pixmap.load(butImg);
- if(!ret)
- {
- qDebug() << butImg << "加载图片失败!";
- }
-
- this->setFixedSize( pixmap.width(), pixmap.height() );
- this->setStyleSheet("QPushButton{border:0px;}");
- this->setIcon(pixmap);
- this->setIconSize(QSize(pixmap.width(),pixmap.height()));
-
- }
在翻金币场景 PlayScene中,我们测试下封装的金币类是否可用,可以在创建好的金币背景代码后,添加如下代码:
- //金币对象
- MyCoin * coin = new MyCoin(":/res/Coin0001.png");
- coin->setParent(this);
- coin->move(59 + i*50,204+j*50);
运行效果如图

当然上述的测试只是为了让我们知道提供的对外接口可行,但是每个关卡的初始化界面并非如此,因此需要我们引用一个现有的关卡文件,文件中记录了各个关卡的金币排列清空,也就是二维数组的数值。
首先先将dataConfig.h 和 dataConfig.cpp文件放入到当前项目下:

其次在Qt_Creator项目右键,点击添加现有文件。

选择当前项目下的文件,并进行添加

我们可以看到,其实dataConfig.h中只有一个数据是对外提供的,如下图

在上图中,QMap
其中,int代表对应的关卡 ,也就是QMap中的key值,而value值就是对应的二维数组,我们利用的是 QVector
在Main函数可以测试第一关的数据,添加如下代码:
- dataConfig config;
- for(int i = 0 ; i < 4;i++)
- {
- for(int j = 0 ; j < 4; j++)
- {
- //打印第一关所有信息
- qDebug() << config.mData[1][i][j];
-
- }
- qDebug()<< "";
- }
输出结果如下图:

对应着dataConfig.cpp中第一关数据来看,与之匹配成功,以后我们就可以用dataConfig中的数据来对关卡进行初始化了。

首先,可以在playScene中声明一个成员变量,用户记录当前关卡的二维数组。
int gameArray[4][4]; //二维数组数据
之后,在.cpp文件中,初始化这个二维数组
- //初始化二维数组
- dataConfig config;
- for(int i = 0 ; i < 4;i++)
- {
- for(int j = 0 ; j < 4; j++)
- {
- gameArray[i][j] = config.mData[this->levalIndex][i][j];
- }
- }
初始化成功后,在金币类 也就是MyCoin类中,扩展属性 posX,posY,以及flag
这三个属性分别代表了,该金币在二维数组中 x的坐标,y的坐标,以及当前的正反标志。
- int posX; //x坐标
- int posY; //y坐标
- bool flag; //正反标志
然后完成金币初始化,代码如下:
- //金币对象
- QString img;
- if(gameArray[i][j] == 1)
- {
- img = ":/res/Coin0001.png";
- }
- else
- {
- img = ":/res/Coin0008.png";
- }
- MyCoin * coin = new MyCoin(img);
- coin->setParent(this);
- coin->move(59 + i*50,204+j*50);
- coin->posX = i; //记录x坐标
- coin->posY = j; //记录y坐标
- coin->flag =gameArray[i][j]; //记录正反标志
运行测试各个关卡初始化,例如第一关效果如图:

关卡的初始化完成后,下面就应该点击金币,进行翻转的效果了,那么首先我们先在MyCoin类中创建出该方法。
在MyCoin.h中声明:
- void changeFlag();//改变标志,执行翻转效果
- QTimer *timer1; //正面翻反面 定时器
- QTimer *timer2; //反面翻正面 定时器
- int min = 1; //最小图片
- int max = 8; //最大图片
MyCoin.cpp中做实现
- void MyCoin::changeFlag()
- {
- if(this->flag) //如果是正面,执行下列代码
- {
- timer1->start(30);
- this->flag = false;
- }
- else //反面执行下列代码
- {
- timer2->start(30);
- this->flag = true;
- }
- }
当然在构造函数中,记得创建出两个定时器。
- //初始化定时器
- timer1 = new QTimer(this);
- timer2 = new QTimer(this);
当我们分别启动两个定时器时,需要在构造函数中做监听操作,并且做出响应,翻转金币,然后再结束定时器。
构造函数中 进行下列监听代码:
- //监听正面翻转的信号槽
- connect(timer1,&QTimer::timeout,[=](){
- QPixmap pixmap;
- QString str = QString(":/res/Coin000%1.png").arg(this->min++);
- pixmap.load(str);
- this->setFixedSize(pixmap.width(),pixmap.height() );
- this->setStyleSheet("QPushButton{border:0px;}");
- this->setIcon(pixmap);
- this->setIconSize(QSize(pixmap.width(),pixmap.height()));
- if(this->min > this->max) //如果大于最大值,重置最小值,并停止定时器
- {
- this->min = 1;
- timer1->stop();
- }
- });
-
- connect(timer2,&QTimer::timeout,[=](){
- QPixmap pixmap;
- QString str = QString(":/res/Coin000%1.png").arg((this->max)-- );
- pixmap.load(str);
- this->setFixedSize(pixmap.width(),pixmap.height() );
- this->setStyleSheet("QPushButton{border:0px;}");
- this->setIcon(pixmap);
- this->setIconSize(QSize(pixmap.width(),pixmap.height()));
- if(this->max < this->min) //如果小于最小值,重置最大值,并停止定时器
- {
- this->max = 8;
- timer2->stop();
- }
- });
监听每个按钮的点击效果,并翻转金币。
- connect(coin,&MyCoin::clicked,[=](){
- //qDebug() << "点击的位置: x = " << coin->posX << " y = " << coin->posY ;
- coin->changeFlag();
- gameArray[i][j] = gameArray[i][j] == 0 ? 1 : 0; //数组内部记录的标志同步修改
- });

此时,确实已经可以执行翻转金币代码了,但是如果快速点击,会在金币还没有执行一个完整动作之后 ,又继续开始新的动画,我们应该在金币做动画期间,禁止再次点击,并在完成动画后,开启点击。
在MyCoin类中加入一个标志 isAnimation 代表是否正在做翻转动画。
bool isAnimation = false; //做翻转动画的标志
在MyCoin做动画期间加入
this->isAnimation = true;
也就是changeFlag函数中将标志设为true
加入位置如下:

并且在做完动画时,将标志改为false

重写按钮的按下事件,判断如果正在执行动画,那么直接return掉,不要执行后续代码。
代码如下:
- void MyCoin::mousePressEvent(QMouseEvent *e)
- {
- if(this->isAnimation )
- {
- return;
- }
- else
- {
- return QPushButton::mousePressEvent(e);
- }
- }
将用户点击的周围 上下左右4个金币也进行延时翻转,代码写到监听点击金币下。
此时我们发现还需要记录住每个按钮的内容,所以我们将所有金币按钮也放到一个二维数组中,在.h中声明
MyCoin * coinBtn[4][4]; //金币按钮数组
并且记录每个按钮的位置
coinBtn[i][j] = coin;

延时翻动其他周围金币 。
- QTimer::singleShot(300, this,[=](){
- if(coin->posX+1 <=3)
- {
- coinBtn[coin->posX+1][coin->posY]->changeFlag();
- gameArray[coin->posX+1][coin->posY] = gameArray[coin->posX+1][coin->posY]== 0 ? 1 : 0;
- }
- if(coin->posX-1>=0)
- {
- coinBtn[coin->posX-1][coin->posY]->changeFlag();
- gameArray[coin->posX-1][coin->posY] = gameArray[coin->posX-1][coin->posY]== 0 ? 1 : 0;
- }
- if(coin->posY+1<=3)
- {
- coinBtn[coin->posX][coin->posY+1]->changeFlag();
- gameArray[coin->posX][coin->posY+1] = gameArray[coin->posX+1][coin->posY]== 0 ? 1 : 0;
- }
- if(coin->posY-1>=0)
- {
- coinBtn[coin->posX][coin->posY-1]->changeFlag();
- gameArray[coin->posX][coin->posY-1] = gameArray[coin->posX+1][coin->posY]== 0 ? 1 : 0;
- }
- });
在MyCoin.h中加入 isWin标志,代表是否胜利。
bool isWin = true; //是否胜利
默认设置为true,只要有一个反面的金币,就将该值改为false,视为未成功。
代码写到延时翻金币后 进行判断。
- //判断是否胜利
- this->isWin = true;
- for(int i = 0 ; i < 4;i++)
- {
- for(int j = 0 ; j < 4; j++)
- {
- //qDebug() << coinBtn[i][j]->flag ;
- if( coinBtn[i][j]->flag == false)
- {
- this->isWin = false;
- break;
- }
- }
- }
如果isWin依然是true,代表胜利了!
- if(this->isWin)
- {
- qDebug() << "胜利";
- }
将胜利的图片提前创建好,如果胜利触发了,将图片弹下来即可
- QLabel* winLabel = new QLabel;
- QPixmap tmpPix;
- tmpPix.load(":/res/LevelCompletedDialogBg.png");
- winLabel->setGeometry(0,0,tmpPix.width(),tmpPix.height());
- winLabel->setPixmap(tmpPix);
- winLabel->setParent(this);
- winLabel->move( (this->width() - tmpPix.width())*0.5 , -tmpPix.height());
如果胜利了,将上面的图片移动下来
- if(this->isWin)
- {
- qDebug() << "胜利";
- QPropertyAnimation * animation1 = new QPropertyAnimation(winLabel,"geometry");
- animation1->setDuration(1000);
- animation1->setStartValue(QRect(winLabel->x(),winLabel->y(),winLabel->width(),winLabel->height()));
- animation1->setEndValue(QRect(winLabel->x(),winLabel->y()+114,winLabel->width(),winLabel->height()));
- animation1->setEasingCurve(QEasingCurve::OutBounce);
- animation1->start();
- }
- 当胜利后,应该禁用所有按钮的点击状态,可以在每个按钮中加入标志位 isWin,如果isWin为true,MousePressEvent直接return掉即可
-
- MyCoin中.h里添加:
- bool isWin = false;//胜利标志
-
- 在鼠标按下事件中修改为
- void MyCoin::mousePressEvent(QMouseEvent *e)
- {
- if(this->isAnimation|| isWin == true )
- {
- return;
- }
- else
- {
- return QPushButton::mousePressEvent(e);
- }
- }
- //禁用所有按钮点击事件
- for(int i = 0 ; i < 4;i++)
- {
- for(int j = 0 ; j < 4; j++)
- {
- coinBtn[i][j]->isWin = true;
- }
- }
测试,胜利后不可以点击任何的金币。
QSound *startSound = new QSound(":/res/TapButtonSound.wav",this);
点击开始按钮,播放音效
startSound->play(); //开始音效
在选择关卡场景中,添加音效。
- //选择关卡按钮音效
- QSound *chooseSound = new QSound(":/res/TapButtonSound.wav",this);
选中关卡后,播放音效
chooseSound->play();
在选择关卡场景与翻金币游戏场景中,分别添加返回按钮音效如下:
- //返回按钮音效
- QSound *backSound = new QSound(":/res/BackButtonSound.wav",this);
分别在点击返回按钮后,播放该音效
backSound->play();
在PlayScene中添加,翻金币的音效以及 胜利的音效
- //翻金币音效
- QSound *flipSound = new QSound(":/res/ConFlipSound.wav",this);
- //胜利按钮音效
- QSound *winSound = new QSound(":/res/LevelWinSound.wav",this);
在翻金币时播放 翻金币音效
flipSound->play();
胜利时,播放胜利音效
winSound->play();
测试音效,使音效正常播放。
当我们移动场景后,如果进入下一个场景,发现场景还在中心位置,如果想设置场景的位置,需要添加如下下图中的代码:
MainScene中添加:

ChooseScene中添加:

测试切换三个场景的进入与返回都在同一个位置下,优化成功。
至此,本案例全部制作完成。