【Qt OpenGL教程】21:线、反走样、正投影和简单的声音
2015-08-09 18:16
615 查看
第21课:线、反走样、正投影和简单的声音 (参照NeHe)
这次教程中,我们将介绍线、反走样、正投影和简单的声音,这是第一个大教程,希望这一课的东西大家能够喜欢(NeHe原文中有介绍计时器,但是Qt已经为我们封装好了计时器,所以这次教程中我省略了这部分,有兴趣了解VC中设置计时器的请点击这里)。
在这一课里,我们将学会绘制直接,使用反走样,正投影,基本的音效和一个简单的游戏逻辑,希望这里的东西可以让你高兴,毕竟我们会完成一个游戏!在这一课的结尾,你将获得一个叫“GRID CRAZY”的游戏,你的任务是走完每一段直线。这个程序有了一个基本游戏的一切要素:关卡,生命值,声明和一个游戏道具。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课代码的基础上修改代码,这次是个大程序,我们一一解释新的内容和游戏逻辑,希望大家能理解和喜欢这第一个openGL游戏(虽然只是2D游戏)。首先打开项目文件(.pro文件)和myglwidget.h文件,将两个文件内容更改如下:
项目文件中,我们增加multimedia部分,这使得我们能够使用QSound等媒体播放对象。
myglwidget.h文件中,首先我们增加了2个布尔变量数组m_Vline和m_Hline,用于记录垂直方向和水平方向各110段线段是否走过。继续是3个布尔变量,当网格被填满时,m_Filled被设置为true而反之则为false;m_Gameover的作用易见,当它的值为true时,游戏结束;m_Anti指出抗锯齿功能是否打开,当设置为true时,该功能是打开着的。
接下来是5个整形变量和1个static const 整形数组,m_Delay用来减缓敌人的行动,其实就是当m_Delay小于某个值时,敌人不能移动(由于画面刷新很快,m_Delay变化也快,你是看不出敌人有很小一段时间没动的)。m_Adjust用来控制玩家和敌人移动的速度,其实就是控制玩家和敌人每一步能走多远,我们通过和下面的s_Steps[]数组配合一起完成这一控制目的。m_Lives保存了玩家的剩余生命值,m_Level保存了游戏内部的等级难度,m_Level2保存了显示出来的游戏难度,m_Stage保存了游戏的关卡(m_Level、m_Level2和m_Stage的区别后面大家会明白的,不用在这里纠结)。m_Steps[]保存了可供m_Adjust选择的数值。
然后我们定义了一个结构体来记录游戏中的对象。fx和fy记录每次在网格上移动我们的英雄和敌人的精确像素位置,x和y则记录着对象即将移动到网格交点是哪个。而最后一个变量spin用来使对象在z轴上选择。定义完后,我们就利用这个结构体创建我们的玩家对象,敌人对象(最多有9个所以是长度为9的数组)和宝物沙漏对象(m_Hourglass)。
还有我们需要载入两个纹理,所以有了m_FileName[2]和m_Texture[2。而m_Base储存字符显示列表的开始值,m_Sound用来指向一个QSound对象,该对象保存了吃到宝物后的计时音乐(注意声明QSound时需要在类前面加上class QSound声明)。最后是5个新的函数的声明,作用大家先看注释留个印象吧,后面会慢慢来介绍解释。
接下来,我们打开myglwidget.cpp,加上声明#include <QTimer>、#include <QSound>、#include <QTime>、#include <QCoreApplication>,在构造函数中对新增变量进行初始化并修改析构函数,具体代码如下:
我们来先看下我们几个新增函数的定义,说是新增,其实有3个已经是老面孔了,3个我们比较熟悉的函数代码如下:
下面来看看两个真正新面孔的函数,说一下updateData()函数时重点之一,具体代码如下:
进入重点的updateData()函数,它的作用是在每绘制一帧图像后,对游戏各部分数据进行适当准确的更新,为下一帧的绘图提供数据。首先,if语句判断游戏是否结束,没有结束就进入数据更新,进入游戏循环遍历每一个敌人。循环中,一开始我们利用4个if语句,让敌人根据玩家的位置去追击玩家。接下来的if
((m_Delay > (3-m_Level)) && (m_Hourglass.fx != 2))中,前一个语句表示,当前时间不为敌人的延迟时间(敌人在延迟时间中不运动,由于每秒画面帧数很多,延迟时间又很短,所以并看不来有延迟,通过设置延迟时间来减缓敌人的运动速度);后一个语句表示,当前时间不为吃到宝物沙漏后敌人的静止时间。当if条件满足时,说明当前为运动时间,就通过4个if语句让敌人按照当前前进方向调整位置并调整旋转角度(前面4个if语句的目的是当敌人在某一交点时确定下一步的移动方向,而这后面4个if语句是为了实现敌人在相邻两交点之间的运动动画)。注意到我们每次移动的距离是由s_Steps[]和m_Adjust来确定的,这就是为什么m_Adjust能控制移动的速度。
然后我们判断敌人的位置和玩家的位置是否重合,如果重合,玩家会减少1点生命,并使玩家和敌人的位置重置,并利用QSound播放死亡音乐,如果这是玩家的生命减为0,则游戏结束,置m_Gameover为true。值得注意的是,在QSound::play()函数下面,我们设置了一个QTime,让它开始计时,并通过while循环不断检测当前的时间,如果达到2000ms则结束循环。我们这么做是为了在播放死亡音乐时,让程序产生2秒的等待,不会直接进入下一个画面。当然循环中,QCoreApplication::processEvents()这行也是十分重要的,它保证了在while循环过程,音乐播放事件能顺利执行。最后我们用4个if语句调整玩家的位置,让动画看起来自然,这里的机理和上面敌人后面4个if语句的机理是一样的。到这里,最开始的那个if语句就结束了。
下面一开始if语句判断m_Filled是否为true,为true说明已经顺利通过本关,播放通关音乐并产生4秒延迟(与上面同理)。然后我们提高游戏的关卡,重置所有的游戏变量(包括玩家、敌人、记录线段是否走过的数组),准备好进入下一关。到此,if语句结束。
接下来,判断玩家是否吃到宝物沙漏,如果是,则用m_Sound循环播放计时音乐,并修改m_Hourglass的数据信息,fx=2时表明被吃到,此时敌人会进入静止时间。
然后,我们让玩家的角度增加,以实现顺时针旋转的动画效果;让宝物沙漏角度较少,以实现逆时针旋转的动画效果。
最后部分是关于宝物沙漏的代码(我先说明下,m_Hourglass.fx等于0时表示不存在宝物沙漏,等于1时表示存在宝物沙漏,等于2时表示宝物沙漏被玩家吃了)。首先,让m_Hourglass.fy在每一帧都增加一定值。接着if语句判断当前不存在沙漏并且fy足够大(达到出现宝物沙漏的时间),如果是,就播放宝物沙漏出现的音乐,并随机生成宝物沙漏的位置,设置fx为1,fy为0。然后又一个if语句判断存在沙漏并且fy足够大(达到宝物沙漏消失的时间),如果是,就设置fx为0,fy为0,沙漏就会消失(不会被绘制出来)。最后一个if语句判断宝物沙漏被玩家吃了并且fy足够大(达到敌人静止时间结束),如果是,就用m_Sound结束播放的计时音乐,设置fx为0,fy为0,此时敌人的静止时间就会结束,可以重新动起来。
最终updateData()函数结尾,增加敌人的延迟计数器的m_Delay的值,只有当m_Delay足够大时,敌人才能运动,以此方式减缓了敌人的速度(在前面if语句中(m_Delay > (3-m_Level))这个语句就是实现这个功能的,当然这种减缓还与游戏难度等级建立的关系 )。
然后,我们来修改一下initializeGL()函数和resizeGL()。initializeGL()中我们绑定了纹理,删掉了深度测试,启用了混合;resizeGL()中我们把透视投影换成了正投影(因为我们的游戏是2D游戏)。其它不多解释,具体代码如下:
还有就要进入另一个重点paintGL()函数了,我会一一解释,具体代码如下:
if语句后,我们利用循环在屏幕的右上角绘制玩家的剩余生命。值得注意的是,我们会分两次进行绘制的(即有两个glBegin和glEnd),且两次绘制的角度、颜色、线的长短都不同,其实这只是为了让绘制的图标更好看罢了。还有,旋转的时候是根据spin的值进行旋转的,这就是为什么在updateData()我们要增加spin的值,通过这样的控制来旋转,我们可以实现旋转动画。
下面,我们来绘制网格。我们先假定m_Filled为true,表示当前全部网格已填满,接着我们把线的宽度设置为2.0。进入循环遍历每一段水平线段,并把线的颜色设置为蓝色,然后测试该线段是否走过,如果走过颜色就设置为白色。最后就可以把水平线段画出来了,位置的计算就不解释了。同理,我们可以画出垂直线段。
接下来我们检查网格中每个长方形的四条边是否都被走过,如果都被走过我们就绘制一个带纹理的四边形(检测过程其实就是检测m_Hline[i][j]、m_Hline[i][j+1]、m_Vline[i+1][j]、m_Vline[i+1][j+1]的值)。接着的if语句是判断m_Anti来设置是否启用直线反走样。
然后我们判断m_Hourglass.fx是否等于1(等于表示存在宝物沙漏),如果是,就绘制出宝物沙漏。再下面,我们就绘制我们的玩家了,绘制玩家的过程和之前的生命值相似,用两次绘制来让图标更好看些,当然还有利用spin实现旋转动画。再接下去,我们遍历所有敌人,与绘制玩家同样的道理和方法绘制出所有敌人。
最终,我们绘制完全部元素之后,调用updateData()函数,更新得到下一帧画面的数据。
最后,我们来补上键盘控制函数,具体代码如下:
最后,上下左右方向键用于控制玩家的移动。这里的移动控制与前面updateDate()函数中,敌人根据玩家的位置调整前进方向是相似的,都是玩家必须在某一个交点处才能进行“移动”,而这里的“移动”只是确定了方向,并没有一次性就把位置变到下一个交点处。而是在updateData()中由已经确定了的方向,一点点移动到目标交点处,形成连续的动画。当然,每次移动后,要把走过的线段应对的布尔值置为true。
现在就可以运行程序查看效果了!(这次大教程内容真不少)
全部教程中需要的资源文件点此下载
一点内容的补充:我们介绍一下反走样,在光栅图形显示器上绘制非水平且非垂直的直线或多边形边界时,或多或少会呈现锯齿状或台阶状外观。这是因为直线、多边形、色彩边界等是连续的,而光栅则是由离散的点组成,在光栅显示设备上表现直线、多边形等,必须在离散位置采样。由于采样不充分重建后造成的信息失真,就叫走样(aliasing)。而用于减少或消除这种效果的技术,就称为反走样(antialiasing)。
而我们的程序在正投影后,绘制图像就是在光栅图形显示器上绘制了;正常关闭反走样时,是看得到锯齿状线段的,但是由于我们游戏窗口设置为640×480,各元素看起来都比较小,我们肉眼很难捕捉到开启关闭反走样之间绘图的差异。
这次教程中,我们将介绍线、反走样、正投影和简单的声音,这是第一个大教程,希望这一课的东西大家能够喜欢(NeHe原文中有介绍计时器,但是Qt已经为我们封装好了计时器,所以这次教程中我省略了这部分,有兴趣了解VC中设置计时器的请点击这里)。
在这一课里,我们将学会绘制直接,使用反走样,正投影,基本的音效和一个简单的游戏逻辑,希望这里的东西可以让你高兴,毕竟我们会完成一个游戏!在这一课的结尾,你将获得一个叫“GRID CRAZY”的游戏,你的任务是走完每一段直线。这个程序有了一个基本游戏的一切要素:关卡,生命值,声明和一个游戏道具。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课代码的基础上修改代码,这次是个大程序,我们一一解释新的内容和游戏逻辑,希望大家能理解和喜欢这第一个openGL游戏(虽然只是2D游戏)。首先打开项目文件(.pro文件)和myglwidget.h文件,将两个文件内容更改如下:
TARGET = QtOpenGL21 TEMPLATE = app HEADERS += \ myglwidget.h SOURCES += \ myglwidget.cpp \ main.cpp QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets QT += opengl \ multimedia
#ifndef MYGLWIDGET_H #define MYGLWIDGET_H #include <QWidget> #include <QGLWidget> class QSound; class MyGLWidget : public QGLWidget { Q_OBJECT public: explicit MyGLWidget(QWidget *parent = 0); ~MyGLWidget(); protected: //对3个纯虚函数的重定义 void initializeGL(); void resizeGL(int w, int h); void paintGL(); void keyPressEvent(QKeyEvent *event); //处理键盘按下事件 private: void resetObjects(); //重置玩家和敌人信息 void updateData(); //更新下一帧数据 void buildFont(); //创建字体 void killFont(); //删除显示列表 //输出字符串 void glPrint(GLuint x, GLuint y, int set, const char *fmt, ...); private: bool fullscreen; //是否全屏显示 bool m_Vline[11][10]; //保存垂直方向的11根线条中,每根线条中的10段是否被走过 bool m_Hline[10][11]; //保存水平方向的11根线条中,每根线条中的10段是否被走过 bool m_Filled; //网格是否被填满 bool m_Gameover; //游戏是否结束 bool m_Anti; //是否反走样 int m_Delay; //敌人的暂停时间 int m_Adjust; //调整速度 int m_Lives; //玩家的生命 int m_Level; //内部的游戏难度等级 int m_Level2; //显示的游戏难度等级 int m_Stage; //游戏的关卡 static const int s_Steps[6]; //用来调节显示的速度 struct object //记录游戏中的对象 { int fx, fy; //使移动变得平滑 int x, y; //当前游戏者的位置 float spin; //旋转角度 }; object m_Player; //玩家信息 object m_Enemy[9]; //最多9个敌人 object m_Hourglass; //宝物沙漏信息 QString m_FileName[2]; //图片的路径及文件名 GLuint m_Texture[2]; //储存两个纹理 GLuint m_Base; //字符显示列表的开始值 QSound *m_Sound; //保存吃到宝物后的计时音乐 }; #endif // MYGLWIDGET_H
项目文件中,我们增加multimedia部分,这使得我们能够使用QSound等媒体播放对象。
myglwidget.h文件中,首先我们增加了2个布尔变量数组m_Vline和m_Hline,用于记录垂直方向和水平方向各110段线段是否走过。继续是3个布尔变量,当网格被填满时,m_Filled被设置为true而反之则为false;m_Gameover的作用易见,当它的值为true时,游戏结束;m_Anti指出抗锯齿功能是否打开,当设置为true时,该功能是打开着的。
接下来是5个整形变量和1个static const 整形数组,m_Delay用来减缓敌人的行动,其实就是当m_Delay小于某个值时,敌人不能移动(由于画面刷新很快,m_Delay变化也快,你是看不出敌人有很小一段时间没动的)。m_Adjust用来控制玩家和敌人移动的速度,其实就是控制玩家和敌人每一步能走多远,我们通过和下面的s_Steps[]数组配合一起完成这一控制目的。m_Lives保存了玩家的剩余生命值,m_Level保存了游戏内部的等级难度,m_Level2保存了显示出来的游戏难度,m_Stage保存了游戏的关卡(m_Level、m_Level2和m_Stage的区别后面大家会明白的,不用在这里纠结)。m_Steps[]保存了可供m_Adjust选择的数值。
然后我们定义了一个结构体来记录游戏中的对象。fx和fy记录每次在网格上移动我们的英雄和敌人的精确像素位置,x和y则记录着对象即将移动到网格交点是哪个。而最后一个变量spin用来使对象在z轴上选择。定义完后,我们就利用这个结构体创建我们的玩家对象,敌人对象(最多有9个所以是长度为9的数组)和宝物沙漏对象(m_Hourglass)。
还有我们需要载入两个纹理,所以有了m_FileName[2]和m_Texture[2。而m_Base储存字符显示列表的开始值,m_Sound用来指向一个QSound对象,该对象保存了吃到宝物后的计时音乐(注意声明QSound时需要在类前面加上class QSound声明)。最后是5个新的函数的声明,作用大家先看注释留个印象吧,后面会慢慢来介绍解释。
接下来,我们打开myglwidget.cpp,加上声明#include <QTimer>、#include <QSound>、#include <QTime>、#include <QCoreApplication>,在构造函数中对新增变量进行初始化并修改析构函数,具体代码如下:
const int MyGLWidget::s_Steps[] = {1, 2, 4, 5, 10, 20}; MyGLWidget::MyGLWidget(QWidget *parent) : QGLWidget(parent) { fullscreen = false; setFixedSize(640, 480); //设置固定的窗口大小 for (int i=0; i<11; i++) //初始化每个线段都没被走过 { for (int j=0; j<11; j++) { if (i < 10) { m_Hline[i][j] = false; } if (j < 10) { m_Vline[i][j] = false; } } } m_Filled = false; m_Gameover = false; m_Anti = true; m_Delay = 0; m_Adjust = 3; m_Lives = 5; m_Level = 1; m_Level2 = m_Level; m_Stage = 1; resetObjects(); //初始化玩家和敌人信息 m_Hourglass.fx = 0; //初始化宝物沙漏信息 m_Hourglass.fy = 0; m_FileName[0] = "D:/QtOpenGL/QtImage/Font.bmp"; //应根据实际存放图片的路径进行修改 m_FileName[1] = "D:/QtOpenGL/QtImage/Image.bmp"; m_Sound = new QSound("D:/QtOpenGL/QtImage/Freeze.wav"); QTimer *timer = new QTimer(this); //创建一个定时器 //将定时器的计时信号与updateGL()绑定 connect(timer, SIGNAL(timeout()), this, SLOT(updateGL())); timer->start(10); //以10ms为一个计时周期 } MyGLWidget::~MyGLWidget() { killFont(); //删除显示列表 }我觉得数据初始化没什么好讲的这次,大家看注意能明白的。不过注意一下在构造函数前要对s_Steps[]进行初始化,然后构造函数中,我们这次设置了窗口的固定大小(setFixedSize),这是为了我们的游戏能比较好的显示。resetObjects()函数在下面会解释,析构函数的修改在前面讲过,不解释了。
我们来先看下我们几个新增函数的定义,说是新增,其实有3个已经是老面孔了,3个我们比较熟悉的函数代码如下:
void MyGLWidget::buildFont() //创建位图字体 { float cx, cy; //储存字符的x、y坐标 m_Base = glGenLists(256); //创建256个显示列表 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择字符纹理 for (int i=0; i<256; i++) //循环256个显示列表 { cx = float(i%16)/16.0f; //当前字符的x坐标 cy = float(i/16)/16.0f; //当前字符的y坐标 glNewList(m_Base+i, GL_COMPILE); //开始创建显示列表 glBegin(GL_QUADS); //使用四边形显示每一个字符 glTexCoord2f(cx, 1.0f-cy-0.0625f); glVertex2i(0, 16); glTexCoord2f(cx+0.0625f, 1.0f-cy-0.0625f); glVertex2i(16, 16); glTexCoord2f(cx+0.0625f, 1.0f-cy); glVertex2i(16, 0); glTexCoord2f(cx, 1.0f-cy); glVertex2i(0, 0); glEnd(); //四边形字符绘制完成 glTranslated(15, 0, 0); //绘制完一个字符,向右平移10个单位 glEndList(); //字符显示列表完成 } }
void MyGLWidget::killFont() //删除显示列表 { glDeleteLists(m_Base, 256); //删除256个显示列表 }
void MyGLWidget::glPrint(GLuint x, GLuint y, int set, const char *fmt, ...) { char text[256]; //保存字符串 va_list ap; //指向一个变量列表的指针 if (fmt == NULL) //如果无输入则返回 { return; } va_start(ap, fmt); //分析可变参数 vsprintf(text, fmt, ap); //把参数值写入字符串 va_end(ap); //结束分析 if (set > 1) //如果字符集大于1 { set = 1; //设置其为1 } glEnable(GL_TEXTURE_2D); //启用纹理 glLoadIdentity(); //重置模型观察矩阵 glTranslated(x, y ,0); //把字符原点移动到(x,y)位置 glListBase(m_Base-32+(128*set)); //选择字符集 if (set == 0) { glScalef(1.5f, 2.0f, 1.0f); //如果是第一个字符集,放大字体 } glCallLists(strlen(text), GL_BYTE, text); //把字符串写到屏幕 glDisable(GL_TEXTURE_2D); //禁用纹理 }3个函数都只是一点小的修改,注意下glPrint()函数中多了一个函数glScalef(),这个函数用于分x、y、z方向按比例放大缩小将要绘制的对象。其中,参数为1.0时保持原状,大于1.0放大,小于1.0缩小。其它有不懂的,大家看看前面介绍“字体”的几个教程,就不多解释了。
下面来看看两个真正新面孔的函数,说一下updateData()函数时重点之一,具体代码如下:
void MyGLWidget::resetObjects() //重置玩家和敌人信息 { m_Player.x = 0; //把玩家重置在屏幕的左上角 m_Player.y = 0; m_Player.fx = 0; m_Player.fy = 0; for (int i=0; i<(m_Stage*m_Level); i++) //循环随机放置所有的敌人 { m_Enemy[i].x = 5 + rand()%6; m_Enemy[i].y = rand()%11; m_Enemy[i].fx = m_Enemy[i].x * 60; m_Enemy[i].fy = m_Enemy[i].y * 40; } }
void MyGLWidget::updateData() { if (!m_Gameover) //如果游戏没有结束,则进行数据更新 { for (int i=0; i<(m_Stage*m_Level); i++) //循环所有的敌人,敌人数由m_Stage×m_Level求得 { //根据玩家的位置,让敌人追击玩家 if ((m_Enemy[i].x < m_Player.x) && (m_Enemy[i].fy == m_Enemy[i].y*40)) { m_Enemy[i].x++; } if ((m_Enemy[i].x > m_Player.x) && (m_Enemy[i].fy == m_Enemy[i].y*40)) { m_Enemy[i].x--; } if ((m_Enemy[i].y < m_Player.y) && (m_Enemy[i].fx == m_Enemy[i].x*60)) { m_Enemy[i].y++; } if ((m_Enemy[i].y > m_Player.y) && (m_Enemy[i].fx == m_Enemy[i].x*60)) { m_Enemy[i].y--; } //当前时间不为吃到宝物沙漏后的敌人静止时间,也不为敌人延迟时间(影响敌人的速度) if ((m_Delay > (3-m_Level)) && (m_Hourglass.fx != 2)) { m_Delay = 0; //重置宝物沙漏计时 for (int j=0; j<(m_Stage*m_Level); j++) //循环设置每个敌人的位置 { //每个敌人调整位置,并调整旋转变量实现动画 if (m_Enemy[j].fx < m_Enemy[j].x*60) { m_Enemy[j].fx += s_Steps[m_Adjust]; m_Enemy[j].spin += s_Steps[m_Adjust]; } if (m_Enemy[j].fx > m_Enemy[j].x*60) { m_Enemy[j].fx -= s_Steps[m_Adjust]; m_Enemy[j].spin -= s_Steps[m_Adjust]; } if (m_Enemy[j].fy < m_Enemy[j].y*40) { m_Enemy[j].fy += s_Steps[m_Adjust]; m_Enemy[j].spin += s_Steps[m_Adjust]; } if (m_Enemy[j].fy > m_Enemy[j].y*40) { m_Enemy[j].fy -= s_Steps[m_Adjust]; m_Enemy[j].spin -= s_Steps[m_Adjust]; } } } //敌人的位置和玩家的位置相遇 if ((m_Enemy[i].fx == m_Player.fx) && (m_Enemy[i].fy == m_Player.fy)) { m_Lives--; //如果是,生命值减1 if (m_Lives == 0) //如果生命值为0,则游戏结束 { m_Gameover = true; } resetObjects(); //重置玩家和敌人信息 //播放死亡音乐并延迟2秒 QSound::play("D:/QtOpenGL/QtImage/Die.wav"); QTime time; time.start(); while (time.elapsed() < 2000) { QCoreApplication::processEvents(); } } } //调整玩家位置,使移动自然 if (m_Player.fx < m_Player.x*60) { m_Player.fx += s_Steps[m_Adjust]; m_Filled = false; //需要调整说明当前线段未走完,还不算网格填满 } if (m_Player.fx > m_Player.x*60) { m_Player.fx -= s_Steps[m_Adjust]; m_Filled = false; } if (m_Player.fy < m_Player.y*40) { m_Player.fy += s_Steps[m_Adjust]; m_Filled = false; } if (m_Player.fy > m_Player.y*40) { m_Player.fy -= s_Steps[m_Adjust]; m_Filled = false; } } if (m_Filled) //所有网格是否填满 { //播放过关音乐并延迟4秒 QSound::play("D:/QtOpenGL/QtImage/Complete.wav"); QTime time; time.start(); while (time.elapsed() < 4000) { QCoreApplication::processEvents(); } m_Stage++; //增加游戏难度 if (m_Stage > 3) //如果当前的关卡大于3,则进入到下一难度等级 { m_Stage = 1; //重置当前的关卡 m_Level++; //增加当前的难度等级 m_Level2++; if (m_Level > 3) { m_Level = 3; //如果难度等级大于3,则不再增加 m_Lives++; //完成一局给玩家奖励一条生命 if (m_Lives > 5) //如果玩家有5条生命,则不再增加 { m_Lives = 5; } } } resetObjects(); //重置玩家和敌人信息 for (int i=0; i<11; i++) //初始化每个线段都没被走过 { for (int j=0; j<11; j++) { if (i < 10) { m_Hline[i][j] = false; } if (j < 10) { m_Vline[i][j] = false; } } } } if ((m_Player.fx == m_Hourglass.x*60) //玩家吃到宝物沙漏 && (m_Player.fy == m_Hourglass.y*40) && (m_Hourglass.fx == 1)) { //循环播放一段计时音乐 m_Sound->setLoops(5); m_Sound->play(); m_Hourglass.fx = 2; //设置fx为2,表示吃到宝物沙漏 m_Hourglass.fy = 0; //设置fy为0 } m_Player.spin += 0.5f*s_Steps[m_Adjust]; //玩家旋转动画 if (m_Player.spin > 360.0f) { m_Player.spin -= 360.0f; } m_Hourglass.spin -= 0.25f*s_Steps[m_Adjust]; //宝物旋转动画 if (m_Hourglass.spin < 0.0f) { m_Hourglass.spin += 360.0f; } m_Hourglass.fy += s_Steps[m_Adjust]; //增加fy的值,当大于一定值时,产生宝物沙漏 if ((m_Hourglass.fx == 0) && (m_Hourglass.fy > 6000/m_Level)) { //播放提示宝物沙漏产生的音乐 QSound::play("D:/QtOpenGL/QtImage/Hourglass.wav"); m_Hourglass.x = rand()%10 + 1; m_Hourglass.y = rand()%11; m_Hourglass.fx = 1; //fx=1表示宝物沙漏出现 m_Hourglass.fy = 0; } //玩家没有吃掉宝物沙漏,则过一段时间后会消失 if ((m_Hourglass.fx == 1) && (m_Hourglass.fy > 6000/m_Level)) { m_Hourglass.fx = 0; //消失后重置宝物沙漏 m_Hourglass.fy = 0; } if ((m_Hourglass.fx == 2) && (m_Hourglass.fy > 500+(500*m_Level))) { m_Sound->stop(); //停止播放计时音乐 m_Hourglass.fx = 0; //重置宝物沙漏 m_Hourglass.fy = 0; } m_Delay++; //增加敌人的延迟计数器的值 }在resetObjects()函数中,我们要做的是重置玩家和敌人的对象信息。我们先是把玩家对象重置回屏幕的左上角的原点处,然后我们利用循环(敌人的数量等于难度等级×当前关卡),为所以的敌人随机生成一个位置。当然在生成位置时我们必须保证它们不会在原点处,且距离原点有一定的距离(我们设置敌人的初始化位置x方向上距离左侧有5以上的距离)。
进入重点的updateData()函数,它的作用是在每绘制一帧图像后,对游戏各部分数据进行适当准确的更新,为下一帧的绘图提供数据。首先,if语句判断游戏是否结束,没有结束就进入数据更新,进入游戏循环遍历每一个敌人。循环中,一开始我们利用4个if语句,让敌人根据玩家的位置去追击玩家。接下来的if
((m_Delay > (3-m_Level)) && (m_Hourglass.fx != 2))中,前一个语句表示,当前时间不为敌人的延迟时间(敌人在延迟时间中不运动,由于每秒画面帧数很多,延迟时间又很短,所以并看不来有延迟,通过设置延迟时间来减缓敌人的运动速度);后一个语句表示,当前时间不为吃到宝物沙漏后敌人的静止时间。当if条件满足时,说明当前为运动时间,就通过4个if语句让敌人按照当前前进方向调整位置并调整旋转角度(前面4个if语句的目的是当敌人在某一交点时确定下一步的移动方向,而这后面4个if语句是为了实现敌人在相邻两交点之间的运动动画)。注意到我们每次移动的距离是由s_Steps[]和m_Adjust来确定的,这就是为什么m_Adjust能控制移动的速度。
然后我们判断敌人的位置和玩家的位置是否重合,如果重合,玩家会减少1点生命,并使玩家和敌人的位置重置,并利用QSound播放死亡音乐,如果这是玩家的生命减为0,则游戏结束,置m_Gameover为true。值得注意的是,在QSound::play()函数下面,我们设置了一个QTime,让它开始计时,并通过while循环不断检测当前的时间,如果达到2000ms则结束循环。我们这么做是为了在播放死亡音乐时,让程序产生2秒的等待,不会直接进入下一个画面。当然循环中,QCoreApplication::processEvents()这行也是十分重要的,它保证了在while循环过程,音乐播放事件能顺利执行。最后我们用4个if语句调整玩家的位置,让动画看起来自然,这里的机理和上面敌人后面4个if语句的机理是一样的。到这里,最开始的那个if语句就结束了。
下面一开始if语句判断m_Filled是否为true,为true说明已经顺利通过本关,播放通关音乐并产生4秒延迟(与上面同理)。然后我们提高游戏的关卡,重置所有的游戏变量(包括玩家、敌人、记录线段是否走过的数组),准备好进入下一关。到此,if语句结束。
接下来,判断玩家是否吃到宝物沙漏,如果是,则用m_Sound循环播放计时音乐,并修改m_Hourglass的数据信息,fx=2时表明被吃到,此时敌人会进入静止时间。
然后,我们让玩家的角度增加,以实现顺时针旋转的动画效果;让宝物沙漏角度较少,以实现逆时针旋转的动画效果。
最后部分是关于宝物沙漏的代码(我先说明下,m_Hourglass.fx等于0时表示不存在宝物沙漏,等于1时表示存在宝物沙漏,等于2时表示宝物沙漏被玩家吃了)。首先,让m_Hourglass.fy在每一帧都增加一定值。接着if语句判断当前不存在沙漏并且fy足够大(达到出现宝物沙漏的时间),如果是,就播放宝物沙漏出现的音乐,并随机生成宝物沙漏的位置,设置fx为1,fy为0。然后又一个if语句判断存在沙漏并且fy足够大(达到宝物沙漏消失的时间),如果是,就设置fx为0,fy为0,沙漏就会消失(不会被绘制出来)。最后一个if语句判断宝物沙漏被玩家吃了并且fy足够大(达到敌人静止时间结束),如果是,就用m_Sound结束播放的计时音乐,设置fx为0,fy为0,此时敌人的静止时间就会结束,可以重新动起来。
最终updateData()函数结尾,增加敌人的延迟计数器的m_Delay的值,只有当m_Delay足够大时,敌人才能运动,以此方式减缓了敌人的速度(在前面if语句中(m_Delay > (3-m_Level))这个语句就是实现这个功能的,当然这种减缓还与游戏难度等级建立的关系 )。
然后,我们来修改一下initializeGL()函数和resizeGL()。initializeGL()中我们绑定了纹理,删掉了深度测试,启用了混合;resizeGL()中我们把透视投影换成了正投影(因为我们的游戏是2D游戏)。其它不多解释,具体代码如下:
void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置 { m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理 m_Texture[1] = bindTexture(QPixmap(m_FileName[1])); buildFont(); //创建字体 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景 glShadeModel(GL_SMOOTH); //启用阴影平滑 glClearDepth(1.0); //设置深度缓存 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正 glEnable(GL_BLEND); //启用融合 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //设置融合因子 }
void MyGLWidget::resizeGL(int w, int h) //重置OpenGL窗口的大小 { glViewport(0, 0, (GLint)w, (GLint)h); //重置当前的视口 glMatrixMode(GL_PROJECTION); //选择投影矩阵 glLoadIdentity(); //重置投影矩阵 glOrtho(0.0f, 640, 480, 0.0f, -1.0f, 1.0f); //设置正投影 glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵 glLoadIdentity(); //重置模型观察矩阵 }
还有就要进入另一个重点paintGL()函数了,我会一一解释,具体代码如下:
void MyGLWidget::paintGL() //从这里开始进行所以的绘制 { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //旋转字符纹理 glColor3f(1.0f, 0.5f, 1.0f); glPrint(207, 24, 0, "GRID CRAZY"); //绘制游戏名称"GRID CRAZY" glColor3f(1.0f, 1.0f, 0.0f); glPrint(20, 20, 1, "Level:%2i", m_Level2); //绘制当前的级别 glPrint(20, 40, 1, "Stage:%2i", m_Stage); //绘制当前级别的关卡 if (m_Gameover) //游戏是否结束 { glColor3ub(rand()%255, rand()%255, rand()%255); //随机选择一种颜色 glPrint(472, 20, 1, "GAME OVER"); //绘制"GAME OVER" glPrint(456, 40, 1, "PRESS SPACE"); //提示玩家按空格重新开始 } for (int i=0; i<m_Lives-1; i++) //循环绘制玩家的剩余生命 { glLoadIdentity(); //重置当前的模型观察矩阵 glTranslatef(490+(i*40.0f), 40.0f, 0.0f); //移动到屏幕右上角 //绘制剩余的生命图标 glRotatef(-m_Player.spin, 0.0f, 0.0f, 1.0f); //旋转动画 glColor3f(0.0f, 1.0f, 0.0f); glBegin(GL_LINES); glVertex2d(-5, -5); glVertex2d(5, 5); glVertex2d(5, -5); glVertex2d(-5, 5); glEnd(); glRotatef(-m_Player.spin*0.5f, 0.0f, 0.0f, 1.0f); glColor3f(0.0f, 0.75f, 0.0f); glBegin(GL_LINES); glVertex2d(-7, 0); glVertex2d(7, 0); glVertex2d(0, -7); glVertex2d(0, 7); glEnd(); } m_Filled = true; //在测试前,假定填充变量m_Filled为true glLineWidth(2.0f); //设置线宽为2.0 glDisable(GL_LINE_SMOOTH); //禁用反走样 glLoadIdentity(); //重置当前的模型观察矩阵 for (int i=0; i<11; i++) //循环11根线 { for (int j=0; j<11; j++) //循环每段线段 { glColor3f(0.0f, 0.5f, 1.0f); //设置线为蓝色 if (m_Hline[i][j]) //是否走过 { glColor3f(1.0f, 1.0f, 1.0f); //是,设置线为白色 } if (i < 10) //绘制水平线 { if (!m_Hline[i][j]) //如果当前线段没有走过,设置m_Filled为false { m_Filled = false; } glBegin(GL_LINES); //绘制当前的线段 glVertex2d(20+(i*60), 70+(j*40)); glVertex2d(80+(i*60), 70+(j*40)); glEnd(); } glColor3f(0.0f, 0.5f, 1.0f); //设置线为蓝色 if (m_Vline[i][j]) //是否走过 { glColor3f(1.0f, 1.0f, 1.0f); //是,设置线为白色 } if (j < 10) //绘制垂直线 { if (!m_Vline[i][j]) //如果当前线段没有走过,设置m_Filled为false { m_Filled = false; } glBegin(GL_LINES); //绘制当前的线段 glVertex2d(20+(i*60), 70+(j*40)); glVertex2d(20+(i*60), 110+(j*40)); glEnd(); } glEnable(GL_TEXTURE_2D); //使用纹理映射 glColor3f(1.0f, 1.0f, 1.0f); //设置为白色 glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //绑定纹理 if ((i < 10) && (j < 10)) //绘制走过的四边形 { if (m_Hline[i][j] && m_Hline[i][j+1] //是否走过 && m_Vline[i][j] && m_Vline[i+1][j]) { glBegin(GL_QUADS); //是,绘制它 glTexCoord2f(float(i/10.0f)+0.1f, 1.0f-(float(j/10.0f))); glVertex2d(20+(i*60)+59, 70+(j*40)+1); glTexCoord2f(float(i/10.0f), 1.0f-(float(j/10.0f))); glVertex2d(20+(i*60)+1, 70+(j*40)+1); glTexCoord2f(float(i/10.0f), 1.0f-(float(j/10.0f)+0.1f)); glVertex2d(20+(i*60)+1, 70+(j*40)+39); glTexCoord2f(float(i/10.0f)+0.1f, 1.0f-(float(j/10.0f)+0.1f)); glVertex2d(20+(i*60)+59, 70+(j*40)+39); glEnd(); } } glDisable(GL_TEXTURE_2D); } } glLineWidth(1.0f); if (m_Anti) //是否启用反走样 { glEnable(GL_LINE_SMOOTH); } if (m_Hourglass.fx == 1) //宝物沙漏是否存在 { //是,把宝物沙漏绘制出来 glLoadIdentity(); glTranslatef(20.0f+(m_Hourglass.x*60), 70.0f+(m_Hourglass.y*40), 0.0f); glRotatef(m_Hourglass.spin, 0.0f, 0.0f, 1.0f); glColor3ub(rand()%255, rand()%255, rand()%255); glBegin(GL_LINES); glVertex2d(-5, -5); glVertex2d(5, 5); glVertex2d(5, -5); glVertex2d(-5, 5); glVertex2d(-5, 5); glVertex2d(5, 5); glVertex2d(-5, -5); glVertex2d(5, -5); glEnd(); } //绘制玩家 glLoadIdentity(); glTranslatef(m_Player.fx+20.0f, //设置玩家的位置 m_Player.fy+70.0f, 0.0f); glRotatef(m_Player.spin, 0.0f, 0.0f, 1.0f); //旋转动画 glColor3f(0.0f, 1.0f, 0.0f); glBegin(GL_LINES); glVertex2d(-5, -5); glVertex2d(5, 5); glVertex2d(5, -5); glVertex2d(-5, 5); glEnd(); glRotatef(m_Player.spin*0.5f, 0.0f, 0.0f, 1.0f); glColor3f(0.0f, 0.75f, 0.0f); glBegin(GL_LINES); glVertex2d(-7, 0); glVertex2d(7, 0); glVertex2d(0, -7); glVertex2d(0, 7); glEnd(); //循环绘制所有敌人 for (int i=0; i<(m_Stage*m_Level); i++) { glLoadIdentity(); glTranslatef(m_Enemy[i].fx+20.0f, //设置敌人的位置 m_Enemy[i].fy+70.0f, 0.0f); glColor3f(1.0f, 0.5f, 0.5f); glBegin(GL_LINES); glVertex2d(0, -7); glVertex2d(-7, 0); glVertex2d(-7, 0); glVertex2d(0, 7); glVertex2d(0, 7); glVertex2d(7, 0); glVertex2d(7, 0); glVertex2d(0, -7); glEnd(); glRotatef(m_Enemy[i].spin, 0.0f, 0.0f, 1.0f); //旋转动画 glColor3f(1.0f, 0.0f, 0.0f); glBegin(GL_LINES); glVertex2d(-7, -7); glVertex2d(7, 7); glVertex2d(-7, 7); glVertex2d(7, -7); glEnd(); } updateData(); //更新下一帧的绘图数据 }首先我们清空缓存,绑定字体的纹理,来绘制游戏的提示字符串。接着我们来到第一个if语句,检测m_Gameover,如果游戏结束则绘制“GAME OVER”并提示玩家按空格键重新开始。
if语句后,我们利用循环在屏幕的右上角绘制玩家的剩余生命。值得注意的是,我们会分两次进行绘制的(即有两个glBegin和glEnd),且两次绘制的角度、颜色、线的长短都不同,其实这只是为了让绘制的图标更好看罢了。还有,旋转的时候是根据spin的值进行旋转的,这就是为什么在updateData()我们要增加spin的值,通过这样的控制来旋转,我们可以实现旋转动画。
下面,我们来绘制网格。我们先假定m_Filled为true,表示当前全部网格已填满,接着我们把线的宽度设置为2.0。进入循环遍历每一段水平线段,并把线的颜色设置为蓝色,然后测试该线段是否走过,如果走过颜色就设置为白色。最后就可以把水平线段画出来了,位置的计算就不解释了。同理,我们可以画出垂直线段。
接下来我们检查网格中每个长方形的四条边是否都被走过,如果都被走过我们就绘制一个带纹理的四边形(检测过程其实就是检测m_Hline[i][j]、m_Hline[i][j+1]、m_Vline[i+1][j]、m_Vline[i+1][j+1]的值)。接着的if语句是判断m_Anti来设置是否启用直线反走样。
然后我们判断m_Hourglass.fx是否等于1(等于表示存在宝物沙漏),如果是,就绘制出宝物沙漏。再下面,我们就绘制我们的玩家了,绘制玩家的过程和之前的生命值相似,用两次绘制来让图标更好看些,当然还有利用spin实现旋转动画。再接下去,我们遍历所有敌人,与绘制玩家同样的道理和方法绘制出所有敌人。
最终,我们绘制完全部元素之后,调用updateData()函数,更新得到下一帧画面的数据。
最后,我们来补上键盘控制函数,具体代码如下:
void MyGLWidget::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_F1: //F1为全屏和普通屏的切换键 fullscreen = !fullscreen; if (fullscreen) { showFullScreen(); } else { showNormal(); } updateGL(); break; case Qt::Key_Escape: //ESC为退出键 close(); break; case Qt::Key_A: //A为开启禁用反走样的切换键 m_Anti = !m_Anti; break; case Qt::Key_Space: //空格为游戏结束时重置键 if (m_Gameover) //游戏结束则重置变量及数据 { m_Gameover = false; m_Filled = true; m_Level = 1; m_Level2 = m_Level; m_Stage = 0; m_Lives = 5; updateData(); } break; case Qt::Key_Right: //按下向右右行一格 if ((m_Player.x < 10) && (m_Player.fx == m_Player.x*60) && (m_Player.fy == m_Player.y*40) && (!m_Filled)) { m_Hline[m_Player.x][m_Player.y] = true; m_Player.x++; } break; case Qt::Key_Left: //按下向左左行一格 if ((m_Player.x > 0) && (m_Player.fx == m_Player.x*60) && (m_Player.fy == m_Player.y*40) && (!m_Filled)) { m_Player.x--; m_Hline[m_Player.x][m_Player.y] = true; } break; case Qt::Key_Down: //按下向下下行一格 if ((m_Player.y < 10) && (m_Player.fx == m_Player.x*60) && (m_Player.fy == m_Player.y*40) && (!m_Filled)) { m_Vline[m_Player.x][m_Player.y] = true; m_Player.y++; } break; case Qt::Key_Up: //按下向上上行一格 if ((m_Player.y > 0) && (m_Player.fx == m_Player.x*60) && (m_Player.fy == m_Player.y*40) && (!m_Filled)) { m_Player.y--; m_Vline[m_Player.x][m_Player.y] = true; } break; } }可以看到,我们增加A键作为开启关闭反走样的切换键。接着,在m_Gameover为true时,如果按下空格键,就重置游戏难度等级、关卡、玩家生命,设置m_Gameover为false,m_Filled为true,并调用updateData()。最值得注意的是,我们用了一个小技巧,我们把m_Stage置为0,m_Filled置为true,调用updateData()之后,就会自动进入m_Stage为1的“新关卡”。
最后,上下左右方向键用于控制玩家的移动。这里的移动控制与前面updateDate()函数中,敌人根据玩家的位置调整前进方向是相似的,都是玩家必须在某一个交点处才能进行“移动”,而这里的“移动”只是确定了方向,并没有一次性就把位置变到下一个交点处。而是在updateData()中由已经确定了的方向,一点点移动到目标交点处,形成连续的动画。当然,每次移动后,要把走过的线段应对的布尔值置为true。
现在就可以运行程序查看效果了!(这次大教程内容真不少)
全部教程中需要的资源文件点此下载
一点内容的补充:我们介绍一下反走样,在光栅图形显示器上绘制非水平且非垂直的直线或多边形边界时,或多或少会呈现锯齿状或台阶状外观。这是因为直线、多边形、色彩边界等是连续的,而光栅则是由离散的点组成,在光栅显示设备上表现直线、多边形等,必须在离散位置采样。由于采样不充分重建后造成的信息失真,就叫走样(aliasing)。而用于减少或消除这种效果的技术,就称为反走样(antialiasing)。
而我们的程序在正投影后,绘制图像就是在光栅图形显示器上绘制了;正常关闭反走样时,是看得到锯齿状线段的,但是由于我们游戏窗口设置为640×480,各元素看起来都比较小,我们肉眼很难捕捉到开启关闭反走样之间绘图的差异。
相关文章推荐
- QT中的2D绘图的总结
- Qt-QPalette类的用法
- qt下编写andriod程序必须报错总结
- QT中实现在控制台输出
- PyQt5初级教程--PyQt5中部件[8/13]
- 第一个QT文件为毛编译不出来
- QT5---应用程序发布
- PyQt5初级教程--PyQt5中的对话框[7/13]
- Qt之VLFeat SLIC超像素分割(Cpp版)
- PyQt5初级教程--PyQt5中的事件和信号[6/13]
- Qt交叉编译时ICPC命令未找到处理方法
- OpenCV在Windows下编译WITH_Qt
- QT图形视图框架
- Opencv交叉编译到ARM(基于Qt)
- QTP手工启动,录制WinAPP内容为空
- QT仿酷狗
- 漫水填充算法的一个简单实现(Qt版)
- 漫水填充算法的一个简单实现(Qt版)
- PyQt5初级教程--PyQt5中的布局管理[5/13]
- Qt_QJson等的简单应用