您的位置:首页 > 编程语言 > C语言/C++

Qt/C++ 开发Android平台《林中伊人》消球小游戏全记录3——Qt控件的两员大将QLabel和QPushButton

2014-07-20 11:42 375 查看

引子

前面的文章介绍了设计小游戏的灵感起源以及开发环境的建立,终于该说说重头戏了,这个游戏在程序上是如何来实现的~

对于一个游戏来说,程序部分是它的骨骼和血肉,有了程序,它才能运行起来、跑起来。针对程序这部分,我习惯笼统的把它分为展现给玩家交互的可视界面和隐藏在背后运作的逻辑算法

刚开始写程序时,完全没经验,经常会停下来研究该怎么来表现游戏中那些应有的元素~于是各种查找,Qt中有哪些是比较方便使用的“类”?可以帮助实现哪些我想要功能?C/C++中哪些逻辑关系能更有效率的实现相对复杂的运算?等等等等……其实每解决一个问题,每学习一种新的知识,都会给自身带来很大的提高,会帮助你走的更远,但同时遇到新的问题的几率也会更大,没关系,继续学习和提高就是了~

说到这里我有时候会情不自禁的感叹,业余时间做1个小时游戏所收获的东西远比在办公室里8个小时机械劳动写代码所学到的要多多了,这其中的原因我认为是自己做项目可以自由把控、自由学习,提高非常有针对性,相比之下,听从领导指挥的重复劳作效率很低,虽说也不是什么都学不到,但时间浪费太严重。这些是题外话,总之,在这样不断学习和提高的过程中,慢慢的积累、总结出了一些经验,在这里简单说说若干Qt/C++的“用法”,也算对自己的收获有一个交代。很多地方都不够专业和成熟,希望大家“去糟取精”,多包涵,多交流,多提意见~

可视界面

众所周知,Qt最大的特色是“跨平台”,而还有另一个重要的特点是它搭建界面非常快捷方便。说道界面部分,其实也就是展现给玩家看的可视部分,这其中就少不了Qt控件的功劳,Qt中形形色色的控件让人眼花缭乱,可以实现各种各样的交互功能,还可以根据需要自己来定义控件,实现更有针对性的效果。做游戏的时候,Qt中常用的那些控件我基本都试用过了,不过试验之余、熟练之后,根据需要,最后只精简的剩下两个:QLabelQPushButton,一个用来“显示信息”,一个用来“建立连接”。其实只要有了这两员大将,基本其它控件的功能也都可以通过自定义来实现了~嚯嚯~可能有的人说了,那你还不如就只用一个QWidget得了,想实现啥功能全部都自定义~其实也不是完全没有道理,本来绝大部分的控件就是QWidget的子类。说白了,还是在设计游戏的过程中,自己感觉怎么方便就怎么来,这个东西没有铁定的规则,只有更好的点子~

1. QLabel——显示文字、图片我全能

QLabel,标签,最开始接触这个控件类的时候,基本只会用它来显示个文字而已,众多的教学贴、教学视频里也鲜有提到用它来显示图片的(毕竟不是主要功能)。但后来用的多了我就发现,反正总会要用Qlabel来显示文字,它的出场是必不可少的,那干脆让它身兼数职好了~以至于到了现在,做某些场景时甚至连背景图都用QLabel来显示,很多人听了可能都会觉得奇葩~下面就先大致罗列一下《林中伊人》这款游戏中都有哪些地方是用到了QLabel,以及在代码上的实现方法:







a. 开始界面 b.
游戏界面
c. 排行榜界面

文字、数字——QLabel是最常见的用来显示文字和数字的控件,所以这一用法放在首位,如果只是显示文本直接使用setText函数即可,然后用setFont设置字体,setAlignment设置文字位置(上、中、下,左、中、右),setPalette设置字体颜色。例如,“图b”里顶部的“积分标签”,以下代码是“积分标签”各个显示属性的具体设置:

m_labScore = new QLabel(this);    //创建一个新的QLabel控件
m_labScore->setGeometry(iX, iH, iW, iH);    // 设置在场景中的位置和大小
QFont fontText;    // 字体类
fontText.setFamily("Timer");    // 字体类型
fontText.setBold(1);    // 字体加粗
fontText.setPointSize(25);    // 字体大小
ui->labRecord->setFont(fontText);    // 给标签应用字体
fontText.setPointSize(30);
m_labScore->setFont(fontText);
m_labScore->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);    // 文字在QLabel中显示的位置,居中
QPalette paletteText;    // 调色板
paletteText.setColor(QPalette::WindowText,QColor(230, 30, 30));    // 设置字体颜色
paletteText.setColor(QPalette::ButtonText, QColor(230, 30, 30));
paletteText.setColor(QPalette::Text, QColor(230, 30, 30));
m_labScore->setPalette(paletteText);    // 给标签应用调色板



静态、动态图层——QLabel用来显示图片时的位置、大小以及在场景中的上下遮挡关系都是可以灵活控制的,在此称之为一个图层。

静态显示如“图a”中的游戏标题和路牌,以及最下方的草。只需要设置好它们在场景中的位置和大小,然后显示出图片即可。QLabel显示图片有两种方式,用setStyleSheet函数或者setPixmap函数,代码如下:

m_labBottom->setStyleSheet("QLabel {image: url(:android/BottomLabel.png);}");



或者

labObject->setPixmap(QPixmap::fromImage(imgBGP).scaled(iWidth, iHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));<span style="font-size: 18px; ">        </span>


setStyleSheet使用起来非常方便,它可以很自由的设置各类控件的属性以及显示效果,功能异常强大,如果学过HTML,CSS和JS的,想必会对这一部分的语法更加轻车熟路~很多人喜欢直接用它来给各种控件填充颜色,但我更喜欢用它加载图片,显示速度足够快,不会有延迟,而且图片能更好自己来设计和把控。这种方法会使图片自适应QLabel的大小,但是并未对图像做平滑处理,因此会出现锯齿,推荐显示小的图片,例如此游戏中的气泡等。

setPixmap加载图片的速度相对会慢一些,尤其是应用了Qt::SmoothTransformation的平滑处理(不做处理的话,如果QLabel大小和图片大小不一致,显示出来的图片会带有锯齿),会影响游戏的运行效率,因此静态显示某个图层或者是大的背景图时,需要良好的图像显示效果时可以选择这种方法。至于速度慢的原因,除了一部分时间耗在了图像处理上,还有一部分耗在了QImage到QPixmap的转换上。

动态显示如“图a”中四处随机飘动的气泡以及图b中底部不断新生成的气泡。动态显示图片与静态的方法完全一样,只需要根据需要在特定的时机和环境下通过setGeometry、move等函数来改变QLabel的位置或者大小,通过setVisible函数来控制QLabel的显示与否即可。如果想实现更复杂的动态效果,可以进一步结合QTimer定时器以及QPropertyAnimation属性动画来实现,详细的使用方法参考《动画的艺术》一文。

2. QPushButton——连接“游戏世界”的纽带

QPushButton,按钮,这个控件可是个好东西。如果光有静态和动态的图片摆在那里显示,岂不是成了漫画、照片或者动画、电影了,那就不叫游戏了。游戏要有交互,交互的主要方式无非就是通过各种连接connect、信号signal和相应的槽(响应)slot来实现的。按钮则是最常用、易懂的一种建立此类连接的控件:点击一下按钮,发出一个信号,响应一段命令,实现一定效果。详细使用方法这里不做描述,这个控件类太常见了,在网上搜它的用法和示例一大堆。

下面只来说说怎么做出一个漂亮的按钮。其实跟QLabel显示图片的道理一样,做一个漂亮的按钮,也是要通过给QPushButton这个控件加载相应的图片来实现,只不过按钮除了正常状态下看到的样子,还会多出一个悬停状态Hover和一个按住状态Press,分别给这两种状态也加载上图片就OK了,用setStyleSheet函数或者setIcon函数来实现,代码如下:

void SetWidgetBGP_Class::SetBtnBGP(QPushButton *btnObject, QString strBGP)
{
QString strStyle = "QPushButton {border-image: url(:android/" + strBGP + ".png);}";
strStyle += "QPushButton:hover{border-image: url(:android/" + strBGP + ".png);}";
strStyle += "QPushButton:hover:pressed{border-image: url(:android/" + strBGP + "Press.png);}";
btnObject->setStyleSheet(strStyle);
}



或者

void SetWidgetBGP_Class::SetBtnBGP(QPushButton *btnObject, QImage imgBGP, int iWidth, int iHeight)
{
QIcon iconBGP;
iconBGP.addPixmap(QPixmap::fromImage(imgBGP).scaled(iWidth, iHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
btnObject->setIcon(iconBGP);
btnObject->setIconSize(QSize(iWidth, iHeight));
btnObject->setFlat(1);
}



setStyleSheet的语法与之前给QLabel用的基本一致,都是通过“url(图片相对/绝对路径)”来实现图片加载的(这里不得不吐糟一下,太多人使用setStyleSheet只是给控件上个纯色,最多来个渐变色,撑死了再加上一个80%透明度啥的,这怎么能称得上是一个漂亮的控件呢?Qt确实具有强大的“上色”功能,但是再强大,强大的过Photoshop做出来的图片效果么?无论是游戏还是其它任何级别的应用软件,如果想要有一个好的卖相,从任何角度来讲,更多的需要是“贴图”~而不是“上色”~与其研究如何上色的更漂亮,倒不如多想想如何更高效率的载入图片~),只不过按钮的语法中,多了两个状态,hover和pressed,同时注意,这里最好用border-image,会自动根据按钮大小来扩展图片,如果用的是background-image,则会出现白边,造成这一现象的原因、原理请参见官方或第三方文档对QPushButton属性的详细描述。同样的,虽然会根据控件大小自动调整图片的显示,但是因为没做平滑处理,锯齿现象不可避免,好在按钮在场景中一般不会太大,就算有锯齿也基本在肉眼可忍受的范围内。PS:如果有大神知道如何解决使用setStyle加载控件背景图片的平滑处理问题,还请不吝赐教~非常感谢~

setIcon与setPixmap的用法表面上差别不大,同样是需要经过QImage到QPixmap的转换,这当然会让程序在运行效率上打折扣,需要酌情使用,该损耗跟图片和控件的大小有着直接的关系。

使用setIcon的方法给QPushButton加载图片还需要注意两点:如果不使用setFlat函数,就会出现默认的按钮边框(可以想象一下,你做了一个圆按钮,结果有一个方形边框,嚯嚯~);如果想同时显示图片和文字,请使用setStyleSheet方法,setIcon这一方法是行不通的,因为用完setIcon再用setText给QPushButton加载文字,图片和文字会依次并列显示,而不是文字浮在按钮背景图上方,因为setIcon本就不是给按钮加载背景图的,而是给按钮设置一个图标,只不过用setIconSize把图标大小调整为和按钮一样了。PS:其实都可以自由加载背景图片了,做图片时把你想要的文字加上去就好了,就是想要改动文字时图片还得重做,我的解决方法时,保留好原始PSD文件,有需要改动文字时,在上面改好直接新生成一个图片就好,具体方法参见《半个美工的诞生》一文。

最后简单说一下QPushButton的连接和自定义按钮的使用。Qt提供了良好的建立连接、响应的机制,利用connect函数,告诉它是“谁发出的信号”,“是哪个信号”,“由谁来响应这个信号”和“接受到之后执行哪个槽函数”,对于QPushButton来讲,发出信号的自然就是按钮本身,信号默认的是clicked()信号,一般由按钮的父控件来响应(除非有特殊需求),并执行相应的槽函数来实现相应效果即可。拥有了这些,80%的情况下都够用了,剩下的20%就是有特殊需求的情况,可以通过自定义按钮来解决,以《林中伊人》游戏界面中可以点击消除的气泡按钮为例子,如果只用默认的clicked()信号,则需要在相应的槽函数中进行判断循环,利用for循环加上if(sender()
== btn[i])语句来找到是哪个气泡(按钮)发出的信号(被用户点击了),虽然对游戏执行效率影响不大,但是这只是一个11行 X 8列的气泡矩阵,如果是更庞大的一个按钮群体呢?通过自定义一个信号sigClick(int iRow, int iCol)来直接告诉响应它的槽函数,该信号是出自于那个位置的气泡,这样会更方便快捷,但需要注意的是,自定义信号和它对应槽函数中的形式参数可以给出名称,但是在connect函数中,只需给出这些参数的类型即可(否则conenct连接会失效),如以下代码:

connect(m_bubbleClick[i][j].btnBubble, SIGNAL(sigBtnClick(int, int)), this, SLOT(slot_BubbleClick(int, int)));


以上,《林中伊人》中和可视界面有关的部分就基本介绍完了,我只是从本游戏中用到的知识点来介绍和分析的,所以并没有针对这两大控件进行全面的描述和解析,说的不到位的地方多多包含,如果哪位朋友有更好的用法和点子希望可以分享出来,再次谢过~

逻辑算法

前文中提到,这款消除类游戏我是从联想i909手机的一款java游戏中获得的灵感,其实说是灵感,但基本意思就是说,通过玩那款小游戏,我分析出了它背后的算法是如何实现的,于是自己也可以做一个同类型的消除游戏,仅此而已。这个过程是不是有点儿像是照着葫芦画瓢呢?毕竟我没有那款游戏的源码,更不知道它背后是如何运算来实现游戏的运行的,所以只能通过观察它表面的游戏规则和游戏过程中的表现形式来自行摸索,并打造一套属于自己的算法,当然了,最终实现的效果肯定是大同小异,就算有些许创新,但毕竟是同类型的游戏。

消除游戏连连看对对碰有所不同,我觉得消球游戏消除类游戏中更简单一些(这也是我选消球游戏入手的原因之一,另一个原因是连连看对对碰俄罗斯方块什么太泛滥了,做起来着实在没意思,消球游戏相对冷门一些~我就喜欢奇葩~^V^~),只要出现了相同颜色的气泡连在一起的情况,你一点击,它就会被消除掉了,相对的,连连看需要“寻径”,在算法上更复杂一些,而对对碰需要在交换位置后再进行颜色是否一致的判断,在时机把握上要求更高一些。不过消球游戏还是有它自身魅力所在的,当一列被消空了之后,两侧的泡泡会向中间靠拢,同时产生出新的组合,另外底部不断生成的新泡泡也会给玩家带来逼倒计时更强的紧迫感。

虽然没有篇幅限制,但是没必要逐行逐段的去讲解算法的每一行代码,简单说一下思路和实现方法吧:

1. 气泡的生成

气泡的生成包括了游戏一开始就已经存在的“气泡墙”,随着难度级别不同,“气泡墙”的高度也是不一样的,再就是最底部不断新生成的泡泡,每积攒够一行,就会把整个“气泡墙”往上推,同时继续开始产生新的一行气泡。方才说的这些气泡的生成,都是随机产生的,这里我使用的是Qt自带的qsrand随机函数外加QTime获取的系统时间转换为毫秒级作为随机种子。使用
srand
((unsigned)
time
(NULL))也没问题,
据Qt自带文档介绍,qstand是srand的“线程安全版”,可以说就是同一个东西,注意时间种子一定要是毫秒级的,否则不够随机。

2. 气泡的消除

点中某一个气泡后开始进行颜色判断,寻找它的“上下左右”有没有颜色一样的,如果没有,则不再有任何动作响应,只要有一个方向上有颜色相同的,则会顺藤摸瓜继续找寻下去,直到找不到颜色相同的为止。当然了,只要是被检查过的气泡都会被打上标签,这一点很重要,因为找寻同颜色气泡的函数是递归的,避免它一再重复地找寻陷入死循环。该函数部分代码如下:

void JungleBubble_Widget::FindSameColorBubble(int iRow, int iCol)
{
if(iRow > 0)    // 向上找寻同颜色气泡
{
if(!m_bubbleClick[iRow-1][iCol].bCheck && m_bubbleClick[iRow-1][iCol].iColor == m_iCurClickColor)    // 检查上方的气泡颜色是否与被点击气泡颜色一致
{
m_iSameBubbleRowIdx[m_iSameBubbleCount] = iRow-1;        // 如果颜色一致,记录下该气泡所在的行与列,消除时会调用
m_iSameBubbleColIdx[m_iSameBubbleCount] = iCol;
m_iSameBubbleCount ++;                                   // 又多了一个颜色相同的需要被消除的气泡
m_bubbleClick[iRow-1][iCol].bCheck = 1;                  // 给检查过的气泡标注一下
FindSameColorBubble(iRow-1, iCol);                       // 递归查找
}
}
......
}


只贴出来了往上找寻的这一部分代码,往左、往右、往下如出一辙~找寻完之后,就会把找到的同颜色气泡消除掉。

3. 气泡的移动

第一种情况,如果新生成的气泡积攒满了一行,则所有气泡往上推,这时需要更新气泡的颜色信息,同时还需要判断最顶层是否存在气泡,如果有,再往上推气泡就出界了,则游戏结束;

第二种情况,消除了某些气泡之后,如果在这些被消除气泡的上方还存在气泡,则上方这些气泡需要下落,同时更新它们位置信息;

第三种情况,某一列气泡被消除空之后,如果这一列在场景的左半边,则他左侧的泡泡全部向右平移,如果这一列在场景的右半边,则他右侧的泡泡全部向左平移,同时更新气泡位置信息。

以上就是气泡会出现移动的几种情况,每种情况发生时,不但要进行背后的逻辑运算、更新相应的参数,还要在可视化上让这些气泡显示出相应的动画效果,在《动画的艺术》一文中会提到。

4. 其它算法

除了游戏主要部分的逻辑运算之外,还需要诸如计算积分积分排行模式解锁等逻辑上的运算和判断,这样一个游戏才会完整。

算法部分就介绍这么多,其中一些比较基础、大众、没有特色的部分在这里就省略掉了,有兴趣的童鞋可以通过别的途径来进行更多的学习、交流和探讨。

不过我们也可以看出,游戏虽小,但五脏俱全,无论是界面部分还是算法部分,涉及到的方面都是很广的,真要是一条条的细数,又岂能是区区几篇文章所能包罗全的呢~出本书还差不多~各位看官加油~

———— 2014/7/19 飞飞鼠

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐