游戏开发笔记(八)——场景对象管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mooke/article/details/9615491

        上一篇《游戏系统开发笔记(七)——对象系统设计》简单介绍了下对象系统的设计,继续考虑游戏开发所需要完成的工作会发现还少一个很重要的点——对象的活动场所。

        游戏中,我们常常需要把整个游戏世界中零散的角色按照一定方式组织到一起,使它们...额...发生关系。有很多与之相关的许多问题或许会遇到:我这个技能最远攻击距离是多少?范围有多大?这个怪物离我多近的时候会主动攻击我?有些地形我是否可以通过?以什么样的方式通过??十分接近怪物的时候我是会被撞到停下,还是从中穿过?等等等等,诸如此类。

        不只是RPG游戏,就连上一篇提到的贪吃蛇这样简单的游戏,也或多或少的遇到上面谈到的一些问题,比如蛇头的位置、蛇身覆盖的区域、食物的位置,以及——更复杂一点来说,如果放了好几条有竞争关系的Npc蛇抢食物的情况。最为直接的思考得到的结论通常是我们需要一张地图,处理坐标有关的问题至少需要一张地图,有地图才有坐标的概念。对于2D游戏来说,地图通常是一个维护二维矩阵的TileMap对象。


        简单介绍下TileMap。

        逻辑上一个TileMap对象通常表示一个具体的地图,包括地图名称、编号、大小、坐标表示、障碍信息等。

        因为2D游戏的活动空间只在一个平面上,所以TileMap的坐标表示通常采用一个二维矩阵。然后因为众所周知的,计算机的状态表达能力是有限的(资源是一方面,要模拟现实世界信息必须经过AD转换也必然损失信息),所以这里每个矩阵单元一定是具有一定大小的,而且出于资源效率考虑,TileMap需要给这个数据结构定义坐标精度,即一个矩阵单元代表多少实际像素大小。

        障碍信息通常是一个坐标集合,游戏启动时转换为各个矩阵单元上的障碍状态。


        但是,地图产生只能解决坐标的问题,还有许多诸如视野、攻击范围之类的问题解决不了。而且TileMap作为一个概念上较为纯粹的“地图”,把其它功能也堆进去的话就不那么合适了。而且...嗯...我觉得许多模块要频繁的和TileMap这种小喽啰打交道多少让人觉得有点不放心。


        所以这时候“场景(Scene)”来拯救地球了。

        比起Map,Scene在概念上更加适合解决上面提到的这些问题,比方说,“把一个角色加入到场景中”听起来肯定比“把一个角色放到地图里更好”。上面絮絮叨叨了这么多,Scene要做哪些事情也大致上有线索了:

        1、一个场景要有一张地图

        2、场景要负责对象管理

        3、场景要表示位置关系(AOI)

        简单的说,就是包装了Map,增加了对象管理功能,同时产生一些场景事件(如位置关系变化、对象转移等)以及对外提供位置关系查询等功能。


        第一点在介绍了TileMap之后我想没必要细说了。


        第二点说明了场景对象也是要介入角色对象生命周期管理的,创建一个有效角色之后需要将它插入到场景,而删除之前需要先从场景拿掉,特别的,主动删除场景的时候需要对正在交由该场景管理的对象进行操作(删除或是转移)。

        结合上一篇《游戏系统开发笔记(七)——对象系统设计》里的思路,我们要做的就是先拿到场景对象(在哪里创建),然后将逻辑对象插入其中(对象指针向上转型为场景层对象指针)。

        另一种对象和场景关联的方案是把场景层对象和逻辑层对象彻底隔离,场景层对象以指针的方式挂到逻辑对象上。这样的话,实现上面这个创建流程的话就变成,先构建场景层对象,之后把这个对象拿给逻辑层对象进行构造(当然,也可以构建完之后塞给它)。而产生场景事件要求对逻辑对象进行操作的话,通常需要借由一个Handler对象来完成(比如,场景层实现ISceneObjHandler并提供相应操作函数,场景对象实现方法GetHandler()和SetHandler(...),然后逻辑层继承一下ISceneObjHandler实现其具体操作后SetHandler,以此达到解耦的目的)。

        这种做法比前者实现起来和概念上都绕一些,但好处的话,比如说要把角色对象做成对象池的形式,那么就可以节约构建场景对象那部分的开销,也使得场景层管理对象时灵活一点。


        第三点的可见关系是场景对象管理中可能需要密集计算的地方,试想,场景中每个角色每次改变位置的时候可见关系都可能发生改变,而由此产生的关系改变事件又需要一一被处理,除此之外,网游的话位置变化还需要对客户端进行广播。再,如果一台计算机同时开放多个常见,每个场景大量角色在移动...


        下面我们把镜头对准场景中的某个Npc,那么它往前走那么一小步,程序上需要做多少事呢?


        首先是对象改变坐标,然后是通知其它对象自己的位置已改变,这时其它对象各自判断和该对象的这种位置关系是否需要处理,是则转入相应处理流程。改变坐标的流程不解释,通知才是重头戏。


        有那么几种处理AOI问题的方法,下面简单介绍:

        其一当然是最简单粗暴行之有效的——全场景对象广播,言下之意不管你在不在意喜欢还是反抗总之我话先撂那儿了。好处不言而喻,实现起来异常简单,可以工作,不容易被虫蛀。能接受的情况是,场景比较小,对象不会很多的情况(值得一提的是这种情况下就算用其他方法优化也效果不甚明显)。


        其二是早期劳动人民比较惯用的做法——切豆腐,即事先把地图区域划分成一定大小的等大小网格(一般至少要能覆盖可见范围那么大),对象移动时只通知本网格和几个相邻网格的所有对象(可以想象成信号塔)。多了些维护网格的代码,但绝对行之有效,对于大场景多角色的情况优化较明显,实现起来也不困难。缺点的话,主要是视野关系不够灵活,还是多了不少冗余位置判断(尤其是小视野对象多的情况),而且跨网格移动要瞬时做大量操作(卡帧的隐患)。


        其三是十字链表法,即以x和y两条有序链表来维护场景对象,位置变化时只需要交换节点位置(可能交换),然后往前后遍历查找需要通知的对象,因为链表有序,所以超出视野就立即结束遍历。这种方案省去了第二点网格随场景变大而节点越来越多的固定开销,而且也不会有跨网格移动需要大量操作的问题,处理起来相当灵活,在一些人眼里甚至是最具运算效率的方法(这个本人没有验证过,没有自己的结论)。

        其假设所有对象具有相同大小视野的版本实现起来尚且简单,但若每个对象可能具有不同视野的情况实现起来就略复杂了,而且目测效率大幅度降低。由于涉及到点和线段重叠关系统计,我觉得考虑使用线段树来取代十字链表,这样一来就是x、y各有一棵线段树,排序方面应该会优于链表但是因为二叉树前后遍历没线性结构那么快,所以这个效率具体来说还是和操作的种类和频率有关的。

具体实现就不先展开了,这里有十字链表AOI的一些额外资料:

http://blog.codingnow.com/2012/03/dev_note_13.html

http://www.codedump.info/?p=388


        还有一种方法是使用四叉树来管理场景空间,思路上有点类似把第三点中的线段树部分把x、y轴合并之后直接以二维方式来处理问题,具体不展开,有兴趣的自己试试。和第三点的效率比较待测。


        最后是众多场景通常还需要一个场景对象(这里只场景本身)管理器,用于集中控制各个场景的生命周期,考虑做成一个Singleton吧。

没有更多推荐了,返回首页