Post

Area Of Interest (AOI) 系统设计

Area Of Interest (AOI) 系统设计

玩家量少时,场景数据的同步可以简单的选择广播的方式,但是,当玩家数量超过一定数量级,广播可能会导致同步延迟增大。既然广播不行,那么我们选择组播。场景数据或玩家数据的自定义组播被称为AOI(Area Of Interest)技术。在服务端有单独的AOI系统来指导玩家数据的同步。

首先我们来思考下多个玩家同时存在在同一场景中,当前玩家如何实时看到视野内其他玩家的状态变化?场景内玩家数量比较少时,实时广播所有玩家的状态是比较简单方便的方法,出错率不高。但是场景内玩家数到达一定量级,如数百人,假设一个玩家的状态数据大小为1M,那么每次同步流量占用就达到数百M,仅仅是同步玩家状态就占用大量宽带,更别提存在其他的同步任务。这个方案显然是不合理的。

我们来思考下如何优化,在不影响游戏体验下,减少数据的同步量。不管是怎样的场景,当前玩家的视野不可能同时看到所有玩家,或者说不可能一直看到。那么玩家不关注的玩家是不是可以不用同步状态呢?对于每个玩家,划分两个集合:

  • 观察者集合:关注我的玩家集合,我的AOI行为需要向这个集合发送事件
  • 被观察者集合:我关注的玩家集合,用于主动向这个集合的玩家发送事件

这样玩家同步量就从全局划分到各个小的集合中,如何使用这两个集合实现AOI系统是我们接下来介绍的重点:

玩家视野

视野这个概念在AOI系统中是非常重要的。视野决定了两个集合更新的决策。影响两个集合更新的玩家操作大致可以分为以下几种:

  • 进入场景
  • 离开场景
  • 玩家状态更新
  • 玩家移动

玩家进入场景

遍历场景中所有对象,逐一比较它们和我的距离,如果对象在我的视野之内,则向我发送Enter(对象)事件,此时这些对象会加入我的被观察者集合,同时,我会加入到这些对象的观察者集合。

同样如果距离小于对象的视野,则向对象发送Enter(我)事件,此时我会加入到对象的被观察者集合,同时对象会加入到我的观察者集合。

玩家离开场景

对观察者集合内的所有玩家发送离开时间,观察者集合内的玩家接收到消息后,将我从它们的被观察者集合中移除。同时,我也需要将这些玩家从我的观察者集合中移除,就是我的被观察者集合会被清空。

玩家状态更新

玩家操作带来的状态更新,需要将状态发送到观察者集合中的玩家中去。

玩家移动

这是重点,也是视野方案下AOI系统的核心。玩家移动会触发其他玩家的视野的进入或离开。其实和进入场景的更新逻辑类似,可以把玩家的视野想象成一个个小型的扇形场景。通过判断是否在该区域内决定是进入视野还是离开视野。

移动先更新我和其他玩家的集合,在对位置状态进行同步,来避免不必要的流量浪费。

更新逻辑只这样的,从代码的角度看,以遍历的当前玩家为视角。

  • 玩家检查自己的观察者集合,如果对象不在我的视野内,则向对象发送Leave事件并从观察者集合中删除。

  • 玩家接受到Leave事件,将发送者从自己的被观察者集合中移除。

读者可能不理解这个行为,从单向来看,好像两个集合只更新了一个。但是离开视野是双向的,玩家A离开玩家B的视野,玩家B也会离开玩家A的视野。这个行为是双向的。所以两个集合到最后都会被更新。只需遍历所有的玩家,并执行上面的操作即可。

更新完集合后,向剩下的观察者集合中发送移动事件。

移动后,可能会有新的玩家进入我的视野。检查逻辑和离开类似,但是是反向的。

  • 检查自己的观察者集合,如果玩家在视野内,但不在观察者集合中,则向玩家发送Enter事件,并将玩家加入到观察者集合中。
  • 玩家接受到Enter事件,将发送者加入到自己的被观察者集合中。

这个方案仍然存在优化点:在判断是否需要发送Enter或Leave事件时,会对所有玩家进行遍历,如果玩家数量较多,这个遍历会导致性能问题。

网格化优化

将整个场景划分为一个个大小相同的网格,每个网格内存放一个玩家列表。每个玩家只需要关注自己所在网格和周围网格内的玩家。这样可以大大减少遍历的范围。

进入场景时,找到玩家所在的网格,将玩家加入到网格内的玩家列表中。接下来的做法类似于上面,但是只需遍历九宫格内的玩家列表即可。

离开场景类似,不过多赘叙。

移动,各自内移动时,处理方式和上面一致,同样是遍历九宫格内的玩家列表。但是移动到新的网格时,需要把我从原网格的玩家列表中移除,并加入到新的网格的玩家列表中。

如果玩家是均匀分布在所有网格内,这样的优化可以大大减少遍历的范围,提升性能。但是如果存在热点网格,同样会出现上面的问题。

那如果需求中会存在热点网格的情况(如存在主城),我们又该如何优化呢?

这里需要做balance,前面玩家的视野是实时变化的扇形区域,现在将玩家的视野固定为九宫格,即一个小型的上帝视角。这样每个玩家就不必维护两个集合了,逻辑也会简化许多。

状态更新逻辑是这样的:

  • 进入场景:只通知周围9格的对象(发送Enter(我)),同时通知自己(发送Enter(对象))。
  • 离开场景:只通知周围9格的对象(发送Leave(我))。

移动事件的分情况处理:关键优化在于区分是否跨格子,避免全量更新。

  • 未跨格子移动(位置变化但仍在同一格子内):只向周围9格对象发送移动事件。

  • 跨格子移动(位置变化导致格子切换):{OldGrid - NewGrid}:旧位置周围9格中,但不在新位置周围9格的对象集合(即“失去兴趣”的对象)。向这些对象发送Leave(我)事件(通知它们我离开了)。同时向我(移动对象)发送Leave(对象)事件(通知我这些对象离开了我的兴趣区域)。{NewGrid - OldGrid}:新位置周围9格中,但不在旧位置周围9格的对象集合(即“新兴趣”的对象)。向这些对象发送Enter(我)事件(通知它们我进入了)。同时向我发送Enter(对象)事件(通知我这些对象进入了我的兴趣区域)。{NewGrid * OldGrid}:新旧位置周围9格的交集对象集合(即“持续兴趣”的对象)。向它们发送移动事件(仅位置更新,兴趣区域未变)。

使用网格化来优化AOI的思路大致是这样,还有别的优化思路,比如十字链表等等。这些会在后续文章中介绍。

This post is licensed under CC BY 4.0 by the author.