Log4X

链路纵横
一个3D小游戏开发经验分享。

最近准备接触一下3D手机游戏开发,因此利用空闲时间制作了一个iPhone 上面的3D小游戏。因为以前没有在实际项目中应用过OpenGLES 2.0,通过这次开发,积累了不少实战经验,为了分享经验,也为了帮自己增强记忆,选择了写博文。

项目的地址:http://code.google.com/p/invader-demo-with-oolongengine/

源码:

svn checkout http://invader-demo-with-oolongengine.googlecode.com/svn/trunk/ invader-demo-with-oolongengine-read-only

截图:

 http://code.google.com/p/invader-demo-with-oolongengine/downloads/detail?name=iOS%20Simulator%20Screen%20shot%20Feb%2027%2C%202013%208.48.27%20AM.png&can=2&q=#makechanges

在开发过程中遇到问题,我花了很长的时间在网上苦苦寻找答案,但是网上的文章大部分是泛泛而谈,和实战的需要相差很远,而在这篇博文里,我不准备再重复已有的概念,而直接讨论一些个人实战性的经验和结论。

 

关于 3D游戏的实现模式:

我并没有在正儿八经的游戏公司工作过,但通过这个小游戏的制作,我可以谈谈我做一个小游戏所经历的步骤,希望可以让读者们举一反三(只针对实现,不包括游戏设计方面的步骤)。

 

第一步,3D模型的制作:

3D模型现在有很多工具来制作,包括著名的Maya。如果是使用商业的游戏引擎,比如Unity,它也提供3D模型制作,当然游戏引擎的功能更多,从模型制作,动画,物理特效,到代码上使用设计好的模型,这些都可以涵盖,因此使用商业游戏引擎的项目,可以从比较困难的图形效果方面解放出来,而把主要精力放在游戏逻辑上。

以上扯远了,作为买不起游戏引擎的小项目,一切还是得自己来。在3D模型制作工具上做好所有的模型以后,我们可以将所有的3D模型导出。3D模型文件格式很多,比较通用的有OBJ文件,这也是我在小游戏中使用的文件格式,后面再详细分析。

3D模型工具的另一项作用是, 可以调整游戏中要使用到的视角参数来预览效果,减少开发中调整各种参数的工作。

 

第二步,实现游戏逻辑:

通常游戏程序,都是一种对游戏内部世界的模拟,比如我这个3D小游戏,是模拟入侵飞船和己方飞船的战斗,就涉及到模拟对方飞船和己方飞船单位的移动,射击,中弹,爆炸等状态。通常游戏都会有一个game loop,从游戏开始一直循环到游戏结束,在每个循环中,游戏逻辑要处理的就是当前这个时间点上,更新每个游戏单位各自的状态和位置。另外在这些循环中,会有新单位产生(比如新入侵飞船加入战斗,或新发射的子弹),也有老单位消失(比如中弹爆炸的入侵飞船,或者击中物体或飞出边界的子弹)。

如果以上的描述让你觉得有些糊涂,我可以举一个更详细的例子:以己方飞船发射的子弹为例,描述它的整个模拟过程:

在某次循环时,时间点为t1时刻,己方飞船发射一颗子弹。于是一个新单位在t1时刻在飞船的位置产生,设为(0,0,0)坐标。现在假设这个子弹时以v1的速度沿着z轴负方向运动,则在下一次循环时(记为t2时刻),这个子弹沿着z轴负方向飞行了v1*(t2-t1)的距离,所以它的坐标变成了(0,0,-v1*(t2-t1))。t3,t4时刻的子弹位置都可以此类推,直到它在某个时刻记为 t’ 它飞经一个入侵飞船(设为(x1,y1,z1)坐标),通过计算子弹当前的位置和(x1,y1,z1)距离小于某个阈值,则可判定子弹击中飞船。于是更新飞船的状态为爆炸,并且移除子弹单位。

随着时间不断推进,游戏过程也在不断的演进。

当然除了以时间作为游戏世界模拟演进的依据之外,也有游戏以循环总次数作为模拟依据,比如最早的《红色警戒》。不过这就带来一个问题,在不同性能的电脑上面,运行的速度不一致(通常在p3以上的电脑上,都没法玩红警,因为太快了,玩家根本反应不过来。。。又扯远了。。。)。

 

第三步,渲染游戏单位:

这是游戏开发中比较重要的一步,经过前面,游戏逻辑已经实现, 游戏的世界已经建立。但问题是所有的游戏单位都只存在于内存中,玩家看不见摸不着。所以我们需要这一步来把整个游戏世界做视觉展现。

通常商用的游戏引擎开发的游戏,这一步都是游戏引擎接管,但对于我这种买不起游戏引擎的IT屌丝,所有的活都得自己来。

前面我们有每个游戏单位的模型文件,因此我们需要一个模型文件的解析器,比如OBJ文件,通过调用解析器解析OBJ文件,我们可以得到模型的每一个顶点的世界坐标,纹理坐标以及法向量,另外还可以得到一套顶点索引,每三个顶点为一组,以表示一个三角形(三角形是OpenGLES能处理的基本单位),关于世界坐标,纹理坐标,法向量是什么,可以自行百度,需要说明的是这里的世界坐标,是以模型的中心为原点的坐标系的坐标,因此不管这个游戏单位处于游戏世界的哪个位置,都可以通过简单的平移变换以这个游戏单位的位置为中心渲染它的模型。另外每个OBJ文件都会对应一个图片文件,作为它的纹理,OBJ文件中的纹理坐标,指的就是这个纹理图片中对应的坐标。

 

第四步,处理玩家的控制:

有了前面的所有以后,这个程序还称不上是游戏,反而像是3Dmark之类的跑分软件,只有加入玩家的互动以后才可以称为游戏。控制可以利用游戏目标设备可以提供的控制能力,比如iPhone的触摸屏或者重力传感器,PC的键盘之类,通过游戏模拟逻辑中预留的控制接口,来做到交互,好的交互方式也是游戏成功与否的关键。

 

 3D渲染中遇到的问题:

回顾短短的开发过程中,游戏逻辑方面coding和调试时间非常短也很顺利,绝大多数时间都花费在opengl渲染方面。其中问题包括几方面:

 

OpenGL ES2.0的新接口使用:

OpenGL ES2.0和以前版本的最大区别是可以使用glsl对GPU中的可编程渲染单元进行编程。Glsl的内容可以是hardcode在程序中的一段字符串,也可以放在文本文档中用程序加载得到字符串,然后通过OpenGL提供的相应接口对这个字符串进行编译链接,最终形成用一个index来代表的一个shader program.(有点类似数据库的存储过程,只不过是预设在GPU,而不是数据库服务器上)。

Shader program可以有输入变量,但没有输出变量或返回值,一切输入最终通过program的处理都会变成屏幕上的图形。

一个Shader program的输入可以有很多,包括变换矩阵,顶点,纹理坐标,法向量和纹理等。

而所有的这些输入参数的赋值,都很OpenGL style-按我个人总结的规律,我将它戏称为“不直接赋值法则”。所谓的不直接赋值,就是总是不通过第一手能拿到的一些变量标志如“变量名”,“纹理索引”,“顶点缓冲VBO索引”来赋值,一定是通过某个中介来赋值。

比如shader中uniform和attributes类型输入参数,它们都有自己的字符串变量名,所以赋值的话,要通过OpenGL API先查询(对于attributes称为分配更合适)到这个变量名对应的索引,针对索引赋值。

对于没有变量名,只有索引的纹理,VBO, 则要先通过OpenGL相应的Bind方法将这个索引绑定到特殊的Target上(纹理是纹理单元,VBO是ArrayBuffer),然后针对这个Target进行赋值。

 

投影矩阵的生成:

OpenGL 有内建的model和projection 矩阵,但是对于OpenGLES2.0的程序来说,自己进行矩阵运算会更加方便,并且因为自己拥有投影矩阵和它的逆矩阵,可以脱离OpenGL进行坐标映射和反映射,比如用逆矩阵将用户屏幕touch坐标转变为3D空间坐标。          

这个小游戏里面用了一套第三方的OpenGL矩阵运算库,根据预设的视角生成lookat矩阵和投影矩阵,可一开始画面什么都不显示。检查OpenGL的getError没有任何error产生。怎么验证投影矩阵是否被正确构造?其实很简单,用一个逆认为应该显示在屏幕内的空间坐标用矩阵变换一下就知道了。

说道这个办法,得先大致补一下投影变换的一个知识。投影变换分为透视投影和正视投影,它们在空间几何上的意义,实际上都是将世界坐标中的视景体投影成x,y,z轴边界各从-1到1的一个立方体。只不过透视投影的视景体是金字塔锥形,正视投影则是长方体。

有了以上的结论,就可以用一个你想要在视景体内的空间坐标,尝试做一次投影变换,比如己方飞船的 (0,0,0)坐标,并引入齐次坐标的第4维-w分量,用(0,0,0,1)代入投影矩阵运算,可以得到的结果 (x’,y’,z’,w’),然后进行齐次坐标的归一化为(x’/w’, y’/w’,z’/w’,1) ,如果x’/w’, y’/w’,z’/w’ 都在-1到1之间,那么说明投影矩阵没有问题 。否则就要仔细看看是不是构造矩阵的某一步出了问题。

通过上面这个方法,我最终找到了问题的所在:第三方引入的矩阵运算库,LookAt 矩阵和正视矩阵都不正确!所以血的教训告诉我们:千万不要在google code上随便下个开源库,就以为一定视靠谱的!别人的代码也需要多怀疑。最后纠正问题以后,投影正常。

 

glEnable 和glDisable要配对:

将投影问题修正以后,所有的游戏单位在第一帧时,都可以正常显示了,但是一帧过后,马上又消失。是什么导致第一帧和后续的帧出现区别呢,最容易想到的是,如果第一帧没有改变任何OpenGL内部状态,那么第一帧和后续帧理应一致,所以肯定是某些OpenGL的状态在第一帧完成后没有恢复,导致后续帧的初始状态不同于第一帧。仔细检查后发现渲染开始前glEnable了很多gl内置的功能,在渲染结束后没有glDisable。OpenGL内部都是全局状态,所以在特定情况下改变内部状态以后,恢复原样尤为重要,切不可忘记。

 

以上是小3D游戏开发中得到的一些经验,虽然非常凌乱不成系统,却是绝对的原创经验谈,希望同样也能帮到那位在OpenGL大门口苦苦挣扎的你。

posted on 2013-03-05 23:27  YYX  阅读(1224)  评论(0编辑  收藏  举报