游戏截图
源码:https://github.com/CHampppppppp/PVZ_qt
灵感和基本框架来源:https://github.com/GeeeekExplorer/PVZ
基本思路
Qt几个重要的库的运用
QGraphicsScene(以下简称scene)和QGraphicsItem(简称Item)两个库,Item库用来管理item(物件),我们把僵尸和植物,包括卡片,推车等一切会动的能交互的物品都看作一个个item,我们要做的就是给他们名字和功能(定义头文件),然后实现他们(cpp文件),最后将这些item放到窗口中,然而,这个窗口不是一般的窗口,称之为容器更合适,这个容器要能批量管理大量的item,上面提到的scene便是这样一个容器,我们把item们放到里面,让它管理,接着,我们传入item的图片或者gif,QGraphicsItem的painter函数画出来,然而,我们要显示它们,还需要另外一个库,QGraphicsView,一般来讲,scene和view通常是一起出现的,scene管理,view显示。通常这样用:
QGraphicsItem* item=new QGraphicsItem;
QGraphicsScene* scene=new QGraphicsScene;
scene->addItem(item);
QGraphicsView* view=new QGraphicsView(scene);//将scene和view绑定一起
view->show();//运行后就能看到item
PVZ的整体框架
从上到下分为三层,第一层是最基本的QGraphicsItem和QMainWindow类,里面包含了一系列重要的函数原型,之所以说原型,是我们要在这些函数的基础上加上自定义的内容,包装成我们想要的效果(后面会详细解释)。第二层是继承自QGraphicsItem的Zombie类、Plants类和other类,还有继承自QMainWindow的mainwindow类。第三层,就是细化的各种僵尸(继承自Zombie类),各种植物(继承自Plants类)和其他物品如卡片,阳光,推车等(继承自other类)。为什么要继承呢?一是方便管理,减少重复用功,大部分僵尸除了动画和一些属性的不同,其他属性大部分相同,植物也一样,所以继承自基类,而再向上之所以继承自QGraphicsItem,是因为这个库里面提供了管理item(植物、僵尸、其他物品)的功能(这些功能包括实时调整物体的状态并且根据判断条件做出变化,移动,或是删除和创建)。这样一来,我们有了容器,有了物品,放到容器里面,管理他们,显示他们,一个游戏就做好了。
想屁吃呢,没那么简单。
但是其他的东西又多又碎,一时间也不知道该从哪里讲起。。直接看代码吧,应该能理解的。
遇到的问题和解决方法
1.Qt跟其他IDE有啥区别,为什么不能在vs里写,非得在Qt写?
首先,在vs等其他ide里面也能写pvz,只是Qt写的效率更高,效果更好。
信号与槽机制:Qt 引入了信号与槽机制,这是一种事件驱动的编程方式,能够方便地处理对象之间的通信。其他 IDE 和框架虽然也支持事件处理,但 Qt 的实现方式独特且易于使用。
一系列方便的图形库和函数:Qt里面的Item,Scene,GraphicsView相关的函数种类齐全,简单易用,只需查看Qt的文档就能知道各种函数的使用方法,只是文档是全英文的,但是我们也不缺少翻译软件。
2.QGraphicsItem库?
拿Zombie类举例,继承自QGraphicsItem,说明zombie类是item(字面意思),我们给这个item定义hp(血量),atk(攻击力)等属性,注意,在QGraphicsItem里面,item可以有很多个,区分不同的item可以靠这个type值,一般来说,默认值是UserType,这里我们可以把想要区分的item的type值与默认值区分开,要怎么区分都行,乘10,乘100,我这里就简单地+1,+2等等。
讲完变量,讲讲继承自QGraphicsItem的函数,正是这些函数才让我们甘愿做QGraphicsItem的儿子甚至是孙子。
boundingRect()
这个函数,返回一个矩形的边界,在这里我们重写这个函数,自定义它返回的矩形边界,用不同的边界来框住大小不同的item,这样我们后面判断两个item是否碰撞只要通过判断所处的矩形是否重合就行了。为了能充分准确地返回不同植物的矩形边界,每个植物都要重写这个函数,返回不同的矩形面积,至于是多大,可以在前期把这个矩阵边框画出来,自己根据肉眼调整。对于这里的樱桃炸弹来说,它在爆炸前(state==1)和爆炸时(state==2)是两个大小不同的gif,矩形边界相同,gif大小不同,画出来就大小不同,所以对于比较小的gif,设置较大的矩形边界,看起来大小就差不多了。
Qpainter()
提到painter就说一下吧,painter是QGrphicsItem库中用于画出item的函数,对于option和widget参数,我们用不到,就用Q_UNUSED()删掉。对于每个植物都有它自己的gif动画,我们只需要将gif的每一帧图像截取下来并用painter画出来就好了(第一个参数指明绘画的矩形边框,前面提到过。第二个参数就是要画的图像)。如你所视,那个注释掉的就是前期画出矩形边框用于调试其大小以充分贴合植物本体的代码。
collidesWithItem()
这个函数与上面的boundingRect()相关联,返回是否与其他item的矩形边界重合的布尔值,这里我们重写它,将item限定为僵尸,并且不再只是边界重合才算碰撞,只要僵尸类item出现在当前行(纵坐标相同),就判断为碰撞,后面再根据是否碰撞决定植物是否开火。避免植物们自相残杀。
advance()
说到开火,你大概会思考怎么让植物开火呢,植物只是一个gif,要让它开火,就把豆子的图片生成在它嘴里,再让豆子持续增加横坐标,让它动起来。 QGraphicsItem好就好在这里,我们只需改变递增物品的坐标,再让painter画出来就行,不用每一帧都手动删除,重建物品,或者每一帧都生成一张全新的图片,只需update()物品就行了。advance(),参数是phase,如果phase为零,意味该物品不用改变,phase不为零,意味着物品发生变化,执行update()更新物品。注意:advance()函数在程序运行的过程中以很快的速度重复执行,每时每刻都在判断物品是否应更新,并做出相应更新。如果植物hp<0,植物死亡,delete 该物品。根据不同的植物设定不同的状态(state),例如这里的樱桃炸弹,state==1时准备爆炸,等到当前的动画帧播放到最后一帧时state变成2(爆炸),播放爆炸的gif,以QList(类似于链表)存储附近发生碰撞的item,遍历链表,如果item为僵尸,设定僵尸的状态为BURN(烧伤),僵尸hp=0,等到爆炸的gif播放到最后一帧,删除cherrybomb,僵尸的删除部分在僵尸的advance()函数里,设定为如果僵尸的状态为BURN,则播放僵尸被烧成灰的gif,同理,播放完删除僵尸。
3.QMainwindow库?
1.QMainWindow,是一个与显示窗口相关的库,如果什么都不写,运行的话是一个空白的窗口。(在main函数里面运行,如下)
如果要编写窗口内的内容,我们要创建一个继承QMainwindow的类,在头文件里面定义这个mainwindow类,然后再在mainwindow的cpp文件里实现,如下:
mainwindow.h
mainwindow.cpp
想法
1.感受到代码文件归类整理的重要性。这是我个人写的第一款代码量较大的游戏,尽管不少模仿和借鉴,但还是不得感慨个人的能力范围确是有限,这个代码量其实还好,如果遇到更加复杂的项目,缺少清晰的代码管理很容易会造成开发过程中反复横跳找上下文,经常是写到后面忘了前面,或者是找关联的文件找半天,颇影响开发效率。之前我不喜欢头文件,直接声明函数接着实现都写在同一个文件里,我觉得这样方便实时查看函数的内部代码,但其实工程量大起来的话,还是分开写比较清晰易懂。
2.兴趣是最好的动力,此言不用多说。
3.学的时候考虑的是怎么让自己搞懂,挺难的,写博客的时候想的是怎么让别人简单地看懂,更难啊。