AOI(Area Of Interest),就是感兴趣区域,在MMOPRG游戏服务器上是不可或缺的技术。算是基础的核心技术了。
通俗一点说,AOI就是玩家在场景中实时看到的区域,AOI会随着单位(比如玩家角色)在场景中的位置变化而改变。
广义上,AOI系统支持任何游戏世界中的单位对一定半径范围内发生的事件进行处理。MMOPRG中绝大多数需求只是对AOI范围内发生的,围绕单位离开、进入的事件进行的处理。这两个事件可以算AOI的核心事件。比如,当你进入一个游戏场景时,如果你能看到其他玩家,那背后AOI系统就正在运作。
AOI事件通常驱动场景中单位间的互动逻辑,以及随之产生的网络数据的同步广播。因此,良好的AOI算法和基于AOI算法的优化,是提高游戏性能的关键。AOI实现方案的好坏直接决定了服务器能够承载的同时在线人数上限,也决定了策划对游戏玩法的发挥程度。
回合制和即时制的MMOPRG通常选用不同的AOI方案。
这里只介绍我自己的一点体会,不涉及算法具体实现(网上有大量算法实现)。
严格来说,AOI是碰撞检测的一种特例,除了碰撞集本身,它还包括两状态间的碰撞集差异报告。也就是说,如果单位P在[A位置]AOI可见集为[Old],当单位P从[A位置]移动到[B位置],它的AOI可见集会变成[New],而[Old]和[New]之间的差异有两种可能性:
(1)有单位Q原来在[Old]中,但是并不在[New]中
(2)有单位Q在[New]中,但是原来并不在[Old]中。
为方便理解,单位以玩家为例(如果所有玩家的AOI半径一致),上面描述的直接表现通常是
(1)玩家P的客户端画面会删除玩家Q显示模型,那玩家Q的客户端画面也会删除玩家P
(2)玩家P的客户端画面会添加玩家Q显示模型,那玩家Q的客户端画面也会添加玩家P
很容易想象,AOI的需求最简单的做法是全场景中的所有玩家信息全部同步给客户端。这个方案是O(n^2)的复杂度:
(1)如果需要容纳玩家人数很多的大场景中,这个对服务器来说压力是很大的。
(2)如果容纳玩家人数很少,比如十人以下的场景,倒可能是个简洁的方案。
对于上面第一种情况,比较流行的方案是九宫格法,简单,高效:
将地图按设定的格子大小划分为网格,如果玩家移动到某坐标,我们很容易地将玩家归入该坐标所属的网格G的玩家链中,而这个玩家的可见集可以简单地将以网格G为中心的九宫格(小范围)中的玩家链聚合而得到。而要获得两次移动间的可见集差异,也非难事。
(1)格子宽度通常比地图最小单位尺度要大很多,在要求精确距离触发的需求上,需要使用定时器等方案实现。
(2)大场景地图上,网格数量过多。
(3)在面临不同对象拥有不同AOI半径的需求,可能要引入其他方案夹杂完成。但若非必要,网格法还是极理想的AOI方案之一。
到这为止,对于大场景中玩家人数较多的情况,从切割场景区域划分多个AOI单元的方向,将压力分到多个AOI区域中,这对于场景中单位分布均匀且分散的情况,每个AOI区域中的玩家数量已经减少了很多,是一个很大的优化,但是,如果某一时间某个AOI单元中(小区域)大量聚合玩家这一问题,实际上还是会给服务器带来不小的压力。那么如何进一步优化呢?
提到碰撞检测,直觉上便联想到利用空间划分来剔除远距离物体的多余运算。显然,QuadTree/Octree/BSPTree等都是可用的方案。但对于MMORPG大量玩家的移动行为特殊性而言,使用Sweep and Prune方案更加高效,该想法最初由:
D. Baraff. Dynamic Simulation of Non-Penetrating Rigid Bodies.
PhD thesis, Cornell University, 1992 Paper提出,而
I-COLLIDE: An Interactive and Exact Collision Detection System for Large-Scale Environments, 1995 这个Paper则给出了一个改进的src实现。
这两个Paper都可以直接google得到。
Sweep and Prune的优点是对凸多边形的大规模实时小步长运动系统支持的很好,相对于传统空间划分方法的时间复杂度O(n * log2n),它几乎减低到O(n + s)
前提是场景中单位虽是无限制地移动,但移动步长相对短,利用这个短步长的特性,可以假设,每个单位在下一帧中的移动对整个排序好的interval N维链表来说,变化甚小,所以它的移动是基于前一个有序状态的小范围轴向比较,复杂度几乎线性可控。而MMORPG玩家就是小步长运动。跳转场景所导致的节点增加,删除是小概率事件。而如果连节点增加,删除这个Sweep and Prune的弱点也避免,也可以引入系列unrolled linked list(松散链表)来对interval链表来做分段,从而在删除和增加节点事件发生时,使用二分查找法定位到指定的unrolled link list来做可控链表段之间的增删。Sweep and Prune方案甚至可以演化为很有意思的多线程版本的方案。具体的实现可以参考:
Efficient Large-Scale Sweep and Prune Methods with AABB Insertion and Removal, 2008
但非海量单位,似乎杀鸡用牛刀了
比如,售卖NPC / 玩家 / 有AI自动攻击范围内玩家的怪物,这三种就区别对待,因为售卖NPC显然不需要看到任何单位,但是它需要被玩家看到。
而玩家需要看到所有物体;
而怪物则只需要看到玩家,不需要看到其他怪物,也不需要看到NPC。
区分特性的做法也很简单,针对各自的特性来筛选感兴趣的单位,引入一个简单的Interest机制即可。比如InterestMask,只需要一个 & 操作便可能跳过大量的后续处理;又比如使用更通用的InterestFilterCallback。
我们的MMORPG运行在一个非即时的网络环境下,公网上120~200ms的延时通常对玩家而言是可接受的,而我们在接受这个事实的同时也应该利用这个事实。虽然我们实现上可以完全实现移动的同时进行可见集的差异广播,但是,即使服务器每隔200ms向某个玩家同步一次可见集信息,在很多MMORPG的玩法上,也是可以接受的,而该方案下便可引入其他一些特别的优化了。
因为一个在线游戏中,玩家并不是理想地平均分布的,玩家集中的点大部分都受到玩法的影响,比如功能性NPC的周围、刷怪区域、摆摊区域等,上述等方法在这种密集玩家聚集的区域,算法时间复杂度都向O(n平方)靠拢。此时可以考虑达到一定的阈值区域特殊处理。
这个方法是在一个paper上看到的,具体做法是当我们要同步A物体的AOI信息时,对AOI范围内的物体按距离远近做出信息同步频率的区分;假设按远近对A的AOI范围物体分为近(可操作)、中(不可操作,但影响游戏感受)、远(不可操作,且不精确移动也无所谓)三级。近的物体是实时同步的/LOD高,中的物体按照较高频率同步/LOD中,远的物体则低频率同步/LOD低。物体移动时,原来的中、远距离物体会依次变成近的物体,所以逻辑上是准确无误的。但是可能会有一些跑动后的拉扯行为,如果觉得不可接受,引入了dead reckoning做行为预测。
(5)策划应该参与优化
列在最后但是并不是最不重要的。其实,无论怎么优化AOI算法,最有效的优化还是策划设置玩法上尽量避免单点(小区域)大量聚合玩家,单点聚合后接近O(n^2)是少不了的。非常不明智。所以大家也看到为什么很多MMORPG会分门派,每个门派的场景一般不同。程序的优化相对于策划的优化来说,是下策。
1.游戏地图上的单位(如,玩家、npc、怪物以及其他物品等),有移动、释放技能等等触发单位间互动的行为,我们叫做一个AOIEntity,每一个AOIEntity可以挂多个不同半径的AOI,每一个这样的AOI单元我们叫做AOINode,如此,AOIEntity拥有多个AOINode,然后每一个场景管理者AOIManager管理着多个这样的AOIEntity。
2.AOI 核心事件,进入和离开事件,基本由三种行为产生:进入场景,离开场景,场景中的移动,在AOIManager中统一管理,接口类似这样:
void AOIManager:Enter(AOIEntity *entity, cosnt Point& target_pos);
void AOIManager:Leave(AOIEntity *entity);
void AOIManager:Move(AOIEntity *entity, cosnt Point& target_pos);
3.添加一个AOINode的接口,主要参数是Id(用于标识这个AOI),半径,进出事件的callback函数:
void AOIEntity:AddNode(int aoi_id, float radius, AOICB enter_cb, AOICB leave_cb);
4.获取周围单位实体和观察者玩家实体集合的接口,这个可以在更上层,通过在响应进出事件的enter_cb, leave_cb中维护这样的集合。