第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
#include
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
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
首先我们清空缓存,绑定字体的纹理,来绘制游戏的提示字符串。接着我们来到第一个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,各元素看起来都比较小,我们肉眼很难捕捉到开启关闭反走样之间绘图的差异。