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

第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,各元素看起来都比较小,我们肉眼很难捕捉到开启关闭反走样之间绘图的差异。

猜你喜欢

转载自blog.csdn.net/cly116/article/details/47378289