Iphone 游戏开发游戏引擎剖析.docx
- 文档编号:18308037
- 上传时间:2023-08-15
- 格式:DOCX
- 页数:29
- 大小:296.86KB
Iphone 游戏开发游戏引擎剖析.docx
《Iphone 游戏开发游戏引擎剖析.docx》由会员分享,可在线阅读,更多相关《Iphone 游戏开发游戏引擎剖析.docx(29页珍藏版)》请在冰点文库上搜索。
Iphone游戏开发游戏引擎剖析
【Iphone游戏开发】游戏引擎剖析
发布于2011-07-18
为了解决“如何在IPHONE上创建一个游戏”这个大问题,我们需要首先解决诸如“如何显示图像”与“如何播放声音”等一系列小问题。
这些问题关系到创建部分游戏引擎。
就像人类的身体一样,游戏引擎的每个部分虽然不同,但是却都不可或缺。
因此,首先从游戏引擎剖析开始本章。
我们将会讨论一个游戏引擎的所有主要部分,包括应用程序框架、状态机、图像引擎、物理引擎、声音引擎、玩家输入和游戏逻辑。
写一个好玩的游戏是一项牵扯到很多代码的大任务。
非常有必要从一开始就对项目进行良好的,有组织的设计,而不是随着进度的进行而到处杂乱添加代码。
就像建造房屋一样,建筑师为整幢房屋勾画蓝图,建筑工人以此来建造。
但是,许多对游戏编程不熟悉的编程人员会从根据导读建造出房屋的一部分,并随着学习的进行为其添加房间,这无疑将会导致不好的结果。
图2-1游戏引擎的功能结构
图2-1显示了一个适用于大部分游戏的游戏引擎结构。
为了理解一个游戏引擎的所有部分和它们是如何工作在一起的,我们可以先为整个游戏做设计,然后再创建我们的应用程序。
在以下的几个小节中,我们的讲解内容将会涵盖图2-1的每个部分。
∙应用程序框架
∙游戏状态管理器
∙图像引擎
应用程序框架
应用程序框架包含使应用程序工作的必须代码,包括创建一个应用程序实例和初期化其他子系统。
当应用程序运行时,会首先创建一个框架类,并接管创建和销毁状态机、图像引擎和声音引擎。
如果我们的游戏足够复杂以至于它需要一个物理引擎,框架也会管理它。
框架必须适应于我们所选择的平台的独特性,包括相应任何的系统事件(如关机与睡眠),以及管理载入与载出资源以使其他的代码只需要集中与游戏。
主循环
框架会提供主循环,它是一切互动程序后的驱动力量。
在循环中的每一次迭代过程中,程序会检查和处理接受到的事件,运行游戏逻辑中的更新并在必要时将内容描画到屏幕上。
(参见图2-2)
图2-2主循环序列
主循环如何实现依赖于你使用的系统。
对于一个基本的控制台程序,它可能是一个简单的while循环中调用各个函数:
1.while(!
finished){
2.handle_events();
3.update();
4.render();
5.sleep(20);
6.}
注意到这里的sleep函数。
它使得代码休眠一小段时间不致于占用全部的CPU。
有些系统完全不想让用户代码那些写,它们使用了回调系统以强制程序员常规的释放CPU。
这样,当应用程序执行后,程序员注册一些函数给系统在每次循环中回调:
1.voidmain(void){
2.OS_register_event_handler(myEventHandler);
3.OS_register_update_function(myUpdate);
4.OS_register_render_function(myRender);
5.}
一旦程序执行后,根据必要情况,那些函数会间隔性的被调用。
IPHONE是最接近后面这个例子。
你可以在下一章和IPHONESDK中看到它。
游戏状态管理器
一个好的视频游戏不仅有一组动作来维持游戏:
它会提供一个主菜单允许玩家来设定选项和开始一个新游戏或者继续上次的游戏;制作群屏将会显示所有辛勤制作这款游戏的人员的名字;而且如果你的游戏没有用户指南,应该一个帮助区域会给用户一些提示告诉他们应该做什么。
以上任何一种场合都是一种游戏状态,并且代表中一段独立的应用程序代码片段。
例如,用户在主菜单调用的函数与导航与用户在制作群屏调用的是完全不同的,所以程序逻辑也是不同的。
特别的是,在主菜单,你可能会放一张图片和一些菜单,并且等待用户选择哪个选项,而在制作群屏,你将会把游戏制作人员的名字描绘在屏幕上,并且等待用户输入,将游戏状态从制作群屏改为主菜单。
最后,在游戏中状态,将会渲染实际的游戏并等待用户的输入以与游戏逻辑进行交互。
以上的所有游戏状态都负责相应用户输入、将内容渲染到屏幕、并为该游戏状态提供相对应的应用程序逻辑的任务。
你可能注意到了这些任务都来自于之前讨论的主循环中,这是因为它们就是同样的任务。
但是,每个状态都会以它们自己的方式来实现这些任务,这也就是为什么要保持他们独立。
你不必在主菜单代码中寻找处理游戏中的事件的代码。
状态机
状态管理器是一个状态机,这意味着它跟踪着现在的游戏状态。
当应用程序执行后,状态机会创建基本的状态信息。
它接着创建各种状态需要的信息,并在离开每种状态时销毁暂时存储的信息。
状态机维护着大量不同对象的状态。
一个明显的状态是用户所在屏幕的状态(主菜单、游戏中等)。
但是如果你有一个有着人工智能的对象在屏幕上时,状态机也可以用来管理它的“睡眠”、“攻击”、“死亡”状态。
什么是正确的游戏状态管理器结构?
让我们看看一些状态机并决定哪种最适合我们。
有许多实现状态机的方式,最基本的是一个简单的switch语句:
1.classStateManager{
2.voidmain_loop(){
3.switch(myState){
4.caseSTATE_01:
5.state01_handle_event();
6.state01_update();
7.state01_render;
8.break;
9.caseSTATE_02:
10.state02_handle_event();
11.state02_update();
12.state02_render;
13.break;
14.caseSTATE_03:
15.state03_handle_event();
16.state03_update();
17.state03_render;
18.break;
19.}
20.}
21.};
改变状态时所有需要做的事情就是改变myState变量的值并返回到循环的开始处。
但是,正如你看到的,当我们加入越来越多的状态时,代码块会变得越来越大。
而且更糟的是,为了使程序按我们预期的执行,我们需要在程序进入或离开某个状态时执行整个任务块,初始化该状态特定的变量,载入新的资源(比如图片)和释放前一个状态载入的资源。
在这个简单的switch语句中,我们需要加入更多的程序块并保证不会漏掉任何一个。
以上是一些简单重复的劳动,但是我们的状态管理器需要更好的解决方案。
下面一种更好的实现方式是使用函数指针:
1.classStateManager{
2.//thefunctionpointer:
3.void(*m_stateHandleEventFPTR)(void);
4.void(*m_stateUpdateFPTR)(void);
5.void(*m_stateRenderFPTR)(void);
6.voidmain_loop(){
7.stateHandleEventFPTR();
8.m_stateUpdateFPTR();
9.m_stateRenderFPTR();
10.}
11.voidchange_state(void(*newHandleEventFPTR)(void),
12.void(*newUpdateFPTR)(void),
13.void(*newRenderFPTR)(void)
14.){
15.m_stateHandleEventFPTR=newHandleEventFPTR;
16.m_stateUpdateFPTR=newUpdateFPTR;
17.m_stateRenderFPTR=newRenderFPTR
18.}
19.};
现在,即使我们处理再多状态,主循环也足够小而且简单。
但是,这种解决方案依然不能帮助我们很好的解决初始化与释放状态。
因为每种游戏状态不仅包含代码,还有各自的资源,所以更恰当的做法是将游戏状态作为对象的属性来考虑。
因此,接下来,我们将会看看面向对象(OOP)的实现。
我们首先创建一个表示游戏状态的类:
1.classGameState
2.{
3.GameState();//constructor
4.virtual~GameState();//destructor
5.virtualvoidHandle_Event();
6.virtualvoidUpdate();
7.virtualvoidRender();
8.};
接着,我们改变我们的状态管理器以使用这个类:
1.classStateManager{
2.GameState*m_state;
3.voidmain_loop(){
4.m_state->Handle_Event();
5.m_state->Update();
6.m_state->Render();
7.}
8.voidchange_state(GameState*newState){
9.deletem_state;
10.m_state=newState;
11.}
12.};
最后,我们创建一个指定具体游戏状态的类:
1.classState_MainMenu:
publicGameState
2.{
3.intm_currMenuOption;
4.State_MainMenu();
5.~State_MainMenu();
6.voidHandle_Event();
7.voidUpdate();
8.voidRender();
9.};
当游戏状态以类来表示时,每个游戏状态都可以存储它特有的变量在该类中。
该类也可以它的构造函数中载入任何资源并在析构函数中释放这些资源。
而且,这个系统保持着我们的代码有很好的组织结构,因为我们需要将游戏状态代码分别放在各个文件中。
如果你在查找主菜单代码,你只需要打开State_MainMenu类。
而且OOP解决方案使得代码更容易重用。
这个看起来是最适合我们需要的,所以我们决定使用它来作为我们的状态管理器。
图像引擎
图像引擎负责视觉输出,包括用户借以交互的图形用户界面(GUI)对象,2D精灵动画或3D模型动画,并渲染的背景与特效。
虽然渲染2D与3D图片的技术不尽相同,但他们都完成相同的一组图形任务,包括纹理和动画,它们的复杂度是递增的。
纹理
对于显示图片,纹理是中心。
2D时,一个平面图片是以像素为单位显示在屏幕上,而在3D时,一组三角行(或者被称为网格)在数学魔法作用下产生平面图片并显示在屏幕上。
这以后,一切都变得复杂。
像素、纹理与图片
当进行屏幕描绘时,基本单位是像素。
每个像素都可以被分解为红、绿、蓝色值和我们马上要讨论的Alpha值。
纹理是一组关于渲染一组像素的数据。
它包含每个像素的颜色数据。
图片是一个更高层的概念,它并非与一组特殊的像素与纹理相关联。
当一个人看到一组像素,他的大脑会将它们组合成一幅图片,例如,如果像素以正确的顺序表示,他可能会看到一幅长颈鹿的画像。
保持以上这些概念独立是非常必要的。
纹理可能包含构成长颈鹿图片的像素:
它可能包含足够的像素来构成一只长颈鹿的多幅图片,或者仅包含构成一幅长颈鹿图片的像素。
纹理本身只是一组像素的集合,它并不固有的知道它包含的是一幅图片。
透明度
在任一刻,你的游戏会有几个或者多个物体渲染在屏幕上,其中一些会与另外一个重叠。
问题是,如何知道哪个物体的哪个像素应该被渲染出来呢?
如果在最上层的纹理(在其他纹理之后被描画)是完全不透明的,它的像素将会被显示。
但是,对于游戏物体,可能是非矩形图形和部分透明物体,结果会导致两种纹理的结合。
2D图片中最常用的混合方式是完全透明。
假如我们想画一幅考拉(图2-3)在爬在桉树顶上(图2-4)的的图片。
考拉的图片以矩形纹理的方式存储在内存中,但是我们不想画出整个矩形,我们只想画出考拉身体的像素。
我们必须决定纹理中的每个像素是否应该显示。
图2-3考拉纹理
图2-4桉树纹理
有些图片系统通过添加一层遮罩来达到目的。
想象我们在内存中有一幅与考拉纹理大小一样的另外一份纹理,它只包含白色和黑色的像素。
遮罩中每个白色的像素代表考拉应该被描画出来的一个像素,遮罩中的黑色像素则代表不应该被描画的像素。
如果当我们将考拉描画到屏幕上时,有这样的一个遮罩层,我们就可以检查考拉对应的像素并仅将需要描画的像素表示出来。
如果每个像素有允许有一组范围值而不是二进制黑/白值,那么它还可以支持部分透明(参见图2-5)。
图2-5考拉遮罩纹理
纹理混合
为纹理而准备的存储容量大到足以支持每个像素都有一个范围值。
典型的是,一个Alpha值占一个字节,即允许0-255之间的值。
通过合并两个像素可以表现出有趣的视觉效果。
这种效果通常用于部分透明化,例如部分或完全看透物体(图2-6)。
图2-6部分透明的绿色矩形
我们可以为每个像素来设定Alpha以决定它们如何被混合。
如果一个像素允许的范围值为0-255,Alpha的范围值也同样应当为0-255。
尽管红色值为0表示当描画时不应该使用红色,但Alpha值为0则表示该像素根本不应该被描画。
同样,128的红色值表示描画时应该使用最大红色值的一半,128的Alpha值表示当与另外一个像素混合时,应该使用该像素的一半颜色值。
当混合物体时,正确的排列物体顺序是非常重要的。
因为每个混合渲染动作都只会渲染源物体与目标物体,首先被描画的物体不会与后描画的物体发生混合。
尽管这在2D图片中很容易控制,但是在3D图片中变得非常复杂。
旋转
在2D图片中,大部分的纹理都会被直接渲染到目标上而不需要旋转。
这是因为标准硬件不具备旋转功能,所以旋转必须在软件中计算完成。
这是一个很慢的过程,而且容易产商低质量的图片。
通常,游戏开发人员会通过预先渲染好物体在各个方向的图片,并当物体某个方向上时,为其在屏幕上描画正确的图片来避免以上问题的发生。
在3D图片中,旋转的计算方式与照明相同,是硬件渲染处理过程中的一部分。
剪贴
由于某些在后面章节解释的原因,纹理的另外一个重要方面是剪贴。
尽管我们目前的例子都是将源纹理直接描画到目标纹理上,但是经常会出现的情况是,需要将部分源纹理描画到目标纹理的有限的一部分上。
例如,如果你的源纹理是在一个文件中含有多幅图片,裁剪允许你仅将希望描画的部分渲染出来。
剪贴同样允许你进行描画限制到目标纹理的一小部分上。
它可以帮你通过纹理映射以3D方式渲染物体,将纹理铺盖到三角形组成的任意形状的网格上。
例如,一个纹理可以表示衣服或动物的毛皮,而且当3D角色穿着它移动的死后可能产生褶皱。
这时候的纹理通常被称作皮肤。
动画
通过渲染连续的图片,我们可以确保玩家看到一个移动的物体,尽管他所做的仅仅是在同样的像素上,但这些像素在快速的改变颜色。
这就是动画的基本概念。
2D动画很简单,但3D动画通常牵扯到更多的物体与动作,因此更复杂。
除了讨论动画技巧,这一节还会讨论主要的优化类型可以使得我们的图像引擎有效的和可靠的完成复杂的不可能以原始方式来完成的图形任务。
一些主要的优化技巧包括淘汰、纹理排序、使用智能纹理文件、资源管理和细节级别渲染。
2维动画:
精灵
在2D图像中,如果我们要渲染马儿奔驰的完整场景,我们可以先创建出马儿的奔驰各个姿态的图片。
这种图片成为一帧。
当一帧接一帧的渲染到屏幕上时,马儿动起来了(见图2-7)。
这和电影创建动画的方式非常相似,电影也是通过展示连续的帧来达到移动效果。
图2-7斯坦福德的马的动作
为了将这些帧保存在一起,我们将它们放在同一个纹理中,称为精灵。
通过前面章节我们描述的裁剪方法,将只包含当前帧内容的部分渲染到屏幕上。
你可以将每一帧渲染多次直到渲染该序列的下一帧。
这取决于你希望你的动画播放的多快,以及提供了多少帧图片。
事实上,通过渲染的帧速和顺序,你可以创造出多种特效。
3维动画:
模型
与2D动画中每次重画时都维护一幅用来渲染的图片--精灵不同,3D动画是通过实际的计算的计算运动的几何效果。
正如我们之前描述的,所有的3D物体都由包含一个或多个三角形构成,被称作网格。
有多种可以使网格动起来的方法,这些技术与游戏发展与图形硬件有关。
这些技术后的基本概念都是:
关键帧。
关键帧与我们之前讨论的2D动画中的帧有些许不同。
2维动画的美术人员画出每一帧并保存在纹理中。
但是在3D中,只要我们保存了最特殊的几帧,我们就可以通过数学计算得到其他帧。
最开始的使用网格动画的游戏实际上存储了网格的多个拷贝,每一个拷贝都是都在不同的关键帧方向上。
例如,如果我们在3D中渲染马儿,我们应该为上面精灵的每一个关键帧都创建网格。
在time
(1),第一帧被描画出来,在time
(2),第二针被描述出来。
在主要关键帧之间,使用一种叫做“插值”的技术方法。
因为我们知道time
(1)的关键帧和time
(2)的关键帧有着相同数量的三角形,但是方向稍有区别,我们可以创建当前时间点的临时的,融合了前面两个网格的网格。
所以在时间time(1.5),临时网格看起来正好介于time
(1)与time
(2)之间,而在time(1.8),看起来更偏向于time
(2)。
以上技术效率低下的原因是很明显的。
它仅在只有少量的三角形和少量的关键帧时才是可接受的,但是现代图像要求有高解析度与生动细节的动画。
幸运的是,有更好的存储关键帧数据的方法。
这就技术叫做“骨骼动画”(skeletalanimation,orbonerigging)。
还是以马儿为例,你可能注意到了大多数的三角形都是成组的移动,比如头部组、尾部组和四肢组。
如果你将它们都看成是骨头关联的,那么将这些骨头组合起来就形成了骨骼。
骨骼是由一组可以适用于网格的骨头组成的。
当一组骨骼在不同方向连续的表示出来的时候,就形成了动画。
每一帧动画都使用的是相同的网格,但是都会有骨头从前一方位移动到下一个方位的细小的动作变化。
通过仅存储在某一个方位的网格,然后在每一关键帧时都利用它,我们可以创建一个临时的网格并将其渲染到屏幕上。
通过在两个关键帧之间插值,我们可以以更小的成本来创建相同的动画。
动画控制器
动画控制器对象在抽象低层次的任务非常有用,如选择哪一帧来渲染,渲染多长时间,决定下一帧代替前一帧等。
它也起到连接游戏逻辑与图像引擎等动画相关部分的作用。
在顶层,游戏逻辑只关心将设某些东西,如播放跑动的动画,和设定它的速度为可能应该每秒跑动数个单位距离。
控制器对象知道哪个帧序列对应的跑动动画以及这些帧播放的速度,所以,游戏逻辑不必知道这些。
粒子系统
另外一个与动画控制器相似的有用对象是粒子系统管理器。
当需要描画高度支离破碎的元素,如火焰、云朵粒子、火苗尾巴等时可以使用粒子系统。
虽然粒子系统中的每个对象都有有限的细节与动画,它们组合起来却能形成富有娱乐性的视觉效果。
淘汰
最好的增加每秒钟描画到屏幕上的次数的方法是在每次迭代中都减少描画在屏幕上的数目的总量。
你的场景可能同时拥有成百上千的物体,但是如果你只需要描述其中的一小部分,你仍然可以将屏幕渲染得很快。
淘汰是从描画路径上移除不必要的物体。
你可以在多层次上同时进行淘汰。
例如,在一个高层次,一个用户在一间关闭了门的房间里面是看不到隔壁房间的物体的,所以你不必描画出隔壁其他物体。
在一个低层次,3D图像引擎会经常移除部分你让它们描画的网格。
例如,在任意合适的给定时间点,半数的网格几何体在摄影机背面,你从摄像机中看不到这些网格,看到的只是摄影机前方的网格,因此,当网格被渲染时,所有的在摄影机背后的网格都会被忽略。
这叫做背面淘汰。
纹理排序
每次当一个物体被渲染到屏幕上时,图形硬件都会将纹理源文件载入到内存中。
这是被称作上下文交换(contextswitching)的一部分。
如果要将三幅图片描画到屏幕上,而其中两幅图片共用同一个纹理资源,有两种办法来处理纹理排序:
高效的方法是连续的渲染两幅共享资源的图片,这样只需要以此上下文交换,而低效的方法则需要两次上下文交换。
你不应该将第三幅图片放在共享纹理的两幅图片之间描画。
在渲染处理过程中,通过排列共享纹理的物体可以减少上下文交换的次数,从而提高渲染速度。
纹理文件
在一开始就计划好纹理组织结构可以帮助你以最优化方式排列你的纹理。
假设你准备在你的游戏中描画几何体,一个主角和一些生物。
如果前两个关卡是草地,接下来的关卡是沙漠,你可以将所有的树木、草、灌木、岩石以及花儿的图片来放到一起来渲染前两关,并将沙子图片放在另外一个纹理文件中用来渲染第三关。
同样的,你可以将玩家虚拟人偶放到一个纹理中。
如果所有的生物在所有关卡中都用到了,最优的方式可能是将它们放在一个纹理文件中。
但是,如果第一关有吼猴与鼯鼠,而第二关只有森林鼠与苏里南蛤蟆,你可以将第一次前两种动物放在一个纹理中,将后两种放在一个纹理中。
资源管理
大部分的视频游戏在一个时间点只会渲染它们所有图片内容的一小部分。
将所有纹理同时载入内存是非常低效的。
幸运的是,游戏设计通常决定了哪些资源在游戏的各个章节是可见的。
通过保留必须的纹理为载入状态并卸载不使用的纹理,可以最有效的利用有限的内存资源。
还是使用前一节的例子,当游戏引擎载入第一关时,资源管理代码必须确保吼猴与鼯鼠的纹理被载入到内存中。
当程序进行到下一关时,资源管理代码会卸载那些纹理,因为它已经知道它们不会在第二关被使用。
细节层次
另外一个优化技巧,尤其是对3D图像,叫做细节层次。
考虑当一个物体远离摄像机时,它看起来非常小,而且大部分细节都丢失了。
你可以描画一个同样大小,却仅拥有简单网格的物体,或者甚至一张平面贴图。
通过保留不同细节层次的物体的副本在内存中,图像引擎可以根据与摄像机的距离决定使用哪个副本。
物理引擎
物理引擎是游戏引擎中负责距离、移动与其它游戏物理相关的部分。
不是所有的游戏引擎都需要物理引擎。
但是所有的图形游戏都在某种程度上有物理相关代码。
不相信吗?
用“井字游戏”(tic-tac-toe)来举个例子。
确实是一个非常简单的游戏,但是即使这个游戏也有物理部分。
当玩家选择一个正方形用来标记的时候,我们必须检查选择的正方形是否有效。
如果是,我们就将打上标记并判断玩家是否获胜。
这就是物理引擎所完成的两项基本任务的例子:
侦测与解决。
碰撞侦测与碰撞解决
在你脑海中保持这两方面的独立性非常重要。
在游戏代码中,侦测是独立于判定的。
不是所有的物体与其它物体会以相同的方式发生碰撞,进而不是被侦测到的所有碰撞都会以相同的方式来解决。
例如,让我们假想一个游戏:
O’Reilly野生冒险。
假如玩家的虚拟人偶发现在自己无意间来到了O’Reilly野生保护区,它必须避免奇怪和危险的动物并回到安全的地方。
这个游戏中会发生以下几种物理交互:
1.玩家与地图的碰撞
2.动物与地图的碰撞
3.玩家与动物的碰撞
4.玩家与目的地的碰撞
第一种,玩家与地图的碰撞,非常简单。
我们检测玩家的身体区域边界与关卡中的墙。
如果玩家将与墙发生碰撞,我们稍微位移一下玩家,以使其不会与墙发生碰撞。
第二种交互稍微复杂一点。
我们使用同样的侦测方法:
检测动物的身体区域与关卡中的墙。
但是我们的解决方法有一些不同,因为动物不是玩家控制,而是由电脑控制。
我们解决情况
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Iphone 游戏开发游戏引擎剖析 游戏 开发 引擎 剖析