Scene Management -- scene graph
Scene Management -- scene graph
http://blog.csdn.net/soilwork/article/details/3977539
仅供个人学习使用,请勿转载,勿用于任何商业用途。
下面是一个简化的游戏引擎数据流,每一帧,scene data作为数据源,输入到物理/碰撞检测和AI子系统,子系统更新所有物体的状态(包括位置,光照信息等等),之后数据流入渲染系统,最终显式在屏幕上。
显然,后面两部分的效率都依赖于scene data所传入的数据量大小,所以scene management的目的就是高效的为其他子系统提供数据,他是引擎中最核心的部分。
对scene management来说,要处理的第一个问题就是如何保存场景数据。为了方便讨论,假设我们的场景只包含非常有限的几个物体:地形,房屋和一个人物角色。最简单的方法就是让3个物体都派生于GameObject类,用一个数组保存所有物体以及他们的位置。每帧直接把数组中的所有元素传递给子系统。当然,还需要提供一些类似GetObjByID()或者GetObjByName()的方法以便查找某个对象。对于只有3个物体的系统来说,遍历数组,找到所需要的对象并没有什么不妥。但是当物体增加到几百或者几千以后,这样的方法就无法满足需求了。因此我们也许想改用dictionary来代替数组,以ID或者对象名作为key,这样,查找一个对象的时间就是常量。当然,我们还是每一帧把dictionary中的所有元素都传递给子系统。
你大概正在想这样的方法完全没有技术性可言。确实是这样,不过不要小看最简单的方法,某些类似xx团的跳舞游戏就完全可以用这样的方法完成场景管理:摄像机和观察点位置都几乎不变,看到的也只是一个几乎完全静态的场景。所以只需要在Maya或者相应的编辑器里搭建好场景,导出数据保存在文件中,游戏运行时,读取文件,把所有对象保存到dictionary里,然后依次传递给其他子系统,一个xx团克隆就实现了。
假如你希望写一些比xx团高级的游戏,比如《无冬之夜:山寨版》,由于场景将变的非常大,你考虑的第一步就是如何组织场景。
注意到原版的无冬里不同地点的很多建筑都是类似的,不同的地下城,相似的房间。你会如何建立这样的场景呢?比如在A点和B点都有非常相似的木屋,又或者酒店里100间完全类似的客房。如果你是Brute Force超级爱好者,也许会在A点搭建一个模型,然后又在B点搭建一个模型,或者在酒店100个不同的位置建立100个相同的模型。考虑到一间木屋可能有n个房间,每个房间里可能有床,柜子,桌子,桌子上有书,刀叉,盘子,盘子里有鱼,鱼身上正站着一只可爱的苍蝇,就算不需要为每个相同的木屋建立独立模型,而是把建好的模型副本分别放到场景中的相应位置,对于有100间木屋的情况来说,也是相当可怕的工作量。
我们当然不想这样做,理想的方法是搭建好一个木屋,以及里面所有物体,把木屋以及其中的物体作为一个整个,添加到场景时,只需要指定木屋的世界坐标,木屋内的所有物体就能自动获得其相应的坐标。为了达到这样的目标,在建立木屋时,就不能以世界坐标来记录木屋中的物体位置,而要以局部坐标,或者说相对坐标。比如房间A相对于木屋原点的坐标为(0,1),房间B的相对坐标为(0,2),而桌子相对于房间A的坐标又为(0,1),等等。当把屋子放到世界坐标中的(x,y)位置时,房间A的世界坐标就等于(x,y)+ (0,1),桌子的世界坐标则是 (x,y) + (0,1) + (0,1)。局部坐标还能处理一类很常见效果,就是当你装备上一个新武器,比如一把剑之后,无论你的角色如何移动,剑总是在角色的手中。
因此,scene management的任务除了之前讨论的保存场景中所有物体之外,还需要记录物体之间的相互位置关系以及位置的更新,专业的说法则是保存物体层次结构并提供统一的坐标系统。通常,实现上述功能的系统就称为Scene Graph. 也许你已经听过这个术语无数次,这却是一个非常不精确的术语,因为实际的scene graph的数据通常是tree而非graph,所以在后面的部分,我都使用scene tree来代替scene graph。你肯定想看看一个scene tree是什么样子,下面就是一个例子:
Root
|
|----村庄-----房屋A---------房间A-----桌子----。。。。。
| | |
| |---房屋B |-- 房间B
| |
| |---房屋C
|
|----野外雕像
|
|
|---玩家A-----火把---火焰----烟雾
| |
| |--火把光源
|
|----Npc A。。。。。。。。。。
基本上,这就是一个有n个可变节点的树,当然,你也可以使用其它任何你觉得合适的数据结构,只要能完成相同的任务就可以。树中的每个node都保存着两组坐标:世界坐标和局部坐标。每个node的世界坐标都等于其父节点坐标+自身局部坐标。比如,村庄和玩家都链接在跟节点,所以他们的局部坐标就是世界坐标;而房屋A连接在村庄上,他的坐标就等于村庄的世界坐标+房屋A的局部坐标。每当一个节点位置发生变化之后,就遍历其子节点更新坐标,保证火把总是在玩家手中。如果你有实现骨骼动画的经验,就会发现这其实和骨骼动画的原理是一样的,只不过我们把这个想法从模型范围扩大到了整个世界范围。
特别需要注意的是,纯粹的scene tree描述的是物体的空间逻辑层次关系(很高深的词汇吧,我自己发明的),而并非空间关系!这初看起来有些矛盾,也是scene tree最容易被误解的地方,不过理解2者的区别非常,非常重要。举例来说,房屋A虽然是村庄的子节点,但A可以不在村庄所覆盖的空间范围内,A完全可以在村庄之外的某个山头上,只是我们这里恰好用A相对于村庄的位置作为他的局部坐标而已。更明显的例子是,玩家手中的火把肯定不属于玩家模型所占的空间范围之内,在裁剪时(虽然我们暂时还没讨论到裁剪),你不能说因为玩家是可见的,玩家A的所有子节点(火把,光源,烟雾)就是可见的,火把有可能刚好超出了显示范围。永远不要使用scene tree来进行裁剪,你不会得到任何好结果。如果有人告诉你他用quadtree,BSP或者任何空间划分结构来实现scene tree,那么他得到的肯定不是一个纯粹的scene tree(我会在下一次详细讨论这一点)。
你可能想到了,可以把储存场景数据的功能合并到scene tree中来,不再需要array或者dictionary,scene tree中的每个节点就是场景中的一个对象。当然,这样就失去了快速访问对象的功能,或者为scene tree编写一些额外的代码以支持对象查找。如果你仍然决定保留最初的dictionary设计,显然,scene tree中的所有对象就仅仅是对dictionary中元素的引用而已。
目前为止,讨论了scene management两个最基本的任务或者说功能:保存和组织场景数据。既然你已经知道如何组织大型场景,新的问题也来了,你的游戏世界变的很大,覆盖了10w * 10w 大小的区域,有成千上万个对象,显然不能都把这些物体同时加载到内存中,并且全部发送给子系统进行计算,特别是对于最终的渲染结果说,只有一小部分场景是可见的,因此,动态加载和裁减也是scene management必不可少的功能。
to be continue…….
ps: 本来要写关于model design的第三部分,不过后来发现如果不介绍场景管理,实在很难把最终的设计结果说清楚,所以打算先写一部分关于scene management的文章,再继续讨论模型:)