AOI (Area Of Interest) 算法应该算是游戏的基础核心了,AOI 进出事件会触发很多的业务事件。简单来说,地图上的每个玩家都在一个 AOI 区域内,当玩家状态发生改变时 (如移动,动作行为等) ,需要该将玩家的信息广播给 AOI 区域内的所有玩家,而这些玩家收到这条广播消息时,就要做出对应的响应状态。
以 25x25 的地图为例,地图上的每个点表示一个玩家,假设目标玩家所在的位置为坐标 (16, 9) :
以玩家的进入游戏,移动,离开游戏设计接口:
1 | type AOIStrategyManager interface { |
全局查找
这是最简单的方案,我们可以获取到地图坐标中的所有玩家,然后判断每个玩家是否在目标玩家的通知范围内,如果在的话,就对该玩家发送消息。
假设我们设定的范围为 10x10,那么浅黄色范围中的所有玩家都是我们需要通知的:
判断距离的方式也很简单:|x-16|<=5 && |y-9|<=5
实现示例(golang)
1 | // 地图大小25x25:x坐标范围[0,25],y坐标范围[0,25] |
九宫格
这种方案是将整张地图按照格子划分,每个格子里记录了处于该格子内的所有玩家列表,当玩家状态改变时,需要通知该玩家所在格子以及周围最多8个格子内的玩家。在这种情况下,我们可以避免每次都循环全部玩家。
假设我们设定的格子为 5x5,那么整张地图共可以分成 25个格子,绿色格子为目标玩家所在格子,而浅绿色格子为周围8个格子,所有需要通知的玩家处于这9个格子中:
以玩家移动,进入,离开为例:
进入:根据目标玩家坐标计算出玩家所在格子,将该玩家添加到该格子中,并且将消息发送给九宫格内的玩家
离开:根据目标玩家坐标计算出玩家所在格子,将该玩家从该格子中删除,并且将消息发送给九宫格内的玩家
移动:
- 在本格子内移动:对九宫格内的玩家发送移动消息
- 从格子A移动到格子B:假设玩家移动到了 (14, 9) (如下图),这种情况下,玩家从格子8移动到了格子7,那么需要对格子7,8,12,13,2,3内的玩家发送移动消息,对格子11,6,1内的玩家发送进入视野消息,而对格子14,9,4内的玩家发送离开视野消息
代码示例(golang)
1 | // 地图大小25x25:x坐标范围[0,25],y坐标范围[0,25] |
灯塔兴趣列表
这种方案可以看做是九宫格方案的扩展。以九宫格为例,在每个九宫格中心建立一个灯塔,灯塔需要维护该灯塔范围内的所有玩家,以及对该灯塔感兴趣的玩家,同时每个玩家本身也会维护一个需要通知的玩家列表,可以看出来,对比九宫格,少了计算九宫格内的所有玩家的操作,这是一种以空间换时间的方式。
每个橙色的点表示一个灯塔,管理范围为每个5x5的格子(绿色区域),而玩家视野范围为10x10(浅黄色区域),可以看到每个玩家最多被4个灯塔观察,而浅绿色区域则为4个灯塔的观察者所在区域集合:
以玩家移动,进入,离开为例:
- 进入:根据目标玩家坐标计算出玩家所属灯塔,将玩家添加到该灯塔管理的玩家列表,如果灯塔上绑定了观察者,则通知观察者有玩家进入。然后找出玩家视野内的所有灯塔,将自身绑定为灯塔的观察者,同时将灯塔的观察者列表发送给玩家,添加至玩家维护的玩家列表。
- 离开:根据目标玩家坐标计算出玩家所属灯塔,将玩家从该灯塔管理的玩家列表中删除,如果灯塔上绑定了观察者,则通知观察者有玩家离开。然后找出玩家视野内的所有灯塔,解除灯塔的观察者绑定,同时将灯塔的观察者列表发送给玩家,从玩家维护的玩家列表中移除。
- 移动:
- 玩家视野内的灯塔没有变动:不做灯塔操作,对玩家维护的玩家列表发送移动消息
- 玩家视野内的灯塔发生变动:对旧灯塔执行对象离开逻辑,对新灯塔执行对象进入逻辑,前后视野交集内的灯塔不需要执行解绑和绑定操作,仅发送移动消息
可以看到灯塔的视野越小,在碰撞检测时过滤的无效对象就越多,整张地图灯塔的数目也就越多,消耗的内存就越大,而且对象进出灯塔的计算量就越多,而灯塔的视野越大,在碰撞检测时能过滤的无效对象就越少,如果只有一个灯塔的情况,那就是全局查找方式了。
云风的博客中还提到了另一种优化方式,使用六边形灯塔代替方形灯塔,在这种情况下,每个玩家最多只会被3个灯塔观察。
代码示例(golang)
1 | // 地图大小25x25:x坐标范围[0,25],y坐标范围[0,25] |
十字链表
所谓十字链表,即把地图坐标轴中的 X 和 Y 轴看成是2个链表,将玩家的 X 坐标按照从小到大插入 X 链表,将玩家的 Y 坐标按照从小到大插入 Y 链表,查询时根据玩家的坐标分别从2个链表中取出范围内的所有玩家,对两个玩家列表做交集,即为我们需要发送消息的玩家列表。
以玩家A(10, 3),玩家B(4, 18),玩家C(17, 24) 为例,则 X 链表为:B(4) - A(10) - C(17),Y 链表为:A(3) - B(18) - C(24),假设目标玩家D的坐标为(16, 9),通知范围为7:根据D坐标分别查找 X 链表和 Y 链表,然后根据对应坐标向前向后查找范围在7之内的玩家( X 链表中查找到的玩家为AC,Y 链表中查找到的玩家 A ),然后对两个玩家集合取交集(结果为玩家 A ),然后对交集中的所有的玩家发送消息。
代码示例(golang)
1 | // 地图大小25x25:x坐标范围[0,25],y坐标范围[0,25] |