文章目录
- 1. 介绍
- 2. 设计
- 3. 准备阶段
- 4. 角色构建
- 5. 场景构建
- 6. 交互部分
- 6.1 键盘交互
- 6.2 鼠标交互
- 6.3 鼠标点击出多级菜单进行交互
- 7. 缺点与问题
- 7.1 程序bug
- 7.2 游戏乐趣不足
- 7.3 画面不够好看
- 8. 完整代码
1. 介绍
前面已经分享过了关于CPT205的CW1的2D作业,这次CW2要求的是3D作业,但是主题不限,不像上次规定了必须得是生日贺卡。这里将以这个大作业为例,讲解OpenGL的3D实践。
首先虽然我们来到了3D作业,但我们能靠OpenGL做一个类似于我们现在所处的世界一样的作业吗?答案当然是不能。虽然现在的游戏或者说计算机图形学已经可以让我们能做出接近现实世界的虚拟世界,但是那需要非常细致的建模和设计。很明显不是作业要求的。我们使用的工具较为简单,我们需要考虑的主要是以下几个问题:1. 建模建成什么样的?2. 怎么给建好的模型上色?3D不像2D那样直接在建模的时候就给物体赋予了颜色,3D的颜色最后要靠灯光和材质等最后呈现。当然我们有贴图,贴图也可以让我们的模型呈现出我们想要的颜色或者样子。3. 我们建好的这些模型在一起是什么主题?虽然作业要求中没有给出我们的主题,但是我不能这里摆几个方形的房子,那里摆几个方形的车,这里摆几个火柴人,在一起完全不是一个主题的在一起。或者说由于它们是一个主题的,所以它们有相同的画风,因此它们在一起给人以一种和谐的感觉,人们才会愿意沉浸在这个虚拟的世界中,如果我们搭建的场景里的不同对象有不同的画风,那组合在一起用户就很容易与这个虚拟世界脱离。
相较于一般做作业的时候我们先想好主题再围绕主题开展,这次我更推荐先去思考我能做出或者找到什么贴图,然后是我该怎么用贴图。因为比如我想做一个类似真实世界的贴图,我拍了很多现实世界的照片作为贴图,很明显我们只有主题和贴图,其余的部分不是我们所能完成的。但是我们如果找到一系列符合某个主题的贴图,或者说我们把一个画风的贴图在一起我们就找到了我们的主题。这便是我想说的这次作业和上次作业的一大不同,只有这样才能做出漂亮优秀的作业。当然这次作业和上次作业一样,你要用到之前学习的知识,相关的知识可以参考这几个链接CPT205计算机图形学Pt.2
CPT205计算机图形学Pt.3
CPT205计算机图形学Pt.4
下面我将和前面一样,分享自己的作业以及想法,并且说出我的问题,希望能帮助到大家。
2. 设计
其实刚开始我想做一个博物馆出来,然后博物馆里面全都是画作,用户可以控制自己的角色再里面移动,观看这些画作。但是问题主要是我当时想的是做卢浮宫,卢浮宫是由玻璃组成,想做出一个逼真的玻璃很明显不能依赖于贴图,因为贴图是静态的,不会因为外部环境的变化而出现变化。我感觉在这里freeglut库中没有能让这里某个物体拥有类似玻璃的那种反光效果的材质的方法。在尝试很多种方法后,我放弃了。
当然你也可以不选择做一个卢浮宫出来,你可以做一个正常的哥特式建筑风格的博物馆,里面摆满画,然后玻璃采用哥特式建筑的彩色玻璃,你或许在想这里的贴图怎么办。我这里推荐一个找资源的好渠道——Quixel BridgeQuixel Bridge官网,在这个上面你可以找到免费的高清资源,其中就有我前面说的需要的资源。但这个网站是全英文的,如果遇到问题还可以查看以下b站上的一些视频教程,然后搜索资源的时候也记得使用英文去搜索。
当然当我前面的弯路走完后我感觉自己已经没有足够的时间去建造一个哥特式建筑了,因为我想做成有外景和内景的博物馆。当然如果有外景的画我们还需要做一个skybox(天空盒),也就是我们建造的这个虚拟世界是一个正方体,活动空间在这个正方体之内。用户在正方体内,所以正方体内部的六个面就是这个世界的地、天与四周。其实我们如果在网上搜索skybox资源网上也会有6张图组成的skybox资源以供使用,当然也可以选择自己去做6张图组成的skybox。所以如果做的就是博物馆的内景的画就可以避开这一步,当然也有一些方式去避开,比如规定用户无法看到头上的天空,即固定视角等。
下面就是我的正式作业,我回归到了之前上一次的卡比主题,因为模拟真实的灯光效果之类的太难了,我还是决定回到卡通的这种赛道上,而且我认为卡比相关的建模和贴图都比较简单。上次用户是用户,卡比是卡比,这次我们可以让卡比作为故事的主角,用户自己操作卡比,让卡比像一个正常角色一样去移动。如果我们玩过一些游戏就知道,要是角色移动的话就会有腿部和手上的动作,甚至角色身上的衣服也会动,但是卡比可以坐着星星,也就是说卡比像开飞船一样开着星星移动,这样就避开了要做一些动画的问题。我们将卡比和星星绑定在一起就可以实现卡比的移动,但是只是让他们进行wasd的移动是不够的,我们虽然没开过星星也没开过飞船,但是我们看过别人骑马,当骑马人想让马停下就会让马前半身抬起来,当骑马人想冲刺,就会压低自己的身姿。我们可以让刚刚卡比和星星的整体在某个方向上旋转从而模拟出这种物理效果,从而提高画面的真实感。
向前的侧视图如下图所示。
向后的侧视图如下图所示。
这里我设计的是ad负责控制卡比转向,因为显示中没人可以斜着走路,都是往前或者往后走,因此我是这样转向的设计。
我们现在规定卡比可以移动,那做什么呢?如果我们想进一步提升这里的交互性,那只移动可不够,控制的角色应该能做出更多的事情,绝不仅仅是移动那么简单。我想的是可以和前面CW1的主题联系在一起。CW1中卡比是去给用户庆祝生日的,我们现在可以做成卡比还可以攻击,为什么要攻击,攻击是为了保护一个东西——给朋友生日准备的礼物。这样我们就有了一个主题。卡比该怎么攻击能,不像别的一些角色的攻击,我们前一篇文章说过,卡比可以吞噬物体,然后变身获取对应物体的能力,但在普通状态下,卡比也可以吞噬物体,然后将其变成星星发射,所以卡比的攻击我们可以做成星星从卡比嘴里发出,从而攻击。因此我们的这个程序的基本玩法也确立了下来,站在星星上的卡比,努力攻击尝试拿走或者破坏生日礼物的敌人。敌人被卡比的星星击中就会死亡,当然怪物也可以攻击卡比,卡比的生命有限,要是一直被攻击,那卡比也会晕过去,无法及时参加朋友的生日,所以失败。当然怪物要是碰到生日礼物,游戏也会结束,因为生日礼物被破坏了。因此前面的转向设计也方便了这里的攻击操作。
所以我们就确定了它的整个主题,我这里想做成类似于RPG(角色扮演游戏)的操作和视角变化,即用户可以控制角色移动的同时还可以控制摄像机与角色的相对位置,可以方便用户在控制角色攻击的同时观察不同地方的敌人,或者控制摄像机以让用户到自己习惯的角度进行操作。所以我这样的设计是很好的,我给用户更多的选择机会,从而提高了交互性。当然你也可以做成固定视角,比如第一视角,但是这样就没法展现出你做的角色模型。
所以我们就基本确定了整个我们作业的内容了,玩家操作卡比保护生日礼物免遭周围的怪物的破坏,同时玩家也要让卡比免遭怪兽的攻击。
3. 准备阶段
准备阶段依然像前文一样,围绕贴图来准备,所以首先是贴图方法。注意贴图需要的图片是bmp格式,我使用电脑上的画图软件将图片转换成bmp格式导出从而得到的bmp格式。
//This function is reading the image for the texture.
void ReadImage(const char path[256], GLint& imagewidth, GLint& imageheight, GLint& pixellength)
{GLubyte* pixeldata;FILE* pfile;fopen_s(&pfile, path, "rb");if (pfile == 0) exit(0);fseek(pfile, 0x0012, SEEK_SET);fread(&imagewidth, sizeof(imagewidth), 1, pfile);fread(&imageheight, sizeof(imageheight), 1, pfile);pixellength = imagewidth * 3;while (pixellength % 4 != 0) pixellength++;pixellength *= imageheight;pixeldata = (GLubyte*)malloc(pixellength);if (pixeldata == 0) exit(0);fseek(pfile, 54, SEEK_SET);fread(pixeldata, pixellength, 1, pfile);p.push_back(pixeldata);fclose(pfile);
}//This function is used for some initializations. Such as enabling depth testing, texture and lighting.
//This function call "ReadImage" for the textures and set the texture objects.
//It also set the light.
void myinit(void) {glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glShadeModel(GL_SMOOTH);glEnable(GL_TEXTURE_2D);ReadImage("kirbyFace.bmp", imageWidth[0], imageHeight[0], pixelLength[0]);ReadImage("kirbyFoot.bmp", imageWidth[1], imageHeight[1], pixelLength[1]);ReadImage("kirbyHand.bmp", imageWidth[2], imageHeight[2], pixelLength[2]);ReadImage("waddleFace.bmp", imageWidth[3], imageHeight[3], pixelLength[3]);ReadImage("waddleFoot.bmp", imageWidth[4], imageHeight[4], pixelLength[4]);ReadImage("waddleHand.bmp", imageWidth[5], imageHeight[5], pixelLength[5]);ReadImage("star.bmp", imageWidth[6], imageHeight[6], pixelLength[6]);ReadImage("water.bmp", imageWidth[7], imageHeight[7], pixelLength[7]);ReadImage("leftSky.bmp", imageWidth[8], imageHeight[8], pixelLength[8]);ReadImage("frontSky.bmp", imageWidth[9], imageHeight[9], pixelLength[9]);ReadImage("rightSky.bmp", imageWidth[10], imageHeight[10], pixelLength[10]);ReadImage("backSky.bmp", imageWidth[11], imageHeight[11], pixelLength[11]);ReadImage("topSky.bmp", imageWidth[12], imageHeight[12], pixelLength[12]);ReadImage("box.bmp", imageWidth[13], imageHeight[13], pixelLength[13]);ReadImage("specialBox.bmp", imageWidth[14], imageHeight[14], pixelLength[14]);glPixelStorei(GL_UNPACK_ALIGNMENT, 1);glGenTextures(textureNumber, &texture[0]);for (int i = 0; i < 15; i++){glBindTexture(GL_TEXTURE_2D, texture[i]);glTexImage2D(GL_TEXTURE_2D, 0, 3, imageWidth[i], imageHeight[i], 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, p[i]);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);}glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);GLfloat mat_shininess[] = { 50.0 };glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);GLfloat light_position[] = { light_x, light_y, light_z, 0.0 };glLightfv(GL_LIGHT0, GL_POSITION, light_position);glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);
}
贴图的方法在Lab中提及,先由ReadImage方法将贴图的图片读取起来,然后在myinit方法中将读取的图片制作成对应的纹理,然后在后面我们需要对应的纹理时,我们就可以直接使用,从而完成贴图。
前面我们在设计中说过这里的背景故事,以及游戏里的故事。那么如果让用户或者玩家了解这里的故事呢?你可以直接写字告诉它们,但这样很明显索然无味,不是好的解决方案。你可以用一些设计的方式潜在地告诉用户这里的故事。所以刚出场我们可以看到卡比,卡比身下会有这个礼物,我将其做成马里奥里的问号方块,这让用户有想打开的想法,但是我并没有教怎么操作。那这里的操作最好是符合人们的习惯,因此wasd操作,人们不知道怎么操作的时候就会用他们熟悉的操作方式。然后用户会发现它们可以控制卡比移动,所以他们自己就是这个卡比,但是怎么移动都无法靠近碰触到这个宝箱,所以这个宝箱不是打开的。然后用户可能会去别的地方寻找答案,他们发现这个程序的名字是Kirby Defends Gift(卡比保卫礼物),所以他们可能就会想:“哦!这个应该就是卡比要保卫的礼物。”用户此时可能发现视野里会出现一些怪物靠近这边,如果用户移动了一定的距离,就会发现那些怪物的目标是礼物,所以那些就成了这个故事里的怪物。当然这里还会有一些插曲,比如用户怎么知道是wasd移动而不是键盘上的方向键移动,方向键也是用户很容易想到的操作角色移动的方式,但那其实被我设置成了控制摄像头移动的方式,它们也能靠这里的尝试知道这样的方式,这给了用户探索的兴趣。包括鼠标我也设计了交互方式。
由于我们的作业需要使用上课学习到的知识,所以我制作了一个菜单以作为提示以演示这个程序如何交互,同时这个菜单还添加了额外功能。如下图所示。
菜单的详细情况将在第六节(交互部分)介绍。
当然在准备阶段的时候我想强调这里的游戏里的一些提示,我想到一款小时候很热门的游戏——植物大战僵尸,当玩家失败时,除了要进房子的僵尸,其余的画面都会静止,通过这样的方式,将画面聚焦于一个地方,从而告诉了玩家失败的原因。于是我准备做一个类似于2D里的过场动画,当游戏失败,画面会静止,通过一个动画让玩家聚焦于失败的地方,从而让游戏结束。当然我们无法直接使用上次2D作业里的代码,因为那是2D的,我们现在是3D的,那我们直接加一个z值就解决问题了吗?当然不是。但是我们的3D画面都会在摄像机的投影下变成2D画面呈现在屏幕上,所以我们先要想好摄像机的位置在哪里。我将其放在能清楚拍清楚失败原因的位置,然后将这个星星动画以一个与摄像机垂直的角度放置在距离摄像机一定距离的位置,这样我们便能呈现出和我们想法近似的效果。当然这个方法有问题,比如如果有别的东西出现在前面我们说的星星动画和摄像机之间,就会暴露出来,我尽力让这个概率降低了,但还是存在这样的问题,这是我计算不好导致的问题。相关的代码如下。
//This function is the animation for failure.
//When the game is over this function will be called and shows a star viewport for the failure reason
//It will be created outside of the viewport first and be shown when the game is over.
void scene()
{R = 10 - (10 * transitionTimer);glPushMatrix();glTranslatef(cameraPositionX + 0.0f, cameraPositionY - 0.4f, cameraPositionZ - 0.4f);glRotatef(-45.0f, 1.0f, 0.0f, 0.0f);for (int i = 0; i < 5; i++){glRotatef(72.0, 0.0, 0.0, 1.0);glBegin(GL_POLYGON);glVertex3f(0, R - sceneDisappear, 0);glVertex3f(0, 10 - sceneDisappear, 0);glVertex3f(-10, 10 - sceneDisappear, 0);glVertex3f(-10, R * cosf(72.0f * angleRadian) - sceneDisappear, 0);glVertex3f(-R * cosf(72.0f * angleRadian) / cosf(36.0f * angleRadian) * sinf(36.0f * angleRadian), R * cosf(72.0f * angleRadian) - sceneDisappear, 0);glEnd();}glPopMatrix();
}
我不知道为什么,在这里的实践中我发现OpenGL里的场景无法像我想象中的那样当场创建某一对象,所以我的想法是将这些东西提前创建好,然后拿到摄像机看不见的位置。当需要的时候,控制它们的位置,包括后面的敌人以及星星攻击我都是这么设计的。在这里我修复了上次说的建模上的计算问题,而是采用的使用OpenGL的Transformation方法从而快速创建了这里的动画。
现在我们说说后面的建模,在卡比的世界观里有一种敌人叫做瓦豆鲁迪,其实和卡比长得很像,我决定将其作为这里的敌人,因为它可以和卡比使用相同的方式做出来,而且足够可爱。在这门课的第8部分我们学习了层次建模,其实和上次的2D卡比类似,我们将这里的卡比就像层次建模里的例子一样处理,我们创建一个卡比的身体,然后根据卡比的身体对应卡比的四肢,然后我们在最后再加上乘坐的星星,我们就完成了卡比。当然瓦豆鲁迪也是这个方法。
下面的代码是卡比的相关组件。
//This function creates the body of Kirby.
//It will be called for creating Kirby.
void kirbyBody()
{glBindTexture(GL_TEXTURE_2D, texture[0]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);gluSphere(quadricObj, 0.16, 360, 360);gluDeleteQuadric(quadricObj);
}//This function creates the foot of Kirby.
//It will be called for creating Kirby.
void kirbyFoot()
{glBindTexture(GL_TEXTURE_2D, texture[1]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(0.8f, 0.5f, 1.0f);gluSphere(quadricObj, 0.12, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function creates the hand of Kirby.
//It will be called for creating Kirby.
void kirbyHand()
{glBindTexture(GL_TEXTURE_2D, texture[2]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(1.0, 0.6f, 0.6f);gluSphere(quadricObj, 0.08, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}
有的人可能对这里的贴图有疑问,因为贴图感觉是一个方形贴方形,像卡比这样的球体该怎么处理呢,但其实Lab中的例子举的两个例子中的另一个就是球体。我们可以想象这里贴图就是一块贴画,然后我们沿着球体将这个贴画正好贴下去,想清楚这一点很关键,我将利用这一点制作后面立体的五角星。你可以自己多尝试几次。
相应的瓦豆鲁迪的代码如下,两者代码的区别在于使用了不同的贴图。
//This function creates the body of Waddle.
//It will be called for creating Waddle.
void waddleBody()
{glBindTexture(GL_TEXTURE_2D, texture[3]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);gluSphere(quadricObj, 0.16, 360, 360);gluDeleteQuadric(quadricObj);
}//This function creates the foot of Waddle.
//It will be called for creating Waddle.
void waddleFoot()
{glBindTexture(GL_TEXTURE_2D, texture[4]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(0.8f, 0.5f, 1.0f);gluSphere(quadricObj, 0.12, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function creates the hand of Waddle.
//It will be called for creating Waddle.
void waddleHand()
{glBindTexture(GL_TEXTURE_2D, texture[5]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(1.0, 0.6f, 0.6f);gluSphere(quadricObj, 0.08, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}
这里不像前面2D作业先有一些基础图元的方法然后再是一些组件,然后是最后具体的对象。因为这里后面的建模很简单,所以我没有采用上次的三段式结构,当然如果你的设计比较复杂,觉得复用是必要的可以像我前一次作业一样作为三段结构呈现。
4. 角色构建
前面准备阶段我们已经将卡比和瓦豆鲁迪的组件已经准备好了,这时候我们将这些组件组装起来。
卡比的代码如下。
//This function uses hierarchical modeling to creat Kirby with the previous functions.
void kirby() {glPushMatrix();glTranslatef(kirbyPosition[0], kirbyPosition[1], kirbyPosition[2]);glRotatef(-kirbyAngle, 0.0, 1.0, 0.0);glRotatef(angleMoving, 1.0, 0.0, 0.0);glPushMatrix();glRotatef(-90.0, 1.0, 0.0, 0.0);kirbyBody();glPopMatrix();glPushMatrix();glTranslatef(0.16, 0.0, 0.0);kirbyHand();glTranslatef(-0.06, -0.16, 0.0);kirbyFoot();glPopMatrix();glPushMatrix();glTranslatef(-0.16, 0.0, 0.0);kirbyHand();glTranslatef(0.06, -0.16, 0.0);kirbyFoot();glPopMatrix();glPushMatrix();glTranslatef(0.0, -0.28, -0.06);star(0.36f, 0.1f);glPopMatrix();glPopMatrix();
}
卡比的特点是首先它本来出现在虚拟世界的中心,也是我们的画面中心。然后玩家操作后,这个位置的值是变化的,我使用了一个kirbyPosition的数组去储存这里的xyz的值。当然这里的y值其实不会变化,这里我是为了方便起见直接创立了一个大小为3的数组储存xyz的值。
而且卡比只有一个,对于瓦豆来说,你需要的是一个能生成多个瓦豆的方法,所以这个方法需要额外的参数记录瓦豆的位置,当然位置还不够。首先瓦豆的目标是拿到礼物,所以它会向礼物方向前进,因此它会直接面向礼物,所以我们需要旋转瓦豆从而让瓦豆面向礼物前进。因此这个方法还需要旋转角度的参数。当然这个角度是靠瓦豆的位置以及礼物的位置确定的,但是这个方法涉及到瓦豆的位置移动,所以如果这个角度不是作为参数传入,而是一直计算这就浪费了资源。我们完全可以在生成瓦豆的初始位置时,将这个角度值直接计算好然后作为定值传入。知道这个角度后,其实也可以使用Transformation直接将其在自己的坐标系中不断向前,使用glTranslatef即可。但我这里使用的是将具体的坐标位置利用angle计算出来。
具体的代码如下。
//This function uses hierarchical modeling to creat Waddle with the previous functions.
//Unlike the previous method for creating Kirby, it rotates for showing Waddle is moving.
//"x","y","z" defines the position of Waddle.
//"angle" keeps Waddle can face to the center.
void waddle(float x, float y, float z, float angle) {glPushMatrix();glTranslated(x, y, z);glRotatef(-angle, 0, 1, 0);glRotatef(20.0, 1.0, 0.0, 0.0);glPushMatrix();glRotatef(-90.0, 1.0, 0.0, 0.0);waddleBody();glPopMatrix();glPushMatrix();glTranslatef(0.16, 0.0, 0.0);waddleHand();glTranslatef(-0.06, -0.16, 0.0);waddleFoot();glPopMatrix();glPushMatrix();glTranslatef(-0.16, 0.0, 0.0);waddleHand();glTranslatef(0.06, -0.16, 0.0);waddleFoot();glPopMatrix();glPushMatrix();glTranslatef(0.0, -0.28, -0.06);star(0.36f, 0.1f);glPopMatrix();glPopMatrix();
}
就像前文所说,我发现无法现场创建和删除对象,我想的方案是将对象的位置进行瞬移。所以刚开始这些瓦豆也被拿到了摄像机无法看见的位置,然后需要的时候立马出现在场景中,不需要的时候立马将其移到场景之外。
因此关于卡比攻击所使用的星星也是这么处理的。但星星如果我们在建模时直接使用一个黄色的贴图去贴我认为会让五角星不够立体,所以我在贴图上使用的是提前将立体效果放进去的贴图。然后我用这样特殊的贴图去实现五角星的立体感觉。
贴图如下。
五角星是由五个角构成的,这个角可以拆分成两个三棱锥,其中两个面重合在一起,靠里的两个面不用展示,所以是四个三角形组成。三角形不是前面贴图中的那些矩形或者前面说的球形,但是其实和矩形是类似的,我们可以理解四边形是三个定点还有一个动点组成的,这三个定点正是三角形,这个四边形是由正常矩形拉伸得来的,所以我们将这里进行一个逆向,我们就可以得出需要的立体贴图是什么样的。这里需要你对贴图的原理有一定的了解,即它是坐标的对应,然后你可以想象一下这里转换的方程是什么样的,然后尝试自己尝试一下,具体的细节也可以参考我后面的资源分享。
星星的代码具体如下。
//This function creates a star.
//It will be called for Kirby, Waddle and the star bullet.
//"R" is the radius of the star,"height: is the thickness of the star.
void star(float R, float height)
{glBindTexture(GL_TEXTURE_2D, texture[6]);float r = 1.8 * R / (sinf(36.0f * angleRadian) * cosf(18.0f * angleRadian) / sinf(18.0f * angleRadian) + cosf(36.0f * angleRadian));glPushMatrix();for (int i = 0; i < 5; i++){glRotatef(72.0, 0.0, 1.0, 0.0);glBegin(GL_TRIANGLES);glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(-r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, -height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, -height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(-r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glEnd();}glPopMatrix();
}
然后这个星星被用于作为卡比和瓦豆脚下移动的工具,这个星星通过Transformation的旋转和缩放还可以作为卡比攻击时发射的星星使用。
代码如下所示。
//This function creates a star bullet.
void starBullet()
{glPushMatrix();glTranslatef(starBulletPosition[0], starBulletPosition[1], starBulletPosition[2]);glRotatef(-starBulletAngle, 0.0, 1.0, 0.0);glRotatef(-90.0, 1.0, 0, 0);star(0.18f, 0.05f);glPopMatrix();
}
当然这里星星子弹也是和前面处理方式一样,先放到场景外,需要的时候再到场景中出现。
无论是卡比被怪物攻击,还是怪物成功被攻击,这里最好有一个受到攻击的提示动画,我这里主要是需要想我这里应该是什么样的场景比较好呢。其实如果是在野外草地上的话,在传统的场景建造中,如果反复使用相同的资源就会让用户觉得这里的场景是假的,因此我想避免这样的情况,再加上我需要找一个符合卡比世界的周围的环境作为skybox(天空盒),这个四周的面最好自然有山之类的作为阻挡。根据这个我在卡比的游戏中截取了画面作为这个天空盒的一部分,这些场景中满足我刚刚的需求,然后由于这里的图案中有山有水,如下图所示。
所以我想着要不将这一切都放在海上,放在海上然后我随机加一些海上的东西,这样就避免了使用草地作为地面的重复感,而且可以让用户觉得因为是海上,所以卡比和瓦豆都需要在星星上移动。因此这里受到攻击的动画可以做一种类似于机毁人亡的感觉,我想到一些卡丁车游戏里道具进攻别人,会把别人弄得在原地晕头转向,无法操作一段时间。于是我决定当任何角色被攻击,角色就会原地旋转,无法控制。如果是卡比,那如果卡比血量还有,那么还可以继续游戏,如果卡比血量没有了,那游戏结束。对于瓦豆,如果被攻击那就是在动画结束后,自动消失,代表死亡。因为前面说的海上星星驾驶的设定,因此如果怪物碰到了卡比,双方可以理解为撞车在一起,双双原地眩晕转圈。
相关的代码其实很简单,就是利用OpenGL的Transformation让对象原地旋转即可。
代码如下所示。
void kirbyHurt()
{kirbyAngle += 5.0f;kirbyAnimationTimer += 0.2f;
}
问题的关键在于控制,这里需要一个flag控制卡比是否在眩晕的状态,如果在那么就运行上面的方法,同时这个值的变化导致后面交互部分里用户无法再控制卡比。
完成这一步后,我们回到瓦豆上,瓦豆就需要复杂一些,而且我后面的菜单功能加了额外功能,这个功能便是很多游戏里出现的“秘籍”,即可以直接操控游戏里的一些东西,其中我想了一些方法,简单的比如控制卡比血量,但是复杂的就有全屏秒杀之类的,对瓦豆的攻击方法。
前面我们有一个flag控制卡比是否在眩晕的状态,这里只需要一个布尔值就能控制,但是瓦豆的话我认为有三个状态——1. 死亡状态,该状态下瓦豆其实在场景之外。 2. 生存状态,该状态下,瓦豆正努力靠近礼物。 3. 被攻击了,正在动画中,可以理解成正在死亡的状态。因此我这里使用了一个int值来控制这三个状态。然后进行一系列的控制。比如检查所有的瓦豆中,哪些是生存状态,若不足上限,便让一个死亡状态的瓦豆转化为生存状态,即创造一个瓦豆敌人出来。再比如检查所有的生存下的瓦豆,将这些瓦豆全部进入到第三个状态,播放对应的动画,从而将这些瓦豆全部清楚。
关于具体的实现细节包括判定攻击的距离计算都可以看我后面第八节(完整代码)中的细节。
5. 场景构建
前面的几个部分其实已经将这里的场景构建说的差不多了。我们先做出这里的skybox(天空盒),代码如下。
//This function is used for making a skyBox for this game.
void skyBox()
{//WaterglBindTexture(GL_TEXTURE_2D, texture[7]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -0.9, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, -0.9, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, -0.9, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -0.9, 10.0);glEnd();//LeftSkyglBindTexture(GL_TEXTURE_2D, texture[8]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -2, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(-10.0, -2.0, -10.0);glEnd();//FrontSkyglBindTexture(GL_TEXTURE_2D, texture[9]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -2.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -2.0, -10.0);glEnd();//RightSkyglBindTexture(GL_TEXTURE_2D, texture[10]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(10.0, -2.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(10.0, 18.0, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -2.0, 10.0);glEnd();//BackSkyglBindTexture(GL_TEXTURE_2D, texture[11]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(10.0, -2.0, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(-10.0, -2.0, 10.0);glEnd();//TopSkyglBindTexture(GL_TEXTURE_2D, texture[12]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, 18.0, -10.0);glEnd();
}
你可以根据这里的注释将你准备的贴图进行替换。
回到刚刚说的这里的地面,或者说海面,我为了避免使用相同贴图的重复,我决定使用一张贴图作为海面,而这张贴图我随机添加了一些荷叶在上面,从而避免重复。如下图所示。
就这样我们完成了天空盒的创建。但是这样的场景还是有一些单调,我又想到了既然卡比是守护礼物,那可能周围也有一些其他的方块的箱子,但是卡比的这个不一样,所以瓦豆看到才会过来哄抢。这样我们建模简单,又可以添加一些不一样的东西在场景中。于是我又随机添加了一些箱子在场景中,为了体现这个地方是海面,所以有的方块高有的方块低,甚至我这里的高低会变化,但是我这里的变化值很小,非常不明显,这一点可以再进行一些改进。
相关的代码如下。
//It creats the box for decorating the game scene.
// "x","y","z" defines the postion of the box.
// "size" difines the half length of the box.
//It will be called in "boxScene".
void box(float x, float y, float z, float size)
{glBindTexture(GL_TEXTURE_2D, texture[13]);glBegin(GL_QUADS);//frontglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z + size);//backglTexCoord2f(0.0, 0.0); glVertex3f(x + size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x - size, y - size, z - size);//leftglTexCoord2f(0.0, 0.0); glVertex3f(x + size, y - size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x + size, y + size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z - size);//rightglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x - size, y - size, z + size);//topglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y + size, z + size);//bottomglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y - size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y - size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z - size);glEnd();
}//It creates some box with "box" functions to decorate the game scene.
//"y" will combines with the global variable "angleWave" to control the y value for the box to make the box up and down like function of the waves.
void boxScene(float y)
{box(3.0, 0.1 * cos(y) - 1.0, 7.0, 0.15);box(2.0, 0.2 * cos(y) - 1.0, 3.0, 0.15);box(-5.0, 0.2 * cos(y) - 1.0, -6.5, 0.15);box(-8.0, 0.1 * cos(y) - 1.0, 1.0, 0.15);glRotatef(30.0, 0.0, 1.0, 0.0);box(-1.0, 0.1 * cos(y) - 1.0, 7.0, 0.15);box(7.0, 0.2 * cos(y) - 1.0, -1.0, 0.15);box(4.0, 0.1 * cos(y) - 1.0, 6.0, 0.15);box(6.0, 0.2 * cos(y) - 1.0, -3.2, 0.15);glRotatef(15.0, 0.0, 1.0, 0.0);box(-1.4, 0.15 * cos(y) - 1.0, 2.0, 0.15);box(8.8, 0.25 * cos(y) - 1.0, -0.7, 0.15);box(0.9, 0.1 * cos(y) - 1.0, -6.0, 0.15);box(1.8, 0.2 * cos(y) - 1.0, -3.2, 0.15);box(-3.0, 0.15 * cos(y) - 1.0, 5.0, 0.15);box(-4.0, 0.25 * cos(y) - 1.0, 3.0, 0.15);box(-5.0, 0.1 * cos(y) - 1.0, 4.1, 0.15);box(5.5, 0.2 * cos(y) - 1.0, 2.9, 0.15);box(-2.7, 0.1 * cos(y) - 1.0, 1.1, 0.15);box(-1.8, 0.2 * cos(y) - 1.0, -3.4, 0.15);glRotatef(-45.0, 0.0, 1.0, 0.0);
}
由于这些箱子是一样的,所以我先写了一个创建箱子的方法,然后再随机添加一些坐标作为其出现的位置,并且方便后续模拟海浪的效果从而箱子上上下下移动。
同理我们很容易得到卡比要守护的礼物的方法,如下所示。
//It creates the defence target, which is the gift for friend's birthday.
//"y" controls its position.
//"size" difines the half length of the box.
void specialBox(float y, float size)
{glBindTexture(GL_TEXTURE_2D, texture[14]);glBegin(GL_QUADS);//frontglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 + size);//backglTexCoord2f(0.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);//leftglTexCoord2f(0.0, 0.0); glVertex3f(0 + size, y - size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 + size, y + size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);//rightglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 - size, y - size, 0 + size);//topglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y + size, 0 + size);//bottomglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y - size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y - size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);glEnd();
}
我们就基本完成了整个程序。
6. 交互部分
交互部分,首先除了基础的操作以外,一些设计也提高了这里的交互感,比如卡比和瓦豆移动的时候的倾斜以及受到攻击时的动画这些都是设计细节上的交互感。这里着重说一些交互方法上的交互,不仅有基础的操作,还有交互菜单。
6.1 键盘交互
键盘交互除了基础的退出程序以外,还有如何操控卡比以及摄像机的移动,或者这里你可以根据自己的需求进行设计。我想做成wasd移动,方向键上下左右控制摄像机移动,e键作为经常使用到的交互键,这里是用来发射子弹。但是这里需要注意,摄像机这里应该是在空间中移动,或者理解成摄像机有自己的轨道以及镜头,轨道决定其的一个位置,它可能是沿着轨道绕着某个物体旋转(即我们不能让摄像机一直向上,从而让这个世界颠倒过来了)。镜头可以理解成广角我们就能看得很远,也可以是摄像头被拉远了。因此我们在设计时候要有6个关于摄像机的控制,并且对于前面我说的轨道的控制需要规定其移动的范围。其实卡比也要规定其移动的范围,否则玩家就会控制其离开这里创造的skybox(天空盒)中,也就是需要设计一个空气墙。
相关的代码如下。
//This function controls the interaction with special keys in the keyboard.
void specialKeys(int key, int x, int y)
{//The up arrow key can move the camera upwards.if (key == GLUT_KEY_UP && gaming == true && cameraVerticalAngle <= 89.0f * angleRadian){cameraVerticalAngle += 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The down arrow key can move the camera downwardsif (key == GLUT_KEY_DOWN && gaming == true && cameraVerticalAngle >= -20.0f * angleRadian){cameraVerticalAngle -= 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The left arrow key can make the camera turn leftif (key == GLUT_KEY_LEFT && gaming == true){cameraHorizontalAngle -= 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The right arrow key can make the camera turn rightif (key == GLUT_KEY_RIGHT && gaming == true){cameraHorizontalAngle += 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}glutPostRedisplay();
}//This function controls the interaction with normal keys in the keyboard.
void keyboardInput(unsigned char key, int x, int y)
{//Press 'W' or 'S' will make Kirby forward or backward.//Kirby will tilt towards the direction of motion to make it more like it is in motion.//Press 'A' or 'D' will make Kirby turn left or right.//Press 'E' or 'Space' to shoot the star bullet.//Press 'Esc' to quit the game.angleMoving = 0.0f;if (((key == 'w' || key == 'W') && kirbyNormal == true) && gaming == true){angleMoving = 20.0f;if ((std::abs(kirbyPosition[2] + 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5) && (std::abs(kirbyPosition[0] + 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5)){kirbyPosition[2] += 0.05 * cos(-kirbyAngle * angleRadian);kirbyPosition[0] += 0.05 * sin(-kirbyAngle * angleRadian);}}if (((key == 's' || key == 'S') && kirbyNormal == true) && gaming == true){angleMoving = -20.0f;if ((std::abs(kirbyPosition[2] - 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5) && (std::abs(kirbyPosition[0] - 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5)){kirbyPosition[2] -= 0.05 * cos(-kirbyAngle * angleRadian);kirbyPosition[0] -= 0.05 * sin(-kirbyAngle * angleRadian);}}if (((key == 'a' || key == 'A') && kirbyNormal == true) && gaming == true){kirbyAngle -= 1;}if (((key == 'd' || key == 'D') && kirbyNormal == true) && gaming == true){kirbyAngle += 1;}if ((((key == 'e' || key == 'E' || key == ' ') && starBulletMoving != true) && kirbyNormal == true) && gaming == true){starAttack();}if (key == 27){exit(0);}glutPostRedisplay();}
我这里将退出程序做成按esc键而非Q键,在specialKeys方法中我规定了摄像机移动的轨道,其的镜头我放在了鼠标交互中。
6.2 鼠标交互
前面说到用鼠标控制摄像机的镜头,我觉得这样比较自然,当我们鼠标滚轮向上滚动,我们一般都是放大,因此这里摄像机也会靠近,即放大了对象。同理,当我们鼠标滚轮向下滚动,我们就是缩小,摄像机远离对象,即缩小了对象。相关的代码如下。
//This function controls the interation with the mouse.
void mouse(int button, int state, int x, int y)
{//Sliding the roller upwards will enlarge.if (button == 3 && gaming == true && cameraDistance > 0.4f){cameraDistance -= 0.1f;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//Sliding the roller downwards will shrink.else if (button == 4 && gaming == true && cameraDistance < 2.0f){cameraDistance += 0.1f;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}glutPostRedisplay();
}
这里只有鼠标的滚轮操作,因为右击呼出菜单时在创建菜单时,将创建的菜单附加到鼠标右键点击事件上。
6.3 鼠标点击出多级菜单进行交互
这里的代码使用其实不难,Lab中的代码拿过来进行一定量的修改即可,我将其做成多个二级菜单,其中第一个二级菜单提供类似于软件的帮助支持,告诉用户该如何操作。第二个二级菜单即我前面说的“秘籍”,这个菜单下面还有子菜单从而提供相应的支持。相关的代码如下。
//Following two functions provides the menu for the user.
//In the menu the user can get the help for how to play the game.
//In the menu the user can cheat with the cheat operations submenu for the cheating modes.
//In the menu the user can quit the game directly.
//This function will react the operation with the corresponding "num" value.
void menu(int num) {switch (num) {case 1:exit(0);break;case 2:killCheat();break;case 3:killAllCheat();break;case 4:if (kirbyLife < 3){lifeCheat(3 - kirbyLife);}break;case 5:lifeCheat(10);break;case 6:lifeCheat(100);break;case 7:lifeCheat(999);break;case 8:break;case 9:waddleMovingSpeedUp();break;case 10:waddleMovingSpeedDown();break;}
}//This function initials and creates the menus.
void initMenu() {int help = glutCreateMenu(menu);glutAddMenuEntry("w: Kirby go staight", 8);glutAddMenuEntry("s: Kirby go back", 8);glutAddMenuEntry("a: Kirby turn left", 8);glutAddMenuEntry("d: kirby turn right", 8);glutAddMenuEntry("Using direction keys controls the camera", 8);glutAddMenuEntry("e / space: Launch stars", 8);glutAddMenuEntry("esc: Quit the game", 8);int difficultyCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Difficulty up", 9);glutAddMenuEntry("Difficulty down", 10);int killCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Kill one", 2);glutAddMenuEntry("Kill all", 3);int lifeCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Life max", 4);glutAddMenuEntry("Life +10", 5);glutAddMenuEntry("Life +100", 6);glutAddMenuEntry("Life +999", 7);int cheatOptions = glutCreateMenu(menu);glutAddSubMenu("Kill cheat options:", killCheatOptions);glutAddSubMenu("Life cheat options:", lifeCheatOptions);glutAddSubMenu("Difficulty cheat options:", difficultyCheatOptions);int menuID = glutCreateMenu(menu);glutAddSubMenu("Operation instructions:", help);glutAddSubMenu("Cheat options:", cheatOptions);glutAddMenuEntry("Quit the game(Esc)", 1);glutAttachMenu(GLUT_RIGHT_BUTTON);
}
这里的initMenu方法下的glutAttachMenu(GLUT_RIGHT_BUTTON)将创建的菜单附加到鼠标右键点击事件上。所以不需要在mouse的方法上添加额外的操作。这里initMenu相当于创建对应的菜单和对应的事件编号,然后根据事件编号,前面的menu方法会将其跳转到对应的操作下。这部分其实不难。
7. 缺点与问题
7.1 程序bug
程序主要有以下两个bug——1. 星星结算动画可能会导致穿模,这个地方我前文说过是因为我的计算问题,其实如果设计的没问题,可以解决。
2.限制卡比移动时的方法有问题,以至于如果卡比装上空气墙。卡比可能无法移动。这里代码处理的不好,其实可以变成如果值增加就会超出边界,则这个值不变。
7.2 游戏乐趣不足
首先是这个背景故事量其实存在一定的联想量,而实际上游戏给的提示不够。而这里游戏的难度大,很容易无法给玩家带来乐趣,这会导致玩家不会有足够的耐心去思考游戏里的故事,有精力也更多的放在了游戏上,而非故事背景上,所以可能感受到的乐趣会进一步下降。即游戏带来的挫败感让游戏的乐趣下降了。这主要是我时间不足的原因,希望大家能花更多时间去制作自己的作业。
7.3 画面不够好看
相较于上次的2D作业,很明显这次的作业我觉得自己的画面不如上次那样游戏,我这里比如周围的场景可以再改进。以及这些贴图和建模也可以再精致一些,但由于时间问题,我都没有来得及去进一步修饰。
8. 完整代码
前面很多地方只是展示了最相关的代码,以及部分示例代码,没有展示这些代码中调用的一些方法,以及其他的代码。这里展示全部代码,希望能帮助到大家。当然如果想要直接获取这个程序的相关所有贴图以及程序,可以查看我的GitHub项目GitHub仓库。
//This coursework makes a small game about Kirby.
//The story of this game can be viewed as a prequel to coursework1, Kirby encountered an accident on the way to the birthday party.
//In this game the user should control Kirby to denfence the box at the center which is the birthday gift for Kirby's friend.
//This game is a 3D game and it provides many operations for the users, which includes controlling Kirby and the camera, and some additional functions.
//Such as exit the game and cheating mode menu.
//When Kirby is out of health value or Waddle(the monster) get to the center, this game will be immovable and focus on the perspective of failure.
#define FREEGLUT_STATIC
#include <GL/freeglut.h>
#include <math.h>
#include <cmath>
#include <iostream>
#include <random>
#include <chrono>
#include <stdio.h>
#include <stdlib.h>
#include "vector" using namespace std;//There are some constants pre defined in the project for future use.
const float angleRadian = 3.1415926 / 180.0f;
const int textureNumber = 16;//There are some variables for controlling the whole game.
//Some variables control the camera, some variables control Kirby, some variables control Waddles, some variables control the failure animation and the scene.
float cameraDistance = 0.8f;
float cameraHorizontalAngle = 0.0f;
float cameraVerticalAngle = 0.0f;
float lookatPositionX = 0.0f, lookatPositionY = 0.0f, lookatPositionZ = 0.0f;
float cameraPositionX = lookatPositionX + cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = lookatPositionY + cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = lookatPositionZ + cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);
float kirbyPosition[3] = { 0.0f, 0.0f, 0.0f };
float kirbyAngle = 0.0f;
int kirbyLife = 3;
float kirbyAnimationTimer = 0.0f;
bool kirbyNormal = true;
float starBulletPosition[3] = { 10.0f, -10.0f, 0.0f };
float starBulletAngle = 0.0f;
bool starBulletMoving = false;
bool moving = false;
int waddleNumber = 0;
float waddlePosition[4][3] = {{0.0f, -10.0f, 0.0f},{0.0f, -10.0f, 0.0f},{0.0f, -10.0f, 0.0f},{0.0f, -10.0f, 0.0f},
};
float waddleAngle[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
int waddleState[4] = { 0, 0, 0, 0 };
float waddleAnimationTimer[4] = { 0.0f, 0.0f , 0.0f, 0.0f };
float waddleMovingSpeed = 4.0f;
float timer = 0.0f;
bool gaming = true;
float transitionTimer = 0.0f;
float R = 10 - (10 * transitionTimer);
float sceneDisappear = 1000.0f;
float angleMoving = 0.0f;
float angleWave = 0.0f;//These are used for textures and lighting.
GLint imageWidth[textureNumber];
GLint imageHeight[textureNumber];
GLint pixelLength[textureNumber];
vector<GLubyte*>p;
GLuint texture[textureNumber];
GLfloat light_x = 0.0, light_y = 2.0, light_z = 0.0;//This function is reading the image for the texture.
void ReadImage(const char path[256], GLint& imagewidth, GLint& imageheight, GLint& pixellength)
{GLubyte* pixeldata;FILE* pfile;fopen_s(&pfile, path, "rb");if (pfile == 0) exit(0);fseek(pfile, 0x0012, SEEK_SET);fread(&imagewidth, sizeof(imagewidth), 1, pfile);fread(&imageheight, sizeof(imageheight), 1, pfile);pixellength = imagewidth * 3;while (pixellength % 4 != 0) pixellength++;pixellength *= imageheight;pixeldata = (GLubyte*)malloc(pixellength);if (pixeldata == 0) exit(0);fseek(pfile, 54, SEEK_SET);fread(pixeldata, pixellength, 1, pfile);p.push_back(pixeldata);fclose(pfile);
}//This function is used for some initializations. Such as enabling depth testing, texture and lighting.
//This function call "ReadImage" for the textures and set the texture objects.
//It also set the light.
void myinit(void) {glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LEQUAL);glShadeModel(GL_SMOOTH);glEnable(GL_TEXTURE_2D);ReadImage("kirbyFace.bmp", imageWidth[0], imageHeight[0], pixelLength[0]);ReadImage("kirbyFoot.bmp", imageWidth[1], imageHeight[1], pixelLength[1]);ReadImage("kirbyHand.bmp", imageWidth[2], imageHeight[2], pixelLength[2]);ReadImage("waddleFace.bmp", imageWidth[3], imageHeight[3], pixelLength[3]);ReadImage("waddleFoot.bmp", imageWidth[4], imageHeight[4], pixelLength[4]);ReadImage("waddleHand.bmp", imageWidth[5], imageHeight[5], pixelLength[5]);ReadImage("star.bmp", imageWidth[6], imageHeight[6], pixelLength[6]);ReadImage("water.bmp", imageWidth[7], imageHeight[7], pixelLength[7]);ReadImage("leftSky.bmp", imageWidth[8], imageHeight[8], pixelLength[8]);ReadImage("frontSky.bmp", imageWidth[9], imageHeight[9], pixelLength[9]);ReadImage("rightSky.bmp", imageWidth[10], imageHeight[10], pixelLength[10]);ReadImage("backSky.bmp", imageWidth[11], imageHeight[11], pixelLength[11]);ReadImage("topSky.bmp", imageWidth[12], imageHeight[12], pixelLength[12]);ReadImage("box.bmp", imageWidth[13], imageHeight[13], pixelLength[13]);ReadImage("specialBox.bmp", imageWidth[14], imageHeight[14], pixelLength[14]);glPixelStorei(GL_UNPACK_ALIGNMENT, 1);glGenTextures(textureNumber, &texture[0]);for (int i = 0; i < 15; i++){glBindTexture(GL_TEXTURE_2D, texture[i]);glTexImage2D(GL_TEXTURE_2D, 0, 3, imageWidth[i], imageHeight[i], 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, p[i]);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);}glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);GLfloat mat_shininess[] = { 50.0 };glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);GLfloat light_position[] = { light_x, light_y, light_z, 0.0 };glLightfv(GL_LIGHT0, GL_POSITION, light_position);glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);
}//This function is used for making a skyBox for this game.
void skyBox()
{//WaterglBindTexture(GL_TEXTURE_2D, texture[7]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -0.9, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, -0.9, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, -0.9, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -0.9, 10.0);glEnd();//LeftSkyglBindTexture(GL_TEXTURE_2D, texture[8]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -2, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(-10.0, -2.0, -10.0);glEnd();//FrontSkyglBindTexture(GL_TEXTURE_2D, texture[9]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, -2.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, -10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -2.0, -10.0);glEnd();//RightSkyglBindTexture(GL_TEXTURE_2D, texture[10]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(10.0, -2.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(10.0, 18.0, -10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, -2.0, 10.0);glEnd();//BackSkyglBindTexture(GL_TEXTURE_2D, texture[11]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(10.0, -2.0, 10.0);glTexCoord2f(0.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(-10.0, -2.0, 10.0);glEnd();//TopSkyglBindTexture(GL_TEXTURE_2D, texture[12]);glBegin(GL_QUADS);glTexCoord2f(0.0, 0.0); glVertex3f(-10.0, 18.0, -10.0);glTexCoord2f(0.0, 1.0); glVertex3f(-10.0, 18.0, 10.0);glTexCoord2f(1.0, 1.0); glVertex3f(10.0, 18.0, 10.0);glTexCoord2f(1.0, 0.0); glVertex3f(10.0, 18.0, -10.0);glEnd();
}//This function is the animation for failure.
//When the game is over this function will be called and shows a star viewport for the failure reason
//It will be created outside of the viewport first and be shown when the game is over.
void scene()
{R = 10 - (10 * transitionTimer);glPushMatrix();glTranslatef(cameraPositionX + 0.0f, cameraPositionY - 0.4f, cameraPositionZ - 0.4f);glRotatef(-45.0f, 1.0f, 0.0f, 0.0f);for (int i = 0; i < 5; i++){glRotatef(72.0, 0.0, 0.0, 1.0);glBegin(GL_POLYGON);glVertex3f(0, R - sceneDisappear, 0);glVertex3f(0, 10 - sceneDisappear, 0);glVertex3f(-10, 10 - sceneDisappear, 0);glVertex3f(-10, R * cosf(72.0f * angleRadian) - sceneDisappear, 0);glVertex3f(-R * cosf(72.0f * angleRadian) / cosf(36.0f * angleRadian) * sinf(36.0f * angleRadian), R * cosf(72.0f * angleRadian) - sceneDisappear, 0);glEnd();}glPopMatrix();
}//The game will end in two ways: one is that Kirby has no haleth value, and the other is that Waddle has reached the center.
//This function is used for the first way.
//It will change the viewport to Kirby.
void kirbyDieEnd()
{cameraPositionX = kirbyPosition[0];cameraPositionY = kirbyPosition[1] + 1.0f;cameraPositionZ = kirbyPosition[2] + 1.0f;lookatPositionX = kirbyPosition[0];lookatPositionY = kirbyPosition[1];lookatPositionZ = kirbyPosition[2];
}//This function is used for the second way.
//It will change the viewport to the Waddle winner(similiar to Plants V.S. Zombies).
void waddleWinEnd()
{cameraPositionX = 0;cameraPositionY = 1.0f;cameraPositionZ = 1.0f;lookatPositionX = 0.0f;lookatPositionY = 0.0f;lookatPositionZ = 0.0f;
}//This function is used for game over.
//It will stop all the moving and focus on the the perspective of failure.
//It will call the corresponding ending method based on the reason for the end of the game.
void gameOver()
{gaming = false;sceneDisappear = 0.0f;waddleNumber += 4;for (int i = 0; i < 4; i++){waddleState[i] = 3;}if (kirbyLife <= 0){kirbyDieEnd();}else{waddleWinEnd();}
}//This function creates a star.
//It will be called for Kirby, Waddle and the star bullet.
//"R" is the radius of the star,"height: is the thickness of the star.
void star(float R, float height)
{glBindTexture(GL_TEXTURE_2D, texture[6]);float r = 1.8 * R / (sinf(36.0f * angleRadian) * cosf(18.0f * angleRadian) / sinf(18.0f * angleRadian) + cosf(36.0f * angleRadian));glPushMatrix();for (int i = 0; i < 5; i++){glRotatef(72.0, 0.0, 1.0, 0.0);glBegin(GL_TRIANGLES);glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(-r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, -height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, -height, 0.0f);glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f, 0.0f, R);glTexCoord2f(1.0f, 1.0f); glVertex3f(-r * sin(36.0f * angleRadian), 0.0f, r * cos(36.0f * angleRadian));glEnd();}glPopMatrix();
}//This function creates the body of Kirby.
//It will be called for creating Kirby.
void kirbyBody()
{glBindTexture(GL_TEXTURE_2D, texture[0]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);gluSphere(quadricObj, 0.16, 360, 360);gluDeleteQuadric(quadricObj);
}//This function creates the foot of Kirby.
//It will be called for creating Kirby.
void kirbyFoot()
{glBindTexture(GL_TEXTURE_2D, texture[1]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(0.8f, 0.5f, 1.0f);gluSphere(quadricObj, 0.12, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function creates the hand of Kirby.
//It will be called for creating Kirby.
void kirbyHand()
{glBindTexture(GL_TEXTURE_2D, texture[2]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(1.0, 0.6f, 0.6f);gluSphere(quadricObj, 0.08, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function uses hierarchical modeling to creat Kirby with the previous functions.
void kirby() {glPushMatrix();glTranslatef(kirbyPosition[0], kirbyPosition[1], kirbyPosition[2]);glRotatef(-kirbyAngle, 0.0, 1.0, 0.0);glRotatef(angleMoving, 1.0, 0.0, 0.0);glPushMatrix();glRotatef(-90.0, 1.0, 0.0, 0.0);kirbyBody();glPopMatrix();glPushMatrix();glTranslatef(0.16, 0.0, 0.0);kirbyHand();glTranslatef(-0.06, -0.16, 0.0);kirbyFoot();glPopMatrix();glPushMatrix();glTranslatef(-0.16, 0.0, 0.0);kirbyHand();glTranslatef(0.06, -0.16, 0.0);kirbyFoot();glPopMatrix();glPushMatrix();glTranslatef(0.0, -0.28, -0.06);star(0.36f, 0.1f);glPopMatrix();glPopMatrix();
}//This function is used for the animation of kirby hurt.
//When Kirby collides with Waddle, Kirby will bleed and be knocked unconscious for a while.
void kirbyHurt()
{kirbyAngle += 5.0f;kirbyAnimationTimer += 0.2f;
}//This function will check the state of Kirby.
//If Kirby is hurt, Kirby will be unconscious for a while.
//If Kirby is out of health value, it will call "gameOver" to end the game.
void kirbyStateCheck()
{if (kirbyAnimationTimer >= 10.0f){kirbyNormal = true;}if (kirbyLife <= 0){gameOver();}
}//This function creates the body of Waddle.
//It will be called for creating Waddle.
void waddleBody()
{glBindTexture(GL_TEXTURE_2D, texture[3]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);gluSphere(quadricObj, 0.16, 360, 360);gluDeleteQuadric(quadricObj);
}//This function creates the foot of Waddle.
//It will be called for creating Waddle.
void waddleFoot()
{glBindTexture(GL_TEXTURE_2D, texture[4]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(0.8f, 0.5f, 1.0f);gluSphere(quadricObj, 0.12, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function creates the hand of Waddle.
//It will be called for creating Waddle.
void waddleHand()
{glBindTexture(GL_TEXTURE_2D, texture[5]);GLUquadric* quadricObj = gluNewQuadric();gluQuadricTexture(quadricObj, GL_TRUE);glPushMatrix();glScalef(1.0, 0.6f, 0.6f);gluSphere(quadricObj, 0.08, 360, 360);glPopMatrix();gluDeleteQuadric(quadricObj);
}//This function uses hierarchical modeling to creat Waddle with the previous functions.
//Unlike the previous method for creating Kirby, it rotates for showing Waddle is moving.
//"x","y","z" defines the position of Waddle.
//"angle" keeps Waddle can face to the center.
void waddle(float x, float y, float z, float angle) {glPushMatrix();glTranslated(x, y, z);glRotatef(-angle, 0, 1, 0);glRotatef(20.0, 1.0, 0.0, 0.0);glPushMatrix();glRotatef(-90.0, 1.0, 0.0, 0.0);waddleBody();glPopMatrix();glPushMatrix();glTranslatef(0.16, 0.0, 0.0);waddleHand();glTranslatef(-0.06, -0.16, 0.0);waddleFoot();glPopMatrix();glPushMatrix();glTranslatef(-0.16, 0.0, 0.0);waddleHand();glTranslatef(0.06, -0.16, 0.0);waddleFoot();glPopMatrix();glPushMatrix();glTranslatef(0.0, -0.28, -0.06);star(0.36f, 0.1f);glPopMatrix();glPopMatrix();
}//All waddles will be created in advance, but unseen because of the viewport.
//This function will find the first waiting Waddle which will be shown in the scene for its mission when waddleCreater is calling this function.
//The "waddleState" records the state of all the Waddles, 0 means it is dead, 1 means it is alive, 2 means it is dying.
//So if a new Waddle shoulbd be created a dead waddle index is needed.
int findAvaliableWaddle()
{for (int i = 0; i < 4; i++){if (waddleState[i] == 0){return i;}}return -1;
}//This function will find the alive waddle and used for kill it for cheating mode.
//It will be called in the cheating mode menu.
int findLiveWaddle() {for (int i = 0; i < 4; i++){if (waddleState[i] == 1){return i;}}return -1;
}//This function is used for creating a new Waddle or make a dead Waddle alive.
//It will call "findAvaliableWaddle" for creating an alive Waddle.
//"distance" defines the distance between Waddle and the center.
void waddleCreater(float distance)
{std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> distr(-180, 180);int waddleIndex = findAvaliableWaddle();if (waddleIndex != -1){waddleAngle[waddleIndex] = distr(gen);waddlePosition[waddleIndex][1] = 0.0f;waddlePosition[waddleIndex][2] = -(distance * cos(-waddleAngle[waddleIndex] * angleRadian));waddlePosition[waddleIndex][0] = -(distance * sin(-waddleAngle[waddleIndex] * angleRadian));waddleState[waddleIndex] = 1;}
}//This function will make Waddles moving to the center.
void waddleMoving()
{for (int i = 0; i < 4; i++){if (waddleState[i] == 1){waddlePosition[i][2] += 0.005 * waddleMovingSpeed * cos(-waddleAngle[i] * angleRadian);waddlePosition[i][0] += 0.005 * waddleMovingSpeed * sin(-waddleAngle[i] * angleRadian);}}
}//This function will make Waddles moving faster.
//It will be called for the cheating mode menu.
void waddleMovingSpeedUp()
{if (waddleMovingSpeed < 20){waddleMovingSpeed += 1;}
}//This function will make Waddles moving slower.
//It will be called for the cheating mode menu.
void waddleMovingSpeedDown()
{if (waddleMovingSpeed > 1){waddleMovingSpeed -= 1;}
}//This function controls Waddle disappear animation
//It will be called when this Waddle is decided to be delete in "waddleDelete"
//"i" is the index of Waddle should be disappeared.
void waddleDisappear(int i)
{waddleAngle[i] += 5.0f;waddleAnimationTimer[i] += 0.2f;if (waddleAnimationTimer[i] >= 10.0f){waddleState[i] = 0;waddlePosition[i][0] = 0.0f;waddlePosition[i][1] = -10.0f;waddlePosition[i][2] = 0.0f;waddleAngle[i] = 0.0f;waddleNumber -= 1;}
}//This function will decide which waddle will be deleted and the reason for deleting it.
//If it is attacked by the bullet star it will disappear normally.
//If it is collision with Kirby, it will make kirby hurt with losing one health value.
//If it is arriving to the center, it will call "gameOver" to end the game.
void waddleDelete()
{for (int i = 0; i < 4; i++){float starDistanceSquare = (waddlePosition[i][0] - starBulletPosition[0]) * (waddlePosition[i][0] - starBulletPosition[0]) + (waddlePosition[i][2] - starBulletPosition[2]) * (waddlePosition[i][2] - starBulletPosition[2]);float kirbyDistanceSquare = (waddlePosition[i][0] - kirbyPosition[0]) * (waddlePosition[i][0] - kirbyPosition[0]) + (waddlePosition[i][2] - kirbyPosition[2]) * (waddlePosition[i][2] - kirbyPosition[2]);float winDistanceSquare = waddlePosition[i][0] * waddlePosition[i][0] + waddlePosition[i][2] * waddlePosition[i][2];if (winDistanceSquare <= 0.01 && waddleState[i] == 1){gameOver();}if (starDistanceSquare <= 0.08 && waddleState[i] == 1){waddleAnimationTimer[i] = 0.0f;waddleState[i] = 2;}if (kirbyDistanceSquare <= 0.08 && waddleState[i] == 1){waddleAnimationTimer[i] = 0.0f;waddleState[i] = 2;kirbyLife -= 1;kirbyAnimationTimer = 0.0f;kirbyNormal = false;}if (waddleState[i] == 2){waddleDisappear(i);}}
}//This function creates a star bullet.
void starBullet()
{glPushMatrix();glTranslatef(starBulletPosition[0], starBulletPosition[1], starBulletPosition[2]);glRotatef(-starBulletAngle, 0.0, 1.0, 0.0);glRotatef(-90.0, 1.0, 0, 0);star(0.18f, 0.05f);glPopMatrix();
}//This function will make star bullet start moving from the position of K0irby.
//It will be called with the user press "E" or "Space".
void starAttack()
{starBulletMoving = true;for (int i = 0; i < 3; i++){starBulletPosition[i] = kirbyPosition[i];}starBulletAngle = kirbyAngle;
}//This function is used for making the bullet dissapear and avoid the error of distance between the star bullet with Waddle.
void starDisappear()
{starBulletMoving = false;starBulletPosition[1] = -10.0f;
}//This function controls the moving of star bullet.
//It will call "starDisappear" for avoiding the bullet out of range.
void starMoving()
{if (gaming){starBulletPosition[2] += 0.1 * cos(-starBulletAngle * angleRadian);starBulletPosition[0] += 0.1 * sin(-starBulletAngle * angleRadian);if (std::abs(starBulletPosition[2]) > 8.5f){starDisappear();}if (std::abs(starBulletPosition[0]) > 8.5f){starDisappear();}}
}//It creats the box for decorating the game scene.
// "x","y","z" defines the postion of the box.
// "size" difines the half length of the box.
//It will be called in "boxScene".
void box(float x, float y, float z, float size)
{glBindTexture(GL_TEXTURE_2D, texture[13]);glBegin(GL_QUADS);//frontglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z + size);//backglTexCoord2f(0.0, 0.0); glVertex3f(x + size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x - size, y - size, z - size);//leftglTexCoord2f(0.0, 0.0); glVertex3f(x + size, y - size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x + size, y + size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z - size);//rightglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x - size, y - size, z + size);//topglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y + size, z + size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y + size, z - size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y + size, z - size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y + size, z + size);//bottomglTexCoord2f(0.0, 0.0); glVertex3f(x - size, y - size, z - size);glTexCoord2f(0.0, 1.0); glVertex3f(x - size, y - size, z + size);glTexCoord2f(1.0, 1.0); glVertex3f(x + size, y - size, z + size);glTexCoord2f(1.0, 0.0); glVertex3f(x + size, y - size, z - size);glEnd();
}//It creates the defence target, which is the gift for friend's birthday.
//"y" controls its position.
//"size" difines the half length of the box.
void specialBox(float y, float size)
{glBindTexture(GL_TEXTURE_2D, texture[14]);glBegin(GL_QUADS);//frontglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 + size);//backglTexCoord2f(0.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);//leftglTexCoord2f(0.0, 0.0); glVertex3f(0 + size, y - size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 + size, y + size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);//rightglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 - size, y - size, 0 + size);//topglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y + size, 0 + size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y + size, 0 - size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y + size, 0 - size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y + size, 0 + size);//bottomglTexCoord2f(0.0, 0.0); glVertex3f(0 - size, y - size, 0 - size);glTexCoord2f(0.0, 1.0); glVertex3f(0 - size, y - size, 0 + size);glTexCoord2f(1.0, 1.0); glVertex3f(0 + size, y - size, 0 + size);glTexCoord2f(1.0, 0.0); glVertex3f(0 + size, y - size, 0 - size);glEnd();
}//It creates some box with "box" functions to decorate the game scene.
//"y" will combines with the global variable "angleWave" to control the y value for the box to make the box up and down like function of the waves.
void boxScene(float y)
{box(3.0, 0.1 * cos(y) - 1.0, 7.0, 0.15);box(2.0, 0.2 * cos(y) - 1.0, 3.0, 0.15);box(-5.0, 0.2 * cos(y) - 1.0, -6.5, 0.15);box(-8.0, 0.1 * cos(y) - 1.0, 1.0, 0.15);glRotatef(30.0, 0.0, 1.0, 0.0);box(-1.0, 0.1 * cos(y) - 1.0, 7.0, 0.15);box(7.0, 0.2 * cos(y) - 1.0, -1.0, 0.15);box(4.0, 0.1 * cos(y) - 1.0, 6.0, 0.15);box(6.0, 0.2 * cos(y) - 1.0, -3.2, 0.15);glRotatef(15.0, 0.0, 1.0, 0.0);box(-1.4, 0.15 * cos(y) - 1.0, 2.0, 0.15);box(8.8, 0.25 * cos(y) - 1.0, -0.7, 0.15);box(0.9, 0.1 * cos(y) - 1.0, -6.0, 0.15);box(1.8, 0.2 * cos(y) - 1.0, -3.2, 0.15);box(-3.0, 0.15 * cos(y) - 1.0, 5.0, 0.15);box(-4.0, 0.25 * cos(y) - 1.0, 3.0, 0.15);box(-5.0, 0.1 * cos(y) - 1.0, 4.1, 0.15);box(5.5, 0.2 * cos(y) - 1.0, 2.9, 0.15);box(-2.7, 0.1 * cos(y) - 1.0, 1.1, 0.15);box(-1.8, 0.2 * cos(y) - 1.0, -3.4, 0.15);glRotatef(-45.0, 0.0, 1.0, 0.0);
}//This function will add "times" health value for Kirby.
//It will be called in the cheating mode menu.
void lifeCheat(int times)
{for (int i = 0; i < times; i++){kirbyLife += 1;}
}//This function will kill one alive Waddle.
//It calls "findLiveWaddle" for finding an alive one.
//It will be called in the cheating mode menu.
void killCheat()
{int killIndex = findLiveWaddle();if (killIndex != -1){waddleAnimationTimer[killIndex] = 0.0f;waddleState[killIndex] = 2;waddleDisappear(killIndex);}
}//This function will kill all alive Waddle.
//It calls "killCheat" four times to kill all alive Waddle.
//It will be called in the cheating mode menu.
void killAllCheat()
{for (int i = 0; i < 4; i++){killCheat();}
}//Update some value for the whole game.
//It also constantly checks some values that control the game's operation, in order to control the game to develop in the prescribed logical direction.
void update(int value)
{angleWave += 0.01 * angleRadian;kirbyStateCheck();waddleMoving();waddleDelete();if (waddleNumber < 5 && timer >= 20.0){waddleNumber += 1;waddleCreater(8.0f);timer = 0;}timer += 0.2;if (kirbyNormal != true){kirbyHurt();}if (starBulletMoving == true){starMoving();}glutPostRedisplay();glutTimerFunc(1000 / 60, update, 0);if (gaming != true) {transitionTimer += 0.01f;}if (gaming){cameraPositionX = lookatPositionX + cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = lookatPositionY + cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = lookatPositionZ + cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);lookatPositionX = kirbyPosition[0];lookatPositionY = kirbyPosition[1];lookatPositionZ = kirbyPosition[2];}
}//This function defines the content that needs to be drawn for each frame.
void display()
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);glLoadIdentity();gluLookAt(cameraPositionX, cameraPositionY, cameraPositionZ, lookatPositionX, lookatPositionY, lookatPositionZ, 0.0, 1.0, 0.0);kirby();skyBox();starBullet();waddle(waddlePosition[0][0], waddlePosition[0][1], waddlePosition[0][2], waddleAngle[0]);waddle(waddlePosition[1][0], waddlePosition[1][1], waddlePosition[1][2], waddleAngle[1]);waddle(waddlePosition[2][0], waddlePosition[2][1], waddlePosition[2][2], waddleAngle[2]);waddle(waddlePosition[3][0], waddlePosition[3][1], waddlePosition[3][2], waddleAngle[3]);scene();specialBox(-0.7, 0.15);boxScene(angleWave * angleRadian);glutSwapBuffers();
}//This function is used to update the viewport and projection.
void reshape(GLsizei w, GLsizei h)
{glViewport(0, 0, w, h);glMatrixMode(GL_PROJECTION);glLoadIdentity();gluLookAt(cameraPositionX, cameraPositionY, cameraPositionZ, lookatPositionX, lookatPositionY, lookatPositionZ, 0.0, 1.0, 0.0);gluPerspective(90.0, 4.0f / 3.0f, 0.01f, 1000.0f);glMatrixMode(GL_MODELVIEW);glLoadIdentity();}//Following two functions provides the menu for the user.
//In the menu the user can get the help for how to play the game.
//In the menu the user can cheat with the cheat operations submenu for the cheating modes.
//In the menu the user can quit the game directly.
//This function will react the operation with the corresponding "num" value.
void menu(int num) {switch (num) {case 1:exit(0);break;case 2:killCheat();break;case 3:killAllCheat();break;case 4:if (kirbyLife < 3){lifeCheat(3 - kirbyLife);}break;case 5:lifeCheat(10);break;case 6:lifeCheat(100);break;case 7:lifeCheat(999);break;case 8:break;case 9:waddleMovingSpeedUp();break;case 10:waddleMovingSpeedDown();break;}
}//This function initials and creates the menus.
void initMenu() {int help = glutCreateMenu(menu);glutAddMenuEntry("w: Kirby go staight", 8);glutAddMenuEntry("s: Kirby go back", 8);glutAddMenuEntry("a: Kirby turn left", 8);glutAddMenuEntry("d: kirby turn right", 8);glutAddMenuEntry("Using direction keys controls the camera", 8);glutAddMenuEntry("e / space: Launch stars", 8);glutAddMenuEntry("esc: Quit the game", 8);int difficultyCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Difficulty up", 9);glutAddMenuEntry("Difficulty down", 10);int killCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Kill one", 2);glutAddMenuEntry("Kill all", 3);int lifeCheatOptions = glutCreateMenu(menu);glutAddMenuEntry("Life max", 4);glutAddMenuEntry("Life +10", 5);glutAddMenuEntry("Life +100", 6);glutAddMenuEntry("Life +999", 7);int cheatOptions = glutCreateMenu(menu);glutAddSubMenu("Kill cheat options:", killCheatOptions);glutAddSubMenu("Life cheat options:", lifeCheatOptions);glutAddSubMenu("Difficulty cheat options:", difficultyCheatOptions);int menuID = glutCreateMenu(menu);glutAddSubMenu("Operation instructions:", help);glutAddSubMenu("Cheat options:", cheatOptions);glutAddMenuEntry("Quit the game(Esc)", 1);glutAttachMenu(GLUT_RIGHT_BUTTON);
}//This function controls the interaction with special keys in the keyboard.
void specialKeys(int key, int x, int y)
{//The up arrow key can move the camera upwards.if (key == GLUT_KEY_UP && gaming == true && cameraVerticalAngle <= 89.0f * angleRadian){cameraVerticalAngle += 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The down arrow key can move the camera downwardsif (key == GLUT_KEY_DOWN && gaming == true && cameraVerticalAngle >= -20.0f * angleRadian){cameraVerticalAngle -= 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The left arrow key can make the camera turn leftif (key == GLUT_KEY_LEFT && gaming == true){cameraHorizontalAngle -= 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//The right arrow key can make the camera turn rightif (key == GLUT_KEY_RIGHT && gaming == true){cameraHorizontalAngle += 1.0f * angleRadian;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}glutPostRedisplay();
}//This function controls the interaction with normal keys in the keyboard.
void keyboardInput(unsigned char key, int x, int y)
{//Press 'W' or 'S' will make Kirby forward or backward.//Kirby will tilt towards the direction of motion to make it more like it is in motion.//Press 'A' or 'D' will make Kirby turn left or right.//Press 'E' or 'Space' to shoot the star bullet.//Press 'Esc' to quit the game.angleMoving = 0.0f;if (((key == 'w' || key == 'W') && kirbyNormal == true) && gaming == true){angleMoving = 20.0f;if ((std::abs(kirbyPosition[2] + 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5) && (std::abs(kirbyPosition[0] + 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5)){kirbyPosition[2] += 0.05 * cos(-kirbyAngle * angleRadian);kirbyPosition[0] += 0.05 * sin(-kirbyAngle * angleRadian);}}if (((key == 's' || key == 'S') && kirbyNormal == true) && gaming == true){angleMoving = -20.0f;if ((std::abs(kirbyPosition[2] - 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5) && (std::abs(kirbyPosition[0] - 0.05 * cos(-kirbyAngle * angleRadian)) < 8.5)){kirbyPosition[2] -= 0.05 * cos(-kirbyAngle * angleRadian);kirbyPosition[0] -= 0.05 * sin(-kirbyAngle * angleRadian);}}if (((key == 'a' || key == 'A') && kirbyNormal == true) && gaming == true){kirbyAngle -= 1;}if (((key == 'd' || key == 'D') && kirbyNormal == true) && gaming == true){kirbyAngle += 1;}if ((((key == 'e' || key == 'E' || key == ' ') && starBulletMoving != true) && kirbyNormal == true) && gaming == true){starAttack();}if (key == 27){exit(0);}glutPostRedisplay();}//This function controls the interation with the mouse.
void mouse(int button, int state, int x, int y)
{//Sliding the roller upwards will enlarge.if (button == 3 && gaming == true && cameraDistance > 0.4f){cameraDistance -= 0.1f;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}//Sliding the roller downwards will shrink.else if (button == 4 && gaming == true && cameraDistance < 2.0f){cameraDistance += 0.1f;cameraPositionX = cameraDistance * cos(cameraVerticalAngle) * sin(cameraHorizontalAngle), cameraPositionY = cameraDistance * sin(cameraVerticalAngle), cameraPositionZ = cameraDistance * cos(cameraVerticalAngle) * cos(cameraHorizontalAngle);}glutPostRedisplay();
}//Set the windown size and some callback function
int main(int argc, char** argv)
{glutInit(&argc, argv);glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);glutInitWindowPosition(100, 100);glutInitWindowSize(800, 600);glutCreateWindow("Kirby Defends Gift");glutDisplayFunc(display);glutReshapeFunc(reshape);glutSpecialFunc(specialKeys);glutMouseFunc(mouse);glutKeyboardFunc(keyboardInput);glutTimerFunc(0, update, 0);initMenu();myinit();glutMainLoop();return 0;
}