基于QT实现的翻金币游戏
一、设计题目
基于QT实现的翻金币游戏
二、开发环境
硬件环境:微机系列,内存在1G以上
软件环境:Microsoft Windows 10家庭版
三、开发工具
Qt Creator 4.11、HM NIS、NSIS(客户端打包程序)
四、设计思想
4.1 游戏的组成
我认为游戏的组成主要是两个部分:游戏资源和游戏机制。
游戏资源泛指游戏中给用户提供的视听资源,比如游戏的场景,各种道具和角色的外观,游戏音乐,与用户进行交互的界面等。
游戏机制泛指游戏中实现游戏运作的方式,在翻金币小游戏中,怎么排列金币,如何翻转金币,如何判断游戏的胜利与失败,这种对于用户来讲相对隐式的存在却在游戏开发中占据核心地位。游戏机制的实现通常需要数据结构与相关算法的结合。
4.2 游戏设计要点
再设计游戏时,因为此项目窗口较多,因此在对于各个窗口的选择上是一个比较需要注意的地方。在初始化各种窗口的时候,是选择QMainWindow,Qwidget,还是QDialog类型呢?这个主要看该窗口的功能,如果作为主窗口(比如交互窗口,游戏窗口)存在的话,选择QMainWindow类型最合适,因为QMainWindow可以搭载工具栏和状态栏,这些功能选择组件在小游戏里面是很实用的(尤其是工具栏)。
至于QDialog则用于对话窗口,单方向地由软件向用户发送信息,一般不做交互,游戏的游戏说明,提示,警告都适合使用QDialog类型。
QWidget适合用来自定义组件,这次的项目中没有组合类的自定义组件,因此没有使用。该项目中自定义的是单个类型的组件(新的按钮),所以直接去继承QT里面的button类就可以了。
4.3 游戏的主内容分析
我们结合游戏的规则来分析初始钱币分布的处理方式。
在翻金币游戏中,银色的钱币实际上是钱币的反面,金色的钱币是钱币的正面,翻金币游戏的游玩方式是:点击一枚硬币一次,令其实现一次正反翻转,同时使得该硬币的上下左右四个钱币跟随其一起翻转(也就是一次最多能翻转五个硬币)。翻金币游戏的成功条件是:16个硬币全部为正面(金币)就取得胜利。
我们可以简化一下内容再分析其本质,比如说“该硬币的上下左右四个钱币跟着一起翻转”,这个在设计思路上是不太重要的,因为你只要实现了一个硬币的翻转,其余的硬币你同时想翻几个就翻几个,因此可以暂时不考虑。
这样的话,游戏流程应该使用代码实现:特定的初始的关卡钱币排列、单个钱币的翻转、胜利条件的判断与游戏终止三个主内容。
了解研究内容了之后我们继续思考更细节层次的实现。
4.4 初始排列方式的分析
首先,每一个关卡都是一个固定的排列方式,其次,该排列方式是个二维矩阵。
固定,阵列,一下子就想到二维数组了。
我们可以认为,应该创建20个二维数组(因为一共有20个关卡),对应20个关卡的初始阵列,当开始初始化加载游戏界面内的钱币时,加载对应的二维数组,对该二维数组进行遍历后,判断阵列中的金币银币分布情况。
4.5 游戏核心元素的分析
游戏的核心元素显而易见:钱币。
道理也很简单,游戏里银币是钱币,金币是钱币,玩家能操作的也只有钱币,最后构成胜负条件的也还是钱币,钱币不当核心元素天理难容。
既然是个核心元素,就要考虑为它设计一个类。
该类从目前来分析,应当至少包含以下几个元素:
- 1.金币与银币的图片加载
- 2.要区分金币还是银币,应该有一个标志位
目前来看,这两个要素是必须的。
4.6 游戏操作的分析
游戏内的操作只有翻转硬币这一项。
既然我们有了钱币类,钱币对象还有判断正反的标志位,那么翻转就变得比较简单了。
不过游戏的操作过程中肯定不能让钱币瞬间翻转,应该是要加入一个动画特效进去的,因此在钱币类中应该还要加入:
3.钱币翻转的动画特效。
4.7 游戏胜负判断的分析
翻金币游戏本质上和五子棋这类游戏的胜负判断是相同的,也就是说每执行一步操作,都应当对整个游戏界面内的元素阵列做一个分析,来判断是否达成胜利条件。
五、设计过程及设计步骤
5.1 游戏总体设计
翻金币游戏分为三大场景,分别为主场景、选择关卡场景、翻金币场景。分别实现不同的游戏功能,本游戏一共有二十关,可以从第一关开始接着来,当完成第一关之后会自动跳到下一关,还可以直接选择自己想要闯的关卡。进入关卡之后会有一个4*4的网格,里面分布着金币和银币,点击银币它本身和附近的币就会翻面,当网格中只剩金币时,闯关成功。游戏的总体流程图如图6.1下所示:
图6.1 总体流程图
5.2 主场景
5.2.1 设置游戏主场景配置
点击mainscene.ui文件,设计其菜单栏如下:
设计“退出”菜单项,objectName为 actionQuit, text 为 退出;
移除自带的工具栏与状态栏
回到MainScene.cpp文件,进入构造函数中,进行场景的基本配置,代码如下:
//设置固定大小 this->setFixedSize(320,588); //设置应用图片 this->setWindowIcon(QPixmap(":/res/Coin0001.png")); //设置窗口标题 this->setWindowTitle("翻金币");运行效果如图:
实现点击开始,退出游戏功能,代码如下:
//点击退出,退出程序 connect(ui->actionQuit,&QAction::triggered,[=](){this->close();});5.2.2 设置背景图片
重写MainScene的PaintEvent事件,并添加一下代码,绘制背景图片
void MainScene::paintEvent(QPaintEvent *) { //创建画家,指定绘图设备 QPainter painter(this); //创建QPixmap对象 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); }运行效果如图:
5.2.3 创建开始按钮
开始按钮点击后有弹跳效果,这个效果是我们利用自定义控件实现的(QPushButton不会自带这类特效),我们可以自己封装出一个按钮控件,来实现这些效果。
创建MyPushButton,继承与QPushButton
点击完成。
修改MyPushButton的父类
提供MyPushButton的构造的重载版本,可以让MyPushButton提供正常显示的图片以及按下后显示的图片
代码如下:
//normalImg 代表正常显示的图片 //pressImg 代表按下后显示的图片,默认为空 MyPushButton(QString normalImg,QString pressImg = ""); QString normalImgPath; //默认显示图片路径 QString pressedImgPath; //按下后显示图片路径实现的重载版本MyPushButton构造函数代码如下:
MyPushButton::MyPushButton(QString normalImg,QString pressImg) { //成员变量normalImgPath保存正常显示图片路径 normalImgPath = normalImg; //成员变量pressedImgPath保存按下后显示的图片 pressedImgPath = pressImg; //创建QPixmap对象 QPixmap pixmap; //判断是否能够加载正常显示的图片,若不能提示加载失败 bool ret = pixmap.load(normalImgPath); if(!ret) { qDebug() << normalImg << "加载图片失败!"; } //设置图片的固定尺寸 this->setFixedSize( pixmap.width(), pixmap.height() ); //设置不规则图片的样式表 this->setStyleSheet("QPushButton{border:0px;}"); //设置图标 this->setIcon(pixmap); //设置图标大小 this->setIconSize(QSize(pixmap.width(),pixmap.height())); }回到MainScene的构造函数中,创建开始按钮
//创建开始按钮 MyPushButton * startBtn = new MyPushButton(":/res/MenuSceneStartButton.png"); startBtn->setParent(this); startBtn->move(this->width()*0.5-startBtn->width()*0.5,this->height()*0.7);运行效果如图:
不规则的开始按钮添加完成。
5.2.4 开始按钮跳跃特效实现
连接信号槽,监听开始按钮点击
//监听点击事件,执行特效 connect(startBtn,&MyPushButton::clicked,[=](){ startBtn->zoom1(); //向下跳跃 startBtn->zoom2(); //向上跳跃 });zoom1与zoom2 为MyPushButton中扩展的特效代码,具体如下:
void MyPushButton::zoom1() { //创建动画对象 QPropertyAnimation * animation1 = new QPropertyAnimation(this,"geometry"); //设置时间间隔,单位毫秒 animation1->setDuration(200); //创建起始位置 animation1->setStartValue(QRect(this->x(),this->y(),this->width(),this->height())); //创建结束位置 animation1->setEndValue(QRect(this->x(),this->y()+10,this->width(),this->height())); //设置缓和曲线,QEasingCurve::OutBounce 为弹跳效果 animation1->setEasingCurve(QEasingCurve::OutBounce); //开始执行动画 animation1->start(QAbstractAnimation::DeleteWhenStopped); } void MyPushButton::zoom2() { QPropertyAnimation * animation1 = new QPropertyAnimation(this,"geometry"); animation1->setDuration(200); animation1->setStartValue(QRect(this->x(),this->y()+10,this->width(),this->height())); animation1->setEndValue(QRect(this->x(),this->y(),this->width(),this->height())); animation1->setEasingCurve(QEasingCurve::OutBounce); animation1->start(QAbstractAnimation::DeleteWhenStopped); }运行代码,点击按钮,测试弹跳效果。
5.2.5 创建选择关卡场景
点击开始按钮后,进入选择关卡场景。
首先我们先创建选择关卡场景,添加新的C++文件
类名为ChooseLevelScene 选择基类为QMainWindow,点击下一步,然后点击完成。
5.2.6 点击开始按钮进入选择关卡场景
目前点击主场景的开始按钮,只有弹跳特效,但是我们还需要有功能上的实现,特效结束后,我们应该进入选择关卡场景
在MainScene.h中 保存ChooseScene选择关卡场景对象
//选择关卡场景 ChooseLevelScene *chooseScene = new ChooseLevelScene;我们在zoom1和zoom2特效后,延时0.5秒,进入选择关卡场景,代码如下:
//延时0.5秒后 进入选择场景 QTimer::singleShot(500, this,[=](){ this->hide(); chooseScene->show(); });测试点击开始,执行特效后延时0.5秒进入选择关卡场景
5.3 选择关卡场景
5.3.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();});运行效果如图:
5.3.2 背景设置
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); }5.3.3 返回按钮
在这里我们点击返回后,延时0.5后隐藏自身,并且发送自定义信号,告诉外界自身已经选择了返回按钮。
//返回按钮功能实现 connect(closeBtn,&MyPushButton::clicked,[=](){ QTimer::singleShot(500, this,[=](){ this->hide(); //触发自定义信号,关闭自身,该信号写到 signals下做声明 emit this->chooseSceneBack(); } ); });在主场景MainScene中 点击开始按钮显示选择关卡的同时,监听选择关卡的返回按钮消息
//监听选择场景的返回按钮 connect(chooseScene,&ChooseLevelScene::chooseSceneBack,[=](){ chooseScene ->hide(); this->show(); });测试主场景与选择关卡场景的切换功能。
5.3.4 创建选择关卡按钮
//创建关卡按钮 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); //鼠标事件穿透 }运行效果如果:
5.3.5 创建翻金币场景
点击关卡按钮后,会进入游戏的核心场景,也就是翻金币的场景,首先先创建出该场景的.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 要提供重载的有参构造版本,来接受这个参数
5.4 翻金币场景
5.4.1 场景基本设置
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();}); }5.4.2 背景设置
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); }5.4.3 返回按钮
//返回按钮 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(); } ); });在ChooseScene选择关卡场景中,监听PlayScene的返回信号
connect(pScene,&PlayScene::chooseSceneBack,[=](){ this->show(); delete pScene; pScene = NULL; });5.4.4 显示当前关卡
//当前关卡标题 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, this->width(),50 )); label->setAttribute(Qt::WA_TransparentForMouseEvents);假设我们选择了第15关卡,运行效果如果:
5.4.5 创建金币背景图片
//创建金币的背景图片 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); } }运行效果如图:
5.4.6 创建金币类
我们知道,金币是本游戏的核心对象,并且在游戏中可以利用二维数组进行维护,拥有支持点击,翻转特效等特殊性,因此不妨将金币单独封装到一个类中,完成金币所需的所有功能。
创建金币类 MyCoin
并修改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);运行效果如图
5.5 引入关卡数据
当然上述的测试只是为了让我们知道提供的对外接口可行,但是每个关卡的初始化界面并非如此,因此需要我们引用一个现有的关卡文件,文件中记录了各个关卡的金币排列清空,也就是二维数组的数值。
5.5.1 添加现有文件dataConfig
首先先将dataConfig.h 和 dataConfig.cpp文件放入到当前项目下:
5.5.2 添加现有文件
其次在Qt_Creator项目右键,点击添加现有文件
5.5.3 完成添加
选择当前项目下的文件,并进行添加
5.5.4 数据分析
我们可以看到,其实dataConfig.h中只有一个数据是对外提供的,如下图
在上图中,QMap<int,QVector<QVector>>mData;都记录着每个关卡中的数据。
其中,int代表对应的关卡 ,也就是QMap中的key值,而value值就是对应的二维数组,我们利用的是 QVector<QVector>来记录着其中的二维数组。
5.5.5 测试关卡数据
在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中的数据来对关卡进行初始化了
5.6 初始化各个关卡
首先,可以在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]; //记录正反标志运行测试各个关卡初始化,例如第一关效果如图:
5.7 翻金币特效
5.7.1 MyCoin类扩展属性和行为
关卡的初始化完成后,下面就应该点击金币,进行翻转的效果了,那么首先我们先在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);5.7.2 创建特效
当我们分别启动两个定时器时,需要在构造函数中做监听操作,并且做出响应,翻转金币,然后再结束定时器。
构造函数中 进行下列监听代码:
//监听正面翻转的信号槽 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; //数组内部记录的标志同步修改 });5.7.3 禁用按钮
此时,确实已经可以执行翻转金币代码了,但是如果快速点击,会在金币还没有执行一个完整动作之后 ,又继续开始新的动画,我们应该在金币做动画期间,禁止再次点击,并在完成动画后,开启点击。
在MyCoin类中加入一个标志 isAnimation 代表是否正在做翻转动画,默认isAnimation值为false。
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); } }5.8 翻周围金币
将用户点击的周围 上下左右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; } });5.9 判断是否胜利
在PlayScene.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() << "胜利"; }5.10 胜利图片显示
将胜利的图片提前创建好,如果胜利触发了,将图片弹下来即可
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(QAbstractAnimation::DeleteWhenStopped); }5.11 胜利后禁用按钮
当胜利后,应该禁用所有按钮的点击状态,可以在每个按钮中加入标志位 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; } }测试,胜利后不可以点击任何的金币。
5.12 音效添加
5.12.1 开始音效
QSound *startSound = new QSound(":/res/TapButtonSound.wav",this);点击开始按钮,播放音效
startSound->play(); //开始音效5.12.2 选择关卡音效
在选择关卡场景中,添加音效
//选择关卡按钮音效 QSound *chooseSound = new QSound(":/res/TapButtonSound.wav",this);选中关卡后,播放音效
chooseSound->play();5.12.3 返回按钮音效
在选择关卡场景与翻金币游戏场景中,分别添加返回按钮音效如下:
//返回按钮音效 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();测试音效,使音效正常播放。
5.13 优化项目
当我们移动场景后,如果进入下一个场景,发现场景还在中心位置,如果想设置场景的位置,需要添加如下下图中的代码:
MainScene中添加:
ChooseScene中添加:
测试切换三个场景的进入与返回都在同一个位置下,优化成功。
六、测试运行
6.1 主场景
对游戏的每个步骤进行测试,首先进入主场景如图7-1所示:
图7-1 主场景
6.2 选择关卡场景
进入主场景后点击START按钮,进入选择关卡场景,如图7-2所示:
图7-2 选择关卡场景
6.3 翻金币场景
选择第三关进入第三关卡的翻金币场景,如图7-3所示:
图7-3 翻金币场景
6.4 游戏胜利场景
再翻金币场景下翻转金币,游戏胜利后进入游戏胜利场景。如图7-4所示:
图7-4 游戏胜利场景
七、评价与修订
翻金币游戏分为三大场景,分别为主场景、选择关卡场景、翻金币场景。分别实现不同的游戏功能,本游戏一共有二十关,可以从第一关开始接着来,当完成第一关之后会自动跳到下一关,还可以直接选择自己想要闯的关卡。进入关卡之后会有一个4*4的网格,里面分布着金币和银币,点击银币它本身和附近的币就会翻面,当网格中只剩金币时,闯关成功。
游戏对游戏资源和游戏机制进行了很好的内外融合,不管是背景音乐、游戏成功还是翻金币音效都是找的比较契合的音效。并且与用户交互的界面也是设计的简洁易懂,不繁琐一眼就明白游戏的流程和操作方法。在游戏机制方面,对于游戏的胜利和失败都有很好的解决办法,可以让用户更好的进行下一关和重开游戏。使用户的粘度增加,在游戏娱乐和闯关的同时也可以很好的进行益智。达到了在学习中娱乐,在娱乐中学习。
美中不足的就是游戏的关卡数量和游戏的模式有待增加,过于单一会导致游戏用户的厌倦性和积极性后期对于游戏的难度和模式会做出相应的增加。增加用户的登录和游戏记录,以及众多用户的pk等功能,这个在学习后面的知识的时候会进行与相应的知识结合,做出更好的解决方案和游戏机制。