OpenGL2D小游戏——是男人就下100层
是男人就下100层想必大家一定都玩过,在这里给大家简单介绍一下游戏规则。游戏规则:
游戏人物从屏幕上方按一定速率下落,同时台阶从屏幕下方随机产生,产生之后向上按一定速率垂直上升。人物踩到台阶后会跟随台阶一起上升,但是人物触碰到顶部尖刺或者跌入底部会死亡,进而停止游戏。
这里台阶一共会有五种。
其中接触到一般台阶会回复三分之一的生命值,加满为止,碰到尖刺台阶会减少三分之一的生命值,减完为止,崩溃台阶只要一接触就会消失,进而人物继续下降,但是不会少血,电梯台阶有两种,一种向左,一种向右,人物落上去即使不用方向键操作,也会快速向左向右移动。所有台阶位置都是随机产生的。
从以上游戏玩法中我们可以看出制作这个游戏所要解决的核心问题有以下这么几点:
1.如何在随机的位置产生台阶。
2.如何区分台阶种类并分别进行贴图。
3.如何进行碰撞检测,当然,这是最复杂的一个问题,因为一共有五种台阶,接触之后会产生不同反应,但是,实现的方式并没有想象中的那么复杂,一会儿我将会为大家揭晓谜底。
4.如何控制血量。
5.如何结束游戏,游戏结束的条件。
首先我们先不要急于解决这些问题,因为这是一个游戏,是游戏,就会有许多实体产生,比如游戏人物,台阶,边框,背景,血槽等,所有这些物体,必须找到承载他们的空间,就好比做饭,不论你是做何等的山珍海味,都必须用到锅碗瓢盆,大米白面这些最基本的东西。
好了,先来看看我们要用到的头文件:
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<gl\glut.h>
#include<time.h>
随后是一些基本常数设置,比如窗口大小,台阶长度,台阶宽度,有了这些常数,我们就可以一次性更改代码里所有的关于这些常量的部分。
#define STEP_WIDTH 150//台阶宽
#define STEP_HEIGHT 20//台阶长
#define PLAYER_WIDTH 40//人物宽
#define PLAYER_HEIGHT 40//人物长度
#define STEP_NUM 20//生成台阶总量
#define Player_Move_Speed 20//人物左右移动速度然后我们就开始定义我们的游戏中出现的物体了,首先是坐标结构体,有了它,表示坐标和渲染图形时会很方便。这里注意我们将该结构体定义为两种数据类型,一种是该结构体本身的类型,另一种是它的指针类型。
typedef struct
{
int x;
int y;
}Point,*pPoint;然后定义台阶专属的数据类型
typedef struct Step
{
Point pt;
int type;//0:normal,1:hole,2.Spine,3Left,4,Right
}Step,*pStep;这里的台阶是用坐标表示的,所以用上面定义的表示坐标的Point类型来建造大小为4的一个数组,分别表示左上,右上,左下,右下四个点,还有一个实数来表示该台阶的类型,这个实数由随机函数产生,好的,五种台阶如何表示这个问题我们我们已经解决了,就是用这简简单单的一个数字,但是不能小瞧这一数字,它在以后区分五种碰撞效果,以及对五种台阶分别进行贴图时,起着至关重要的作用。
分割线来一条:
其实在我公布结果之前,你脑中也许已经有了一种方案来表示台阶的种类了,以前我还想过用C++里的vector动态数组,也就是在代码开头加上#include<vector>,然后声明五条vector链表,用push_back()来进行添加,这种方法最后破产了,因为我不会删除链表里的元素(呵呵~),也就是说我无法使得已经到达顶端的台阶删除,但我不可能放任台阶无限增加,那样内存会占用越来越多,最后导致崩溃,当然这样的游戏表面看起来并没有什么不同,即使不删除台阶,当它们上升到顶端超出窗口范围时,他们也会消失在你视线里,但是它们所占用的内存并没有被返回,这样它也就称不上是一款完整的游戏。
在这里,我其实想说(当然我不是告诉大家要细心,这个上课老师会强调的)写代码就如同写文章,或者是绘画,一千个人动笔就会有一千种结果,这就是程序的魅力所在,用你自己所创造的方法,得到满意结果的那一瞬间,不知道大家是不是,反正我是有无比的成就感的,就好像你玩了一年的英雄联盟账号终于升到了王者一样。
言归正传,下面我们生命一个结构体用来表示游戏人物,这就要简单多了,因为游戏人物只有一个,为了方便起见,我们还是把它声明为普通数据类型和对应的指针类型,与游戏人物相同性质的血量(这里的相同性质指的是他们在游戏中的数量都是一)也用这个结构体来定义
typedef struct
{
Pointpt;
}Player,*pLife,Life;其中Point类型的数组表示人物(矩形)和生命值(矩形)的四个顶点。
当然之定义结构体是没用的,要想在程序中应用这些结构体,必须用结构体变量类型生成实体,也就是结构体变量。
pStep Step_array;
Player player;
pLife life;
GLuint TexId;从名字我们可以1清楚知道他们分别代表台阶,人物和血量,第四行是贴图编号,现在大家可以忽略。现在我们写一下基本的main函数,想必大家在课堂上都学习过这些代码的内涵了吧,无非就是最基本的窗口大小,背景颜色,回调函数(call back function)的登录(或者叫注册?),随着程序的深入,这个main函数会不断完善。
int main(int argc, char **argv)
{
//srand((unsigned)time(NULL));
glutInit(&argc, argv);
glutInitWindowSize(WIDTH, HEIGHT);
glutInitWindowPosition(0, 0);
glutInitDisplayMode(GL_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutCreateWindow("Man Down");
glutReshapeFunc(Reshape);
glutTimerFunc(100, TimeFunc, 0);
glutKeyboardFunc(KeyboardFunc);
glutMainLoop();
return 0;
}现在我们要进行这个游戏的重头戏之一————生成台阶了,玩过是男人就下100层的朋友可能知道,台阶是从游戏窗口底部开始生成,生成之后不断上升,直到上升到窗口顶端消失,当然,生成台阶时位置必须是随机的。
首先来看随机函数,这里我们所要生成的东西其实就是台阶的一个点而已(暂定为左下点,命名为初始点),因为其余的点都可以用这个初始点外加台阶的长和宽来得到(这个已经在前面声明成了常量),关于随机函数这里不再赘述,分为真随机和假随机函数,网络上相关资源很多,这里我们为了演示和说明的便利,采用假随机函数(假随机函数每次打开游戏所生成的台阶位置和上一次打开时一样,是固定不变的,真随机则每次都会不同),当然,我们在生成初始点时必须考虑到今后要制作的游戏边框范围以及台阶的宽度,也就是说我们生成的初始点必须在这个范围之内。
int Rand_x()
{
int i = (int)(rand() % (WIDTH - 150 - 200 + 1) + 200);
return i;
}由这个函数我们可以知道随机函数产生了一个最大范围为屏幕宽度减去台阶宽度,最小范围为从左开始200像素(也就是我们准备绘制边框的范围),这样我们绘制的台阶才不至于跑到边框外面。
之后要构造台阶生成/销毁装置,我们管他叫台阶管理器。
上面这幅图就是我们的台阶管理器它的大小为20,具体运作方式是,每一个元素存储一个台阶信息(位置坐标,台阶类型)的指针,将它放置在渲染函数里,并将所有指针指向NULL(初始化), 每次调用这个管理器函数时,程序都会从头到尾检查该指针数组,看是否有空元素(指向NULL的元素),如果有,利用malloc函数为其分配内存,生成一个台阶,当台阶上升到最顶端时,再用free()函数释放其内存,并使其重新指向NULL,这样在下一次循环中,该元素又可以被重新利用来存放新台阶信息。在这个管理器中,台阶“自动生成”,“自动销毁”,无限循环,直到游戏结束,并且台阶所占内存被限制在20个指针数组中,所有元素都会被再利用,达到资源最优的效果。
void InitStep()
{
for (int i = 0; i < STEP_NUM; i++)
{
Step_array = NULL;
}
}
void Compute_Step()
{
int density = rand() % 10;
if (density == 2) {
int ix = -1;
for (int i = 0; i < STEP_NUM; i++)
{
if (Step_array == NULL)
{
ix = i;
break;
}
}
if (ix == -1)return;
Step_array = (pStep)malloc(sizeof(Step));
Step_array->pt.x =(int) Rand_x();
Step_array->pt.y = 0;
Step_array->pt.x = Step_array->pt.x + STEP_WIDTH;
Step_array->pt.y = 0;
Step_array->pt.x = Step_array->pt.x;
Step_array->pt.y = STEP_HEIGHT;
Step_array->pt.x = Step_array->pt.x;
Step_array->pt.y = STEP_HEIGHT;
Step_array->type = rand() % 5;
}
}
以上就是我们的台阶管理器函数的代码和其初始化代码,细心的朋友已经发现,我在函数最一开始加了两行这样的代码:
int density = rand() % 10;
if (density == 2)
"density"顾名思义,是控制台阶产生密度的一个变量,如果没有该代码,在每次调用该管理函数时,程序会一次性检查出所有指针数组的空位,20个台阶会完全铺满屏幕,像这样:
这显然是不行的,因此,我们设置一个随机函数,产生0~9十个整数,当且仅当该数字等于2时,生成台阶,这样,台阶生成就稀疏好多:
下面考虑该把这个函数放在哪里才能达到让其不间断的被调用,这里,我们将它放置在渲染函数里,因为渲染函数将会在时间函数中重复调用,因此,将它放在渲染函数中最合适不过了,下面我们就来写渲染函数与时间函数:
void Render()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SetupViewTransform();
SetupViewVolume();
glMatrixMode(GL_MODELVIEW);
DrawBounder();
DrawPlayer();
Compute_Step();
DrawStep();
Draw_Life();
glutSwapBuffers();
} void TimeFunc(int id)
{
if (id == 0)
{
Collide();
Update_Player();
Update_Step();
//Collide();
glutPostRedisplay();
glutTimerFunc(100, TimeFunc, 0);
}
}其中还有好多我们现在还没有涉及的函数,大家自动忽略就行,那些我以后会将,不要着急,现在你只需要知道,我们的台阶管理器已经被放置在了时间函数里了,这就足够了!
接下来我们就要根据台阶管理器来对台阶进行渲染了,这里我们要用到Texture mapping的一些知识,很简单,全都是一些固定的函数而已,所谓Texture mapping 并不像它的名字那样高大上,它的原理有点类似于我们小时候玩的模型玩具,比如高达模型,在拼装完机器人主体时 一些收藏玩家会在模型上贴上随包装附带的贴纸,使自己的模型看上去更加帅气(我个人不怎么喜欢贴贴纸)
就像上图所显示的那样,每一张贴纸都对应模型上的一个部位,只有贴对位置才会出效果,并且我们发现,每一个贴纸上都是有编号的 ,模型爱好者们通过这些编号在说明书上找到贴纸应该贴的位置,这与OpenGL中的Texture Mapping的原理如出一辙!所有用于Texture Mapping的BMP图片都是要通过一定的函数来进行编号的!
首先我们需要一个能读取BMP图片的函数,这个在网上或者学校上课的讲义中都应该讲过的,这里不多余赘述:
unsigned char *ReadBmpFile(char *fname, int *w, int *h)
{
FILE *fp = fopen(fname, "rb");
unsigned char *pData; // The pointer to the memory zone in which we will load the texture
// windows.h gives us these types to work with the Bitmap files
BITMAPFILEHEADER fileheader;
BITMAPINFOHEADER infoheader;
RGBTRIPLE rgb;
// Read the fileheader
fread(&fileheader, sizeof(fileheader), 1, fp);
fseek(fp, sizeof(fileheader), SEEK_SET); // Jump the fileheader
fread(&infoheader, sizeof(infoheader), 1, fp); // and read the infoheader
*w = infoheader.biWidth;
*h = infoheader.biHeight;
int size = infoheader.biWidth * infoheader.biHeight;
// Now we need to allocate the memory for our image (width * height * color deep)
pData = new byte;
// And fill it with zeros
memset(pData, 0, size * 4);
for (int i = 0, j = 0; i < size; ++i)
{
fread(&rgb, sizeof(rgb), 1, fp); // load RGB value
pData = rgb.rgbtRed;
pData = rgb.rgbtGreen;
pData = rgb.rgbtBlue;
pData = 255; // Alpha value
j += 4;
}
fclose(fp);
return pData;
}如果大家在理解方面有问题的话,就直接放弃吧,对,没错,有时候我们并不需要纠结于次要的东西,我们只需要知道,调用这个家伙时,需要给予它三个参数,分别是文件(图片)名称,图片长以及图片宽。
要注意,这里的图片长和图片宽不是用户自定义的,而是在函数读取到BMP图片信息后,程序会将正在读取的BMP图片的长和宽的信息赋在事先你定义好的空变量里,好了,也许这么说会有些绕口,给大家举个例子就知道了。
int w, h;
unsigned char *pImage;
pImage = ReadBmpFile("UI/pic1.bmp", &w, &h);如图,该函数的返回值是unsigned char指针类型,我们就声明一个pImage来接收图像,然后在ReadBmpFile里第一个·参数是图片名,注意加上地址,然后在项目文件夹里添加上你的图片文件夹,这里,我的图片文件夹是叫“”UI",图片名称为"pic1.bmp",图片的长与高的信息被存放在事先声明好的整数变量w与h中。
之后,就是给我们在制作这个游戏所用到的图片进行编号,我们就为它起一个名字叫“InitOpenGL()”吧这个函数仅需要在main函数中调用一次即可,我们先看函数,后为大家详细解释:
void InitOpenGL()
{
glEnable(GL_DEPTH_TEST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glGenTextures(10, TexId);
int w, h;
unsigned char *pImage;
pImage = ReadBmpFile("UI/pic1.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[]pImage;
pImage = ReadBmpFile("UI/pic2.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic3.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic4.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic5.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic6.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic7.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic8.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic9.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
pImage = ReadBmpFile("UI/pic10.bmp", &w, &h);
glBindTexture(GL_TEXTURE_2D, TexId);
glTexImage2D(GL_TEXTURE_2D, 0, 4, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pImage);
delete[] pImage;
}第一眼看上去这个函数确实有些复杂,但实际上,这些代码只是在重复同一项任务————对于将要在Texture mapping中运用到的图片进行处理。
其中涉及到的一些不认识的函数,我建议大家去到网站上查一下,毕竟自己查来的东西可能记忆更加深刻一些吧~(*__*),不过还是给大家讲讲吧^_^~~
glTexEnvf():定义贴图环境。
glGenTexture():规定所用贴图的数量以及所存在的数组名。
glBindTexture():将编号与特定图片绑定
glTexImage2D():函数原型:
GL_APICALL void GL_APIENTRY glTexImage2D(GLenum target, GLint level, GLenum internalformat,
GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels);
参数说明:
target 指定目标纹理,这个值必须是GL_TEXTURE_2D。
level 执行细节级别。0是最基本的图像级别,n表示第N级贴图细化级别。
internalformat 指定纹理中的颜色组件。可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等几种。
width 指定纹理图像的宽度,必须是2的n次方。纹理图片至少要支持64个材质元素的宽度
height 指定纹理图像的高度,必须是2的m次方。纹理图片至少要支持64个材质元素的高度
border 指定边框的宽度。必须为0。
format 像素数据的颜色格式, 不需要和internalformatt取值必须相同。可选的值参考internalformat。
type 指定像素数据的数据类型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1。
pixels 指定内存中指向图像数据的指针(网上找的,说的已经很详细啦~~,链接在这里http://baike.baidu.com/link?url=7t1EAJCxmwi1qSI3f9lsWAn0QPUSCw378mfvgfSVHzXKc3TuaUPYSGzITqTbmJI7V1w0vF40OO4K3leOSDuSWK)
glBuild2DMipmaps():gluBuild2DMipmaps(GL_TEXTURE_2D,//此纹理是一个2D纹理
3, //颜色成分
w, //纹理的宽度
h, //纹理的高度
GL_RGB, //告诉OpenGL图像数据由红、绿、蓝三色数据组成
GL_UNSIGNED_BYTE, //组成图像的数据是无符号字节类型
pImage); //告诉OpenGL纹理数据的来源,此例中指向存放在pImage记录中的数据)(这也是摘抄的,链接在这里~http://www.cnblogs.com/qingsunny/archive/2013/03/25/2980727.html)
到此为止,我们对texture mapping所要用到的图片的最基本处理也就完成了,这么繁杂的代码其实只做了两件事——告诉程序,我要开始贴图了哦~,还有我要用这些图片贴哦~
然后就可以对台阶管理器中的台阶进行渲染了。
void DrawStep() {
for (int i = 0; i < STEP_NUM; i++)
{
if (Step_array != NULL&&Step_array->type == 0)//Norrmal
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glEnd();
}
if (Step_array != NULL&&Step_array->type == 1)//hole
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glEnd();
}
if (Step_array != NULL&&Step_array->type == 2)//Left
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glEnd();
}
if (Step_array != NULL&&Step_array->type == 3)//Right
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glEnd();
}
if (Step_array != NULL&&Step_array->type == 4)//Spine
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(Step_array->pt.x, Step_array->pt.y, 0);
glEnd();
}
}
}对于台阶的渲染的总体方法就是,在渲染之前先用if语句判断它是属于哪一个种类的台阶,然后再找对应图片的编号进行贴图。
既然说到了渲染,那我们就先把游戏中需要制作的边框,人物等的渲染代码全都贴上来吧,都很简单,全部是在固定位置画上固定图案的代码:
void Compute_Player()
{
player.pt.x = 400;
player.pt.y = 760;
player.pt.x = player.pt.x+PLAYER_WIDTH;
player.pt.y = 760;
player.pt.x = player.pt.x + PLAYER_WIDTH;
player.pt.y = player.pt.y + PLAYER_HEIGHT;
player.pt.x = player.pt.x;
player.pt.y = player.pt.y + PLAYER_HEIGHT;
}
void DrawPlayer()
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glTranslatef(0, 0, 0);
glBegin(GL_POLYGON);
glTexCoord2i(0, 0);
glNormal3i(0, 0, 1);
glVertex3i(player.pt.x, player.pt.y, 0);
glTexCoord2i(1, 0);
glNormal3i(0, 0, 1);
glVertex3i(player.pt.x, player.pt.y, 0);
glTexCoord2i(1, 1);
glNormal3i(0, 0, 1);
glVertex3i(player.pt.x, player.pt.y, 0);
glTexCoord2i(0, 1);
glNormal3i(0, 0, 1);
glVertex3i(player.pt.x, player.pt.y, 0);
glEnd();
}其中Compute_Player()事先计算好游戏人物的位置,将坐标存在player类型的结构体变量player中,之后再对他进行渲染。
void DrawBounder()
{
//left
glPushMatrix();
glTranslatef(0, 0, 0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2i(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(200, 0, 0);
glTexCoord2i(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210, 0, 0);
glTexCoord2i(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210, HEIGHT, 0);
glTexCoord2i(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(200, HEIGHT, 0);
glEnd();
//top
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
{
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210, HEIGHT, 0);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210, HEIGHT - 20, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210 + WIDTH, HEIGHT - 20, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(210 + WIDTH, HEIGHT, 0);
}
glEnd();
//right
glBindTexture(GL_TEXTURE_2D, TexId);
glBegin(GL_POLYGON);
glTexCoord2d(0, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(WIDTH, HEIGHT, 0);
glTexCoord2d(1, 1);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(WIDTH - 10, HEIGHT, 0);
glTexCoord2d(1, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(WIDTH - 10, 0, 0);
glTexCoord2d(0, 0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(WIDTH, 0, 0);
glEnd();
//button
glBegin(GL_POLYGON);
glVertex3i(210 + WIDTH, 10, 0);
glVertex3i(210, 10, 0);
glVertex3i(200, 0, 0);
glVertex3i(200 + WIDTH, 0, 0);
glEnd();
}这是对于边界的渲染,都是“体力活儿”,打的时候会辛苦一点
别忘了把他们都要加进设置的渲染函数中去,我的渲染函数叫Render(),刚才贴过了,现在再贴一次:
void Render()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SetupViewTransform();
SetupViewVolume();
glMatrixMode(GL_MODELVIEW);
DrawBounder();
DrawPlayer();
Compute_Step();
DrawStep();
Draw_Life();
glutSwapBuffers();
}怎么样,是不是和刚才比起来,不理解的地方要少了?
下面我们再处理另一项,键盘控制的回调函数:
void KeyboardFunc(unsigned char key, int x, int y)
{
if (key == 'a')
{
if (player.pt.x > 200+(PLAYER_HEIGHT)/2)
{
for (int i = 0; i < 4; i++)
{
player.pt.x -= Player_Move_Speed;
}
}
}
if (key == 'd')
{
if (player.pt.x < WIDTH-(PLAYER_WIDTH)/2)
{
for (int i = 0; i < 4; i++)
{
player.pt.x += Player_Move_Speed;
}
}
}
}要注意我们的人物只能在边框范围内左右移动
此处需要分割线
现在我们就要开始这个游戏最关键的部分———碰撞检测了,当游戏人物降落到台阶上时,人物必须停下来,就像我们踩在地面上一样,这就是我们要进行的所谓碰撞检测和碰撞效果程序的编写。
也许大家会问,我们为什么要进行碰撞检测程序的编写,难道神通广大的OpenGL难道就没有一个函数可以帮助我们进行碰撞检测吗?当然是没有的,这里大家要明白一点,OpenGL是图形接口通俗一点讲,OpenGL所涉及到的函数基本都只与画图有关,再通俗一点讲它就是用来画画的,虽然这么说有些不精确,但是,类似于重力,摩擦力,以及各种物理上的作用力与反作用力在OpenGL里是没有的,大家也能体会到,刚才我们仅仅是在正方形上贴了几个贴画儿而已,代码的数量已经开始呈指数上升了,说明OpenGL只是一个比较底层的API它不是像unity,unreal一样支持各种可视化操作的物理引擎(事实上,物理引擎也基本都是用这种底层图形API编写的
首先我们要判定什么时候碰撞,也就是当人物踩到台阶上时,人物会停在上面,换句话说,当人物进入到台阶的坐标范围时,或说当人物坐标与台阶坐标有某种意义上的重叠时,人物会改变原先的移动方式,会与台阶一样同向同速运动,这样,人们的感官就会感知到,人物停在了台阶上
我们先来看人物与台阶各自最开始的运动方式:
void Update_Player()
{
if (Collide_d == false)
{
for (int i = 0; i < 4; i++)
{
player.pt.y -= 10;
}
}
if (player.pt.y > HEIGHT)
{
exit(1);
}
}可以看到如果没有碰撞(预先定义碰撞感知布尔变量Collide_d)人物会以十的速度匀速下降,并且到达窗口最低端时游戏结束。
void Update_Step()
{
for (int i = 0; i < STEP_NUM; i++)
{
if (Step_array != NULL)
{
for (int j = 0; j < 4; j++)
{
Step_array->pt.y+=10;
}
if (Step_array->pt.y>HEIGHT)
{
free(Step_array);
Step_array = NULL;
}
}
}
}先给大家看一幅图:
这幅图告诉我们,当且仅当人物的1点在台阶的3点的右边并且当人物的0点在台阶2点的左边,并且0点的纵坐标与3点纵坐标相等时,碰撞发生
用程序来翻译这句话就是:
if ((player.pt.x>Step_array->pt.x) && (player.pt.x < Step_array->pt.x) && (player.pt.y <= Step_array->pt.y)&& (player.pt.y >= Step_array->pt.y-10))
Collide_d = true;也许大家注意到,在最后我并没有判断y坐标相等,而也是用了一个区间,这与时间函数有关,时间函数的调用频率与OpenGL帧刷新频率不符,导致一些碰撞会检测不到,因此,这里y坐标的检测也会是一个范围
这会是一个基本的碰撞,但是我们这个游戏的乐趣在于,我们有五种不同的台阶,人物跟这五种台阶分别碰撞会分别产生五种不同效果,这样,我们的碰撞代码就变成了这样:
void Collide()
{
bool id = false;
for (int i = 0; i < STEP_NUM; i++)
{
if (Step_array != NULL)
{
if ((player.pt.x>Step_array->pt.x) && (player.pt.x < Step_array->pt.x) && (player.pt.y <= Step_array->pt.y)&& (player.pt.y >= Step_array->pt.y-10))
{
//player.pt.y = Step_array->pt.y;
Collide_d = true;
id = true;
if (Step_array->type == 0)//normal
{
int temp_idex = i;
if (Collide_Normal_Spine == 1 && temp_idex != Collide_Index_Normal)
{
Collide_Normal_Spine = 0;
}
if (Collide_Normal_Spine == 2 && temp_idex != Collide_Index_Normal)
{
Collide_Normal_Spine = 1;
}
Collide_Index_Normal = temp_idex;
}
if (Step_array->type == 1)//hole
{
Delete_Hole = true;
Hole_Step_index = i;
//free(Step_array);
//Step_array = NULL;
}
if (Step_array->type == 4)//spine
{
int temp_index2 = i;
if (Collide_Normal_Spine == 0 && temp_index2 != Collide_Index_Spine)
{
Collide_Normal_Spine = 1;
Collide_Index_Spine = temp_index2;
}
if (Collide_Normal_Spine == 1 && temp_index2 != Collide_Index_Spine)
{
Collide_Normal_Spine = 2;
Collide_Index_Spine = temp_index2;
}
if (Collide_Normal_Spine == 2 && temp_index2 != Collide_Index_Spine)
{
Collide_Normal_Spine = 3;
Collide_Index_Spine = temp_index2;
}
}
if (Step_array->type == 3)//right
{
Collide_Right = true;
}
else Collide_Right = false;
if (Step_array->type == 2)//left
{
Collide_Left = true;
}
else Collide_Left = false;
}
//else(Collide_d == false);
}
if (id == false)Collide_d = false;
}看了这个代码,也许大家会产生很多疑惑,不用着急,我会为大家一一解释:
首先,大家可以看到许多像Collide_d,id,Collide_Right之类的布尔类型的变量,这些变量是用来控制台阶碰撞后发生的变化的,在任务与台阶发生碰撞后,人物与台阶的状态或者运动方式都会发生改变,尽管你完全可以将这些描述变化的代码写进你的碰撞函数里去,完全没有任何错误,但是,本着让代码简洁清楚明了的原则,我们不想让这个函数过于繁杂,因此,我们只在碰撞函数里安放碰撞开关,也就是简单的碰撞触发器(trigger),也就是大家看到的bool类型的Collide_d和Collide_left等变量,之后再在台阶与人物变化函数Update_player()和Update_Step()中对人物与台阶的状态进行变化。下面是经过改变后的Update函数:
void Update_Player()
{
if (Collide_d == false)
{
for (int i = 0; i < 4; i++)
{
player.pt.y -= 10;
}
}
if (Collide_d == true)
{
for (int i = 0; i < 4; i++)
{
player.pt.y += 10;
//player.pt.y = Step_array->pt.y;
if (Collide_Left == true)
{
player.pt.x -= 10;
}
if (Collide_Right == true)
{
player.pt.x += 10;
}
}
}
if (player.pt.y < 0)
{
exit(1);
}
if (player.pt.y > HEIGHT)
{
exit(1);
}
}
void Update_Step()
{
if (Step_array != NULL&&Delete_Hole==true)
{
free(Step_array);
Step_array = NULL;
Delete_Hole = false;
}
for (int i = 0; i < STEP_NUM; i++)
{
if (Step_array != NULL)
{
for (int j = 0; j < 4; j++)
{
Step_array->pt.y+=10;
}
if (Step_array->pt.y>HEIGHT)
{
free(Step_array);
Step_array = NULL;
}
}
}
}最后是血量控制,也就是当人物踩在带有尖刺的台阶上时,血量减少三分之一,再次碰到,继续减少三分之一,直到血量为零之后游戏结束。
要注意一点,当人物与尖刺台阶碰撞时,一定要记录碰撞的台阶编号(index),当人物再次碰到其他,只有当现在的index与刚才记录的index不同的时,才继续减少血量,如果不进行记录的话,会导致只要人物接触到尖刺台阶,血槽就会立刻变空的情况。
以下是血量控制与血槽渲染的代码:
void Init_Life()
{
//0
life = (pLife)malloc(sizeof(Life));
life->pt.x = 20;
life->pt.y = HEIGHT-10;
life->pt.x = 20;
life->pt.y = HEIGHT-50;
life->pt.x = 140;
life->pt.y = HEIGHT - 50;
life->pt.x = 140;
life->pt.y = HEIGHT - 10;
//1
life = (pLife)malloc(sizeof(Life));
life->pt.x = 20;
life->pt.y = HEIGHT - 10;
life->pt.x = 20;
life->pt.y = HEIGHT - 50;
life->pt.x = 100;
life->pt.y = HEIGHT - 50;
life->pt.x = 100;
life->pt.y = HEIGHT - 10;
//2
life = (pLife)malloc(sizeof(Life));
life->pt.x = 20;
life->pt.y = HEIGHT - 10;
life->pt.x = 20;
life->pt.y = HEIGHT - 50;
life->pt.x = 60;
life->pt.y = HEIGHT - 50;
life->pt.x = 60;
life->pt.y = HEIGHT - 10;
//3
life = (pLife)malloc(sizeof(Life));
life->pt.x = 20;
life->pt.y = HEIGHT - 10;
life->pt.x = 20;
life->pt.y = HEIGHT - 50;
life->pt.x = 20;
life->pt.y = HEIGHT - 50;
life->pt.x = 20;
life->pt.y = HEIGHT - 10;
}
void Draw_Life()
{//0
if (Collide_Normal_Spine == 0)
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glTranslatef(0.0, 0.0, 0.0);
glPushMatrix();
glBegin(GL_POLYGON);
{
glTexCoord2d(0.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(0.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
}
glEnd();
glPushMatrix();
}
if (Collide_Normal_Spine == 1)
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glTranslatef(0.0, 0.0, 0.0);
glPushMatrix();
glBegin(GL_POLYGON);
glTexCoord2d(0.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(0.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glEnd();
glPushMatrix();
}
if (Collide_Normal_Spine == 2)
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TexId);
glTranslatef(0.0, 0.0, 0.0);
glPushMatrix();
glBegin(GL_POLYGON);
glTexCoord2d(0.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(0.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 0.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glTexCoord2d(1.0, 1.0);
glNormal3f(0.0, 0.0, 1.0);
glVertex3i(life->pt.x, life->pt.y, 0);
glEnd();
glPushMatrix();
}
if (Collide_Normal_Spine == 3)
{
glColor3f(1.0, 0.0, 0.0);
glTranslatef(0.0, 0.0, 0.0);
glPushMatrix();
glBegin(GL_POLYGON);
for (int i = 0; i < 4; i++)
{
glVertex3i(life->pt.x, life->pt.y, 0);
}
glEnd();
glPushMatrix();
Sleep(1000);
exit(1);
}
}到此为止,我们的2D游戏的主要代码已经全部讲解完毕了,我们做的只是一个利用OpenGL做的最简单的游戏了,大家可以以这个代码为参照,加入背景音乐,游戏开始选项,暂停选项,以及更加好看的背景等,这如介绍中说的那样,这只是抛砖引玉,希望能对大家的学习带来帮助,如果需要完整代码或是有问题的朋友,加QQ852170906,谢谢大家。
文档来源:开源中国社区https://my.oschina.net/dada0526/blog/709731
页:
[1]