您的位置:首页 > 编程语言 > Qt开发

【Qt OpenGL教程】21:线、反走样、正投影和简单的声音

2015-08-09 18:16 615 查看
第21课:线、反走样、正投影和简单的声音 (参照NeHe)
这次教程中,我们将介绍线、反走样、正投影和简单的声音,这是第一个大教程,希望这一课的东西大家能够喜欢(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,各元素看起来都比较小,我们肉眼很难捕捉到开启关闭反走样之间绘图的差异。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: