Mosaic Game 的AOI
在mosaic_game中编写了一个单独的库来管理AOI,其项目地址在https://github.com/huangfeidian/aoi。这个库不仅实现了前述的三种AOI算法,统一了AOI系统的相关接口,而且针对业务系统的常规需求做了一些功能增强,其主要额外功能点包括:
-
支持一个
aoi_entity挂载多个radius,这个主要是光环技能系统引入的需求,因为一个entity上可能携带多个光环。此时引入了两个新的概念aoi_pos_entity和aoi_radius_entity:aoi_pos_entity代表一个带位置信息的对象,原来的aoi_entity概念被aoi_pos_entity替换aoi_radius_entity代表挂载在特定aoi_pos_entity上的一个AOI区域,内部负责维护此区域的AOI状态以及进出回调,一个aoi_pos_entity可以包含多个aoi_radius_entity
此时
aoi_entity->interest与aoi_entity->interested两个成员变量也被拆分为aoi_radius_entity->interest和aoi_pos_entity->interested。aoi_radius_entity->interest集合包含的类型是aoi_pos_entity*,对应的aoi_pos_entity->interested包含的类型是aoi_radius_entity。 -
支持
AOI区域的aoi_entity类型过滤,例如NPC的战斗半径区域只关心玩家类型。为此每个aoi_entity都会有一个uint64_t flag字段代表当前aoi_entity的类型标识,例如第0位代表是否是玩家类型,第1位代表是否是NPC类型等等。然后每个aoi_radius_entity在创建的时候都需要提供三个uint64_t flag来进行类型过滤:any_flag拥有其中任何一个flag都可以进入当前区域need_flag需要拥有其标记的所有flag才能进入当前区域forbid_flag拥有其中任何一个flag都会导致不能进入当前区域
此时判断一个
aoi_pos_entity能否进入一个aoi_radius_entity的flag判定逻辑如下:
bool aoi_radius_entity::check_flag(const aoi_pos_entity& other) const
{
auto other_flag = other.entity_flag();
if (other_flag & m_aoi_radius_ctrl.forbid_flag)
{
// 不能携带forbid flag里任何一个bit
return false;
}
if ((other_flag & m_aoi_radius_ctrl.need_flag) != m_aoi_radius_ctrl.need_flag)
{
// 需要有need flag里的所有bit
return false;
}
// 拥有any flag里的任何一个bit
return other_flag & m_aoi_radius_ctrl.any_flag;
}
- 增加
z轴高度限制,以适应三维空间。这个修改对于网格划分的AOI实现没有什么影响,只需要在原来的判定条件里增加一下z轴的判定即可。不过对于十字链表实现的AOI来说,需要引入第三个链表z_list。由于常规场景内的entity的z轴坐标基本都在一个很小的范围内,z_list属于一个集中聚集的状态,这样会导致维护z_list的代价很大。基于这个限制,十字链表的AOI算法在常规的3D场景中使用很少,只有在只考虑x,y的2D-AOI场景中有竞争力。 - 增加强制关注功能,这个主要是处理一些全局强制可见以及组队强制可见的
AOI逻辑。将一个aoi_pos_entity设置为被某个aoi_radius_entity强制关注之后,aoi_pos_entity的移动将不会触发这个aoi_radius_entity的进出回调。外部逻辑可以调用增加强制关注和取消强制关注这两个接口。 - 添加区域查询接口,用来查询指定区域内的
aoi_pos_entity,主要是应对一些技能系统的寻敌需求。这里的区域包括圆形、矩形、圆柱形、扇形这四个形状。 - 增加
interest集合数量限制功能,这个主要是配合客户端能接受的同步aoi_entity数量上限来使用的。 在mosaic_game中场景space_entity负责创建、销毁以及更新aoi::aoi_manager* m_aoi_manager,同时提供此变量的get方法。
aoi::aoi_manager* space_entity::aoi_mgr()
{
return m_aoi_manager;
}
const std::uint32_t m_aoi_update_gap = 200;
void space_entity::update_aoi()
{
m_aoi_manager->update();
m_aoi_update_timer = add_timer_with_gap(std::chrono::milliseconds(m_aoi_update_gap), [this]()
{
update_aoi();
});
}
然后每个actor_entity上都有一个actor_aoi_component负责暴露AOI相关接口并转发到space_entity::aoi_mgr()上,这里提供的接口主要是:
aoi::aoi_pos_idx m_aoi_pos_idx; //当前actor_entity注册到aoi_mgr之后返回的索引
std::unordered_map<std::string, aoi::aoi_radius_idx> m_aoi_radius_names; // 多个aoi_radius注册到aoi_mgr之后返回的索引
void add_force_aoi(const std::string& cur_radius_name, actor_entity* other_actor); // 添加强制AOI关注
void remove_force_aoi(const std::string& cur_radius_name, actor_entity* other_actor); // 取消强制AOI关注
// 添加一个aoi_radius 这里需要提供回调函数和radius的名字
bool add_aoi_radius(const aoi::aoi_radius_controller& cur_aoi_ctrl, std::function<void(actor_entity*, bool)> radius_cb, const std::string& radius_name);
// 取消一个aoi_radius的注册
void remove_aoi_radius(const std::string& radius_name);
// 当actor位置更新之后通知aoi系统
void update_aoi_pos();
// 查询圆形区域内的特定tag的actor_entity
std::vector<actor_entity*> entities_in_range(std::uint64_t entity_tag, float radius);
由于我们支持了单aoi_entity携带多个radius,所以增加一个radius的时候我们需要提供这个radius的名字和对应的进出AOI的回调radius_cb。对于有客户端的player_entity来说,会拥有一个player_aoi_component,这个component在进入场景时都会注册一个用来给客户端同步actor_entity的radius:
// void player_aoi_component::on_enter_space()
aoi::aoi_radius_controller cur_aoi_ctrl;
cur_aoi_ctrl.any_flag = std::numeric_limits<std::uint64_t>::max();
cur_aoi_ctrl.need_flag = (1ull <<std::uint8_t(enums::entity_flag::is_client_visible));
cur_aoi_ctrl.radius = 30;
cur_aoi_ctrl.min_height = cur_aoi_ctrl.max_height = 0; // 不关心高度坐标 只关心平面坐标
cur_aoi_ctrl.forbid_flag = 0;
cur_aoi_ctrl.max_interest_in = 30; // 最大接受30个 entity进入当前aoi
cur_actor_aoi_comp->add_aoi_radius(cur_aoi_ctrl, [this](actor_entity* other, bool is_enter)
{
if(is_enter)
{
on_aoi_enter(other);
}
else
{
on_aoi_leave(other->entity_id(), other->aoi_idx());
}
}, static_type_name());
在触发了on_aoi_enter之后,就会生成一个消息推送给客户端通知创建一个新的actor_entity,消息内会附带这个actor_entity的一些基本数据下去:
void player_aoi_component::on_aoi_enter(actor_entity* other_entity)
{
utility::rpc_msg aoi_enter_msg;
aoi_enter_msg.cmd = "notify_aoi_enter";
aoi_enter_msg.args.reserve(7);
aoi_enter_msg.args.push_back(other_entity->aoi_idx());
aoi_enter_msg.args.push_back(other_entity->entity_id());
aoi_enter_msg.args.push_back(other_entity->online_entity_id());
aoi_enter_msg.args.push_back(other_entity->m_base_desc.m_type_name);
aoi_enter_msg.args.push_back(other_entity->encode_with_flag(std::uint32_t(enums::encode_flags::other_client)));
aoi_enter_msg.args.push_back( other_entity->pos());
aoi_enter_msg.args.push_back( other_entity->yaw());
m_owner->sync_to_self_client(aoi_enter_msg); // 将此消息推送到当前player的客户端
if(!m_owner->get_space()->is_cell_space())
{
return;
}
m_other_sync_versions[other_entity->aoi_idx()] = other_entity->aoi_sync_version();
other_entity->get_component<actor_aoi_component>()->add_sync_player(m_player);
m_owner->dispatcher().dispatch(enums::aoi_event::enter, other_entity);
}
这里我们将此other_entity的aoi_idx也打包进去了,这样客户端接收到之后就可以建立起这个aoi_idx到新生成的client_actor的映射。on_aoi_enter除了把这个other_entity数据下发到客户端之外,还会将当前player_entity加入到other_entity->m_aoi_sync_players集合中,这样后续other_entity发生了一些客户端可见的状态变化时(例如血量 位置改变),就会将这个改变消息打包并广播到other_entity->m_aoi_sync_players对应的所有客户端连接:
void actor_entity::sync_to_others(const std::string& cmd, const std::vector<json>& args)
{
sync_to_others_without_aoi_data(enums::entity_packet::sync_aoi_rpc, *utility::rpc_msg::to_bytes(cmd, args));
}
void actor_entity::sync_to_others_without_aoi_data(enums::entity_packet entity_packet_cmd, const std::string& without_aoi_data)
{
auto cur_aoi_idx = aoi_idx();
std::shared_ptr<std::string> with_aoi_data = std::make_shared<std::string>(without_aoi_data.size() + sizeof(cur_aoi_idx), '0');
std::copy(reinterpret_cast<char*>(&cur_aoi_idx), reinterpret_cast<char*>(&cur_aoi_idx) + sizeof(cur_aoi_idx), with_aoi_data->begin());
std::copy(without_aoi_data.begin(), without_aoi_data.end(), with_aoi_data->begin() + sizeof(cur_aoi_idx));
sync_to_others_with_aoi_data(entity_packet_cmd, with_aoi_data);
}
void actor_entity::sync_to_others_with_aoi_data(enums::entity_packet entity_packet_cmd, std::shared_ptr<const std::string> data)
{
auto cur_ghost_comp = get_component<actor_ghost_component>();
for(auto one_pair: get_component<actor_aoi_component>()->aoi_sync_players())
{
one_pair.second->sync_from_other(cur_ghost_comp->sync_version(), aoi_idx(), entity_packet_cmd, data);
}
}
这里我们先把要广播的数据打包为一个shared_ptr<string>, string的开头两个字节填充当前actor_entity的aoi_idx(),这样接收到此信息的客户端通过开头的两个字节来查询之前notify_aoi_enter时客户端建立的映射就知道这个消息应该归哪个客户端client_actor处理。这里不使用actor_entity->online_id的原因是这个字段是8字节大小的,而aoi_idx只有两字节大小,更省流量。打包好消息之后,再遍历aoi_sync_players集合进行数据发送,由于我们传入的是shared_ptr<string>,所以多个连接之间可以共享这个要发送的消息,避免多个string实例,同时通过引用计数维护的生命周期也更适合异步网络发送的buffer管理。