AOI--MMO服务器开发技术点记录
对于AOI的详细原理,可以具体参考我这篇博客:AOI; 本篇文章我们的重心是放在如何将一个基本的AOI系统实现进我们的MMO服务器。
可以把一个AOI系统当作一个运行在服务器中的虚拟的场景,玩家实体在其中表现为一个AOI实体。AOI实体的结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct AOIEntity
{
uint32_t id;
Vector3 position;
Vector3 direction;
vector<int> interests; // 关注的实体列表 我关注谁
vector<int> observers; // 观察者列表 谁在关注我
};
同时还需要一个订阅通知结构来通知实体进入和离开场景
```cpp
struct AOINotification
{
enum class Type
{
Enter,
Leave
};
Type type;
AOIEntity entity;
};
struct AOIEntity
{
vector<AOINotification> notifications; // 订阅通知列表
};
AOI实体中有些数据是在同步时我们不需要的,可以单独抽象出一个AOI状态,表示当前实体的AOI状态:
1
2
3
4
5
6
7
8
9
10
struct AoiState {
int eid;
std::vector<int> interests;
std::vector<AoiNotification> notifies;
AOIState(AOIEntity& entity) : eid(entity.eid), interests(entity.interests.begin(), entity.interests.end()) {
notifies.swap(entity.notifications);
}
};
基于AOI系统的实现有多种算法,比如网格法,十字链表法,我们先设计个虚基类,表明AOI系统需要实现的基本能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AOI {
public:
virtual ~AOI();
// AOI 实体增删查改
virtual void enter(int eid, int x, int y, int radius) = 0;
virtual void leave(int eid) = 0;
virtual void update(int eid, int x, int y, int radius) = 0;
// interest 查询
virtual std::vector<int> query_interests(int eid) const = 0;
virtual std::vector<int> query_observers(int eid) const = 0;
virtual std::vector<int> query_entities(int eid, int x, int y, int radius) const = 0;
virtual std::vector<AOIState> _aoi_states() const = 0;
};
接下来讲讲AOI如何指导update中同步的对象选择。AOI实体的状态改变分为三个基本的类型:进入场景、离开场景和更新。
针对进入场景的实例,每一个AOI状态,会维护进入自己视野的和离开自己视野的实体列表。对于每个进入自己视野的实体,会获取全量的属性状态,封装在protobuf结构体中,并写入输出流发送给自己。
针对离开场景的实例,会告知每个AOI状态中离开自己视野的实体,客户端根据这些实体id在场景中移除对应的实体。
接下来是比较复杂的点,针对AOI状态中原本存在于自己视野中的实体的状态的更新,将置脏的属性状态数据写入到protobuf消息中,发送给对应的客户端。对于脏属性数据的序列化和反序列化,我在之前的文章中讲解过,这里就不多说。
广播和组播
AOI系统可以使广播和组播消息的开销变得较小,广播消息就不多说,就是获取所有的实体发送。如果想组播当某一实体所关注的实体,可以使用AOI系统获取这些实体的视野范围内的实体列表,然后只对这些实体进行消息发送。或者如果想组播关注某实体的所有实体,可以使用AOI系统获取该实体的观察者列表,然后对这些观察者进行消息发送。
实现
下面来具体讲下这样的AOI系统是如何实现的。大致的思路其实和AOI中的网格化优化的思路是一致的。
将整个场景划分为一个个大小相同的网格,每个网格内存放一个玩家列表。每个玩家只需要关注自己所在网格和周围网格内的玩家。这样可以大大减少遍历的范围。
进入场景时,找到玩家所在的网格,将玩家加入到网格内的玩家列表中。接下来的做法类似于上面,但是只需遍历九宫格内的玩家列表即可。
离开场景类似,不过多赘叙。
移动,各自内移动时,处理方式和上面一致,同样是遍历九宫格内的玩家列表。但是移动到新的网格时,需要把我从原网格的玩家列表中移除,并加入到新的网格的玩家列表中。
在以玩家移动为例子来讲下,从客户端发送的实体位置是如何处理组件和AOI系统中的状态同步的。首先,客户端会将玩家的位置信息发送给服务器,服务器接收到位置信息后,会获取对应服务器玩家实体中的移动组件来更新玩家实体的位置信息,这个更新函数中会调用AOI系统的AOI实体为位置的更新。
多线程与Aoi的update
AOI系统是多线程的,单独运行在一个线程当中。MMO服务器会单独开个独立的线程,不断的执行update函数,去处理AOI实体的状态更新和通知。update函数的作用很大,我们上面只是讲完了网格中每个玩家列表的更新,但是没有对AOI实体中的观察者和被观察者、通知列表进行更新,更新动作就是发生在这里的。
具体流程如下,对于每个实体,对找到自己所属的网格,并找到除了自己的其他同属一个网格的实体集合。根据之前的观察者列表,得到新加入的实体集合和退出的实体集合,加入到通知队列中。针对新加入的实体,将其的被观察者列表中加入自己;针对退出的实体,将其的被观察者列表中删除自己;同时更新自己的观察者列表为新的观察者列表。
注意使用共享指针去管理资源。