前言
声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。
NeHe OpenGL第二十一课:线的游戏
线,反走样,计时,正投影和简单的声音:
这是我第一个大的教程,它将包括线,反走样,计时,正投影和简单的声音。希望这一课中的东西能让每个人感到高兴。
欢迎来到第21课,在这一课里,你将学会直线,反走样,正投影,计时,基本的音效和一个简单的游戏逻辑。希望这里的东西可以让你高兴。我花了两天的时间写代码,并用了两周的时间写这份HTML文件,希望你能享受我的劳动。
在这课的结尾你将获得一个叫"amidar"的游戏,你的任务是走完所有的直线。这个程序有了一个基本游戏的一切要素,关卡,生命值,声音和一个游戏道具。
我们从第一课的程序来逐步完整这个程序,按照惯例,我们只介绍改动的部分。
#include <windows.h>
#include <stdio.h>
#include <stdarg.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <gl\glaux.h>
HDC hDC=NULL;
HGLRC hRC=NULL;
HWND hWnd=NULL;
HINSTANCE hInstance;
bool类型的变量,vline保存了组成我们游戏网格垂直方向上的121条线,上下水平各11条。hline保存了水平方向上的 121条线,用ap来检查A键是否已经按下。
当网格被填满时,
filled被设置为TRUE而反之则为FALSE。gameover这个变量的作用显而易见,当他的值为TRUE时,游戏结束。anti指出抗锯齿功能是否打开,当设置为TRUE时,该功能是打开着的。active
和 fullscreen 指出窗口是否被最小化以及游戏窗口是窗口模式还是全屏模式。
bool keys[256];
bool vline[11][10]; // 保存垂直方向的11根线条中,每根线条中的10段是否被走过
bool hline[10][11]; //保存水平方向的11根线条中,每根线条中的10段是否被走过
bool ap; // A键是否已经按下
bool filled; // 网格是否被填满?
bool gameover; // 游戏是否结束?
bool anti=TRUE; // 是否启用反走样?
bool active=TRUE;
bool fullscreen=TRUE;
接着设置整型变量。loop1 和 loop2 被用来检查网格,查看是否有敌人攻击我们,以及在网格上给对象一个随机的位置。你将看到loop1 /
loop2在后面的程序得到使用。delay
是一个计数器,我用他来减慢那些坏蛋的动作。当delay的值大于某一个馈值的时候,敌人才可以行动,此时delay将被重置。
adjust是一个非常特殊的变量,即使我们的程序拥有一个定时器,他也仅仅用来检查你的计算机是否运行地太快。如果是,则需要暂停一下以减慢运行速度。在我地GeForce显卡上,程序的运行平滑地简直变态,并且非常非常快。但是在我的PIII/450+Voodoo
3500TV上测试的时候,我注意到程序运行地非常缓慢。我发现问题在于关于时间控制那部分代码只能够用来减慢游戏进行而并不能加速之。因此我引入了一个叫做adjust
的变量。它可以是0到5之间的任何值。游戏中的对象移动速度的不同依赖于这个变量的值。值越小,运动越平滑;而值越大,则运动速度越快。这是在比较慢的机器上运行这个程序最简单有效的解决方案了。但是请注意,不管对象移动的速度有多快,游戏的速度都不会比我期望的更快。我们推荐把adjust值设置为3,这样在大部分机器上都有比较满意的效果。
我们把lives的值设置成5,这样我们的英雄一出场就拥有5条命。level是一个内部变量,用来指出当前游戏的难度。当然,这并不是你在屏幕上所看到的那个Level。变量level2开始的时候和Level拥有相同的值,但是随着你技能的提高,这个值也会增加。当你成功通过难度3之后,这个值也将在难度3上停止增加。level
是一个用来表示游戏难度的内部变量,stage才是用来记录当前游戏关卡的变量。
int loop1; // 通用循环变量
int loop2; // 通用循环变量
int delay; // 敌人的暂停时间
int adjust=3; // 调整显示的速度
int lives=5; // 玩家的生命
int level=1; // 内部游戏的等级
int level2=level; // 显示的游戏的等级
int stage=1; // 游戏的关卡
接下来我们需要一个结构来记录游戏中的对象。fx和fy每次在网格上移动我们的英雄和敌人一些较小的象素,以创建一个平滑的动画效果。x和y则记录着对象处于网格的那个交点上。
上下左右各有11个点,因此x和y可以是0到10之间的任意值。这也是我们为什么需要fx和fy的原因。考虑如果我们只能够在上下和左右方向的11个点间移动的话,我们的英雄不得不
在各个点间跳跃前进。这样显然是不够平滑美观的。
最后一个变量spin用来使对象在Z轴上旋转。
struct object // 记录游戏中的对象
{
int fx, fy; // 使移动变得平滑
int x, y; // 当前游戏者的位置
float spin; // 旋转方向
};
既然我们已经为我们的玩家,敌人,甚至是秘密武器。设置了结构体,那么同样的,为了表现刚刚创设的结构体的功能和特性,我们也可以为此设置新的结构体。
为我们的玩家创设结构体之下的第一条直线。基本上我们将会为玩家提供fx,fy,x,y和spin值几种不同的结构体。通过增加这些直线,仅需查看玩家的x值我们就很容易取得玩家的位置,同时我们也可以通过增加玩家的旋转度来改变玩家的spin值。
第二条直线略有不同。因为同一屏幕我们可以同时拥有至多15个敌人。我们需要为每个敌人创造上面所提到的可变量。我们通过设置一个有15个敌人的组来实现这个目标,如第一个敌人的位置被设定为敌人(0).x.第二个敌人的位置为(1),x等等
第三条直线使得为宝物创设结构体实现了可能。宝物是一个会时不时在屏幕上出现的沙漏。我们需要通过沙漏来追踪x和y值。但是因为沙漏的位置是固定的所以我们不需要寻找最佳位置,而通过为程序后面的其他物品寻找好的可变量来实现(如fx和fy)
struct object player; // 玩家信息
struct object enemy[9]; // 最多9个敌人的信息
struct object hourglass; // 宝物信息
现在我们创建一个描述时间的结构,使用这个结构我们可以很轻松的跟踪时间变量。
接下来的第一步,就是创建一个64位的频率变量,它记录时间的频率。
resolution变量用来记录最小的时间间隔。
mm_timer_start和mm_timer_elapsed保存计时器开始时的时间和计时器开始后流失的时间。这两个变量只有当计算机不拥有performance counter时才启用。
变量performance_timer用来标识计算机是否有performance counter
如果performance counter启用,最后两个变量用来保存计时器开始时的时间和计时器开始后流失的时间,它们比普通的根据精确。
struct // 保存时间信息的结构
{
__int64 frequency; // 频率
float resolution; // 时间间隔
unsigned long mm_timer_start; // 多媒体计时器的开始时间
unsigned long mm_timer_elapsed; // 多媒体计时器的开始时间
bool performance_timer; // 使用Performance Timer?
__int64 performance_timer_start; // Performance Timer计时器的开始时间
__int64 performance_timer_elapsed; // Performance Timer计时器的开始时间
} timer;
下一行代码定义了速度表。如前所说,对象移动的速度依赖于值adjust,而以adjust为下标去检索速度表,就可以获得对象的移动速度。
int steps[6]={ 1, 2, 4, 5, 10, 20 }; // 用来调整显示的速度
接下来我们将为纹理分配空间。纹理一共2张,一张是背景而另外一张是一张字体纹理。如本系列教程中的其他课程一样,base用来指出字符显示列表的基,同样的我们在最后声明了窗口过程WndProc()。
GLuint texture[2]; // 字符纹理
GLuint base; // 字符显示列表的开始值
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
接下来会是很有趣的工作。接下来的一段代码会初始化我们的计时器。代码会检查performance counter(非常精确的计数器)是否可用,如果不可用,则使用多媒体计时器。这段代码是可以移植的。
void TimerInit(void) // 初始化我们的计时器
{
memset(&timer, 0, sizeof(timer)); // 清空计时器结构
// 检测Performance Counter是否可用,可用则创建
if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
{
// 如果不可用
timer.performance_timer = FALSE; // 设置Performance Timer为false
timer.mm_timer_start = timeGetTime(); // 使用普通的计时器
timer.resolution = 1.0f/1000.0f; // 设置单位为毫秒
timer.frequency = 1000; // 设置频率为1000
timer.mm_timer_elapsed = timer.mm_timer_start; // 设置流失的时间为当前的时间
}
如果performance counter 可用,则执行下面的代码:
else
{
// 使用Performance Counter计时器
QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
timer.performance_timer = TRUE; // 设置Performance Timer为TRUE
// 计算计时的精确度
timer.resolution = (float) (((double)1.0f)/((double)timer.frequency));
// 设置流失的时间为当前的时间
timer.performance_timer_elapsed = timer.performance_timer_start;
}
}
上面的代码设置了计时器,而下面的代码则读出计时器并返回已经经过的时间,以毫秒计。代码很简单,首先检查是否支持performance counter,若支持,则调用其相关函数;否则调用多媒体函数。
float TimerGetTime() // 返回经过的时间,以毫秒为单位
{
__int64 time; // 使用64位的整数
if (timer.performance_timer) // 是否使用Performance Timer计时器?
{
QueryPerformanceCounter((LARGE_INTEGER *) &time); // 返回当前的时间
// 返回时间差
return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
}
else
{
// 使用普通的计时器,返回时间差
return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
}
}
在下面的代码里,我们把玩家重置在屏幕的左上角,而给敌人设置一个随机的位置。
void ResetObjects(void) // 重置玩家和敌人
{
player.x=0; // 把玩家置于左上角
player.y=0;
player.fx=0;
player.fy=0;
接着我们给敌人一个随机的开始位置,敌人的数量等于难度乘上当前关卡号。记着,难度最大是3,而最多有3关。因此敌人最多有9个。
for (loop1=0; loop1<(stage*level); loop1++) // 循环随即放置所有的敌人
{
enemy[loop1].x=5+rand()%6;
enemy[loop1].y=rand()%11;
enemy[loop1].fx=enemy[loop1].x*60;
enemy[loop1].fy=enemy[loop1].y*40;
}
}
并没有做任何改动,因此我将跳过它。在LoadGLTextures函数里我将载入那两个纹理--背景和字体。并且我会把这两副图都转化成纹理,这样我们就可以在游戏中使用他们。纹理创建好之后,象素数据就可以删除了。没有什么新东西,你可以阅读以前的课程以获得更多信息。
int LoadGLTextures()
{
int Status=FALSE;
AUX_RGBImageRec *TextureImage[2];
memset(TextureImage,0,sizeof(void *)*2);
if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) && // 载入字体纹理
(TextureImage[1]=LoadBMP("Data/Image.bmp"))) // 载入图像纹理
{
Status=TRUE;
glGenTextures(2, &texture[0]);
for (loop1=0; loop1<2; loop1++)
{
glBindTexture(GL_TEXTURE_2D, texture[loop1]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
for (loop1=0; loop1<2; loop1++)
{
if (TextureImage[loop1])
{
if (TextureImage[loop1]->data)
{
free(TextureImage[loop1]->data);
}
free(TextureImage[loop1]);
}
}
}
return Status;
}
下面的代码建立了显示列表。对于字体的显示,我已经写过教程。在这里我把字体图象分成16×16个单元共256个字符。如果你有什么不明白,请参阅前面的教程
GLvoid BuildFont(GLvoid)
{
base=glGenLists(256);
glBindTexture(GL_TEXTURE_2D, texture[0]);
for (loop1=0; loop1<256; loop1++)
{
float cx=float(loop1%16)/16.0f;
float cy=float(loop1/16)/16.0f;
glNewList(base+loop1,GL_COMPILE);
glBegin(GL_QUADS);
glTexCoord2f(cx,1.0f-cy-0.0625f);
glVertex2d(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);
glEndList();
}
}
当我们不再需要显示列表的时候,销毁它是一个好主意。在这里我仍然把代码加上了,虽然没有什么新东西。
GLvoid KillFont(GLvoid)
{
glDeleteLists(base,256);
}
函数没有做太多改变。唯一的改动是它可以打印变量了。我把代码列出这样你可以容易看到改动的地方。
请注意,在这里我激活了纹理并且重置了视图矩阵。如果set被置1的话,字体将被放大。我这样做是希望可以在屏幕上显示大一点的字符。在一切结束后,我会禁用纹理。
GLvoid glPrint(GLint x, GLint 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)
{
set=1;
}
glEnable(GL_TEXTURE_2D);
glLoadIdentity();
glTranslated(x,y,0);
glListBase(base-32+(128*set));
if (set==0)
{
glScalef(1.5f,2.0f,1.0f);
}
glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);
glDisable(GL_TEXTURE_2D);
}
下面的代码基本没有变化,只是把透视投影变为了正投影
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height==0)
{
height=1;
}
glViewport(0,0,width,height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
初始化的代码和前面的代码相比没有什么改变
int InitGL(GLvoid)
{
if (!LoadGLTextures())
{
return FALSE;
}
BuildFont();
glShadeModel(GL_SMOOTH);
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
glClearDepth(1.0f);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
return TRUE;
}
下面是我们的绘制代码。
首先我们清空缓存,接着绑定字体的纹理,绘制游戏的提示字符串
int DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindTexture(GL_TEXTURE_2D, 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",level2); // 绘制当前的级别
glPrint(20,40,1,"Stage:%2i",stage); // 绘制当前级别的关卡
现在我们检测游戏是否结束,如果游戏结束绘制"Gmae over"并提示玩家按空格键重新开始
if (gameover) // 游戏是否结束?
{
glColor3ub(rand()%255,rand()%255,rand()%255); // 随机选择一种颜色
glPrint(472,20,1,"GAME OVER"); // 绘制 GAME OVER 字符串到屏幕
glPrint(456,40,1,"PRESS SPACE"); // 提示玩家按空格键重新开始
}
在屏幕的右上角绘制玩家的剩余生命
for (loop1=0; loop1<lives-1; loop1++) //循环绘制玩家的剩余生命
{
glLoadIdentity();
glTranslatef(490+(loop1*40.0f),40.0f,0.0f); // 移动到屏幕右上角
glRotatef(-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(-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();
}
下面我们来绘制网格,我们设置变量filled为TRUE,这告诉程序填充网格。
接着我们把线的宽度设置为2,并把线的颜色设置为蓝色,接着我们检测线断是否被走过,如果走过我们设置颜色为白色。
filled=TRUE; // 在测试前,把填充变量设置为TRUE
glLineWidth(2.0f); // 设置线宽为2.0f
glDisable(GL_LINE_SMOOTH); // 禁用反走样
glLoadIdentity();
for (loop1=0; loop1<11; loop1++) // 循环11根线
{
for (loop2=0; loop2<11; loop2++) // 循环每根线的线段
{
glColor3f(0.0f,0.5f,1.0f); // 设置线为蓝色
if (hline[loop1][loop2]) // 是否走过?
{
glColor3f(1.0f,1.0f,1.0f); // 是,设线为白色
}
if (loop1<10) // 绘制水平线
{
if (!hline[loop1][loop2]) // 如果当前线段没有走过,则不填充
{
filled=FALSE;
}
glBegin(GL_LINES); // 绘制当前的线段
glVertex2d(20+(loop1*60),70+(loop2*40));
glVertex2d(80+(loop1*60),70+(loop2*40));
glEnd();
}
下面的代码绘制垂直的线段
glColor3f(0.0f,0.5f,1.0f); // 设置线为蓝色
if (vline[loop1][loop2]) // 是否走过
{
glColor3f(1.0f,1.0f,1.0f); // 是,设线为白色
}
if (loop2<10) // 绘制垂直线
{
if (!vline[loop1][loop2]) // 如果当前线段没有走过,则不填充
{
filled=FALSE;
}
glBegin(GL_LINES); // 绘制当前的线段
glVertex2d(20+(loop1*60),70+(loop2*40));
glVertex2d(20+(loop1*60),110+(loop2*40));
glEnd();
}
接下来我们检测长方形的四个边是否都被走过,如果被走过我们就绘制一个带纹理的四边形。
我们用下图来解释这个检测过程
如果对于垂直线vline的相邻两个边都被走过,并且水平线hline的相邻两个边也被走过,那么我们就可以绘制这个四边形了。我们可以使用循环检测每一个四边形,代码如下:
glEnable(GL_TEXTURE_2D); // 使用纹理映射
glColor3f(1.0f,1.0f,1.0f); // 设置为白色
glBindTexture(GL_TEXTURE_2D, texture[1]); // 绑定纹理
if ((loop1<10) && (loop2<10)) // 绘制走过的四边形
{
// 这个四边形是否被走过?
if (hline[loop1][loop2] && hline[loop1][loop2+1] && vline[loop1][loop2] && vline[loop1+1][loop2])
{
glBegin(GL_QUADS); // 是,则绘制它
glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));
glVertex2d(20+(loop1*60)+59,(70+loop2*40+1));
glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));
glVertex2d(20+(loop1*60)+1,(70+loop2*40+1));
glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));
glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39);
glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));
glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39);
glEnd();
}
}
glDisable(GL_TEXTURE_2D);
}
}
glLineWidth(1.0f);
下面的代码用来设置是否启用直线反走样
if (anti) // 是否启用反走样?
{
glEnable(GL_LINE_SMOOTH);
}
为了使游戏变得简单些,我添加了一个时间停止器,当你吃掉它时,可以让追击的你的敌人停下来。
下面的代码用来绘制一个时间停止器。
if (hourglass.fx==1)
{
glLoadIdentity();
glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f);
glRotatef(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(player.fx+20.0f,player.fy+70.0f,0.0f); // 设置玩家的位置
glRotatef(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(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 (loop1=0; loop1<(stage*level); loop1++)
{
glLoadIdentity();
glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].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(enemy[loop1].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();
}
return TRUE;
}
KillGLWindow函数基本没有变化,只在最后一行添加KillFont函数
GLvoid KillGLWindow(GLvoid)
{
if (fullscreen)
{
ChangeDisplaySettings(NULL,0);
ShowCursor(TRUE);
}
if (hRC)
{
if (!wglMakeCurrent(NULL,NULL))
{
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC))
{
MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL;
}
if (hDC && !ReleaseDC(hWnd,hDC))
{
MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hDC=NULL;
}
if (hWnd && !DestroyWindow(hWnd))
{
MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hWnd=NULL;
}
if (!UnregisterClass("OpenGL",hInstance))
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL;
}
KillFont(); // 删除创建的字体
}
函数CreateGLWindow() and WndProc() 没有变化。
游戏控制在WinMain中完成的
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
BOOL done=FALSE;
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;
}
在创建完OpenGL窗口后,我们添加如下的代码,它用来创建玩家和敌人,并初始化时间计时器
if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
{
return 0;
}
ResetObjects(); // 重置玩家和敌人
TimerInit(); // 初始化时间计时器
while(!done)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if (msg.message==WM_QUIT)
{
done=TRUE;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
接下来取得当前的时间,并在速度快的机器上让其空循环,使得程序在所有的机器上都拥有同样的帧率
float start=TimerGetTime(); // 返回当前的时间
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE;
}
else
{
SwapBuffers(hDC);
}
while(TimerGetTime()<start+float(steps[adjust]*2.0f)) {}// 速度快的机器上让其空循环
下面的部分没有改变,按F1执行窗口和全屏的切换
if (keys[VK_F1])
{
keys[VK_F1]=FALSE;
KillGLWindow();
fullscreen=!fullscreen;
if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
{
return 0;
}
}
按A键切换是否启用反走样
if (keys['A'] && !ap) // 如果'A' 键被按下,启用反走样
{
ap=TRUE;
anti=!anti;
}
if (!keys['A'])
{
ap=FALSE;
}
如果游戏没有结束,执行游戏循环
if (!gameover && active) // 如果游戏没有结束,则进行游戏循环
{
for (loop1=0; loop1<(stage*level); loop1++) // 循环不同的难度等级
{
根据玩家的位置,让敌人追击玩家
if ((enemy[loop1].x<player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
{
enemy[loop1].x++;
}
if ((enemy[loop1].x>player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
{
enemy[loop1].x--;
}
if ((enemy[loop1].y<player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
{
enemy[loop1].y++;
}
if ((enemy[loop1].y>player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
{
enemy[loop1].y--;
}
如果时间停止器的显示时间结束,而玩家又没有吃到,那么重置计时计算器。
if (delay>(3-level) && (hourglass.fx!=2)) // 如果没有吃到时间停止器
{
delay=0; // 重置时间停止器
for (loop2=0; loop2<(stage*level); loop2++) // 循环设置每个敌人的位置
{
下面的代码调整每个敌人的位置,并绘制它们的显示效果
if (enemy[loop2].fx<enemy[loop2].x*60)
{
enemy[loop2].fx+=steps[adjust];
enemy[loop2].spin+=steps[adjust];
}
if (enemy[loop2].fx>enemy[loop2].x*60)
{
enemy[loop2].fx-=steps[adjust];
enemy[loop2].spin-=steps[adjust];
}
if (enemy[loop2].fy<enemy[loop2].y*40)
{
enemy[loop2].fy+=steps[adjust];
enemy[loop2].spin+=steps[adjust];
}
if (enemy[loop2].fy>enemy[loop2].y*40)
{
enemy[loop2].fy-=steps[adjust];
enemy[loop2].spin-=steps[adjust];
}
}
}
如果敌人的位置和玩家的位置相遇,这玩家死亡,开始新的一局
// 敌人的位置和玩家的位置相遇?
if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))
{
lives--; // 如果是,生命值减1
if (lives==0) // 如果生命值为0,则游戏结束
{
gameover=TRUE;
}
ResetObjects(); // 重置所有的游戏变量
PlaySound("Data/Die.wav", NULL, SND_SYNC); // 播放死亡的音乐
}
}
使用上,下,左,右控制玩家的位置
if (keys[VK_RIGHT] && (player.x<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
hline[player.x][player.y]=TRUE;
player.x++;
}
if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
player.x--;
hline[player.x][player.y]=TRUE;
}
if (keys[VK_DOWN] && (player.y<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
vline[player.x][player.y]=TRUE;
player.y++;
}
if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
player.y--;
vline[player.x][player.y]=TRUE;
}
调整玩家的位置,让动画看起来跟自然
if (player.fx<player.x*60)
{
player.fx+=steps[adjust];
}
if (player.fx>player.x*60)
{
player.fx-=steps[adjust];
}
if (player.fy<player.y*40)
{
player.fy+=steps[adjust];
}
if (player.fy>player.y*40)
{
player.fy-=steps[adjust];
}
}
如果游戏结束,按空格开始新的一局游戏
else // 如果游戏结束
{
if (keys[' ']) // 按下空格?
{
gameover=FALSE; // 开始新的一局
filled=TRUE; // 重置所有的变量
level=1;
level2=1;
stage=0;
lives=5;
}
}
如果顺利通过本关,播放通关音乐,并提高游戏难度,开始新的一局
if (filled) // 所有网格是否填满
{
PlaySound("Data/Complete.wav", NULL, SND_SYNC); // 播放过关音乐
stage++; // 增加游戏难度
if (stage>3) // 如果当前的关卡大于3,则进入到下一个大的关卡?
{
stage=1; // 重置当前的关卡
level++; // 增加大关卡的值
level2++;
if (level>3)
{
level=3; // 如果大关卡大于3,则不再增加
lives++; // 完成一局给玩家奖励一条生命
if (lives>5) // 如果玩家有5条生命,则不再增加
{
lives=5;
}
}
}
进入到下一关卡,重置所有的游戏变量
ResetObjects();
for (loop1=0; loop1<11; loop1++)
{
for (loop2=0; loop2<11; loop2++)
{
if (loop1<10)
{
hline[loop1][loop2]=FALSE;
}
if (loop2<10)
{
vline[loop1][loop2]=FALSE;
}
}
}
}
如果玩家吃到时间停止器,记录这一信息
if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))
{
// 播放一段声音
PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);
hourglass.fx=2; // 设置fx为2,表示吃到时间停止器
hourglass.fy=0; // 设置fy为0
}
显示玩家的动画效果
player.spin+=0.5f*steps[adjust]; // 旋转动画
if (player.spin>360.0f)
{
player.spin-=360;
}
显示时间停止器的动画
hourglass.spin-=0.25f*steps[adjust]; // 旋转动画
if (hourglass.spin<0.0f)
{
hourglass.spin+=360.0f;
}
下面的代码计算何时出现一个时间停止计数器
hourglass.fy+=steps[adjust]; // 增加fy的值,当他大于一定的时候,产生时间停止计数器
if ((hourglass.fx==0) && (hourglass.fy>6000/level))
{
PlaySound("Data/hourglass.wav", NULL, SND_ASYNC);
hourglass.x=rand()%10+1;
hourglass.y=rand()%11;
hourglass.fx=1; //fx=1表示时间停止器出现
hourglass.fy=0;
}
如果玩家没有拾取时间停止器,则过一段时间后,它自动消失
if ((hourglass.fx==1) && (hourglass.fy>6000/level))
{
hourglass.fx=0; // 消失后重置时间停止器
hourglass.fy=0;
}
如果玩家吃到时间停止器,在时间停止停止阶段播放一段音乐,过一段时间停止播放音乐
if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))
{
PlaySound(NULL, NULL, 0); // 停止播放音乐
hourglass.fx=0; // 重置变量
hourglass.fy=0;
}
增加敌人的延迟计数器的值,这个值用来更新敌人的运动
delay++; // 增加敌人的延迟计数器的值
}
}
// 关闭
KillGLWindow(); // 删除窗口
return (msg.wParam); // 退出程序
}
原文及其个版本源代码下载: