AOI
大约 3 分钟MMO游戏开发c++unity服务端
unity/c++ mmo大型多人在线游戏开发系列教程,本节代码在此。
在之前的实现里,我们已经实现了一个可以多人游玩的世界,角色在场景里移动、释放技能。但是,实现上依赖了对场景内所有玩家的遍历,在人数较多的情况下性能会非常差,本节我们来提供经典的AOI实现,以解决这一问题。

概念
以移动同步为例,我的移动数据并不需要同步给距离我几百米开外,根本看不到我的人,同时,我也不需要收到来自几百米开外我所看不到的玩家的移动数据。
这就需要场景可以提供一个接口,可以方便筛选出我所关心的玩家,以及关心我的玩家。
AOI(Area Of Interest)正是实现这个接口的基础,可以将其视为一个黑盒系统,我们将玩家、怪物、NPC等场景内的可观察对象都放到AOI系统中,并在对象位置变化时调用AOI系统提供的更新接口,就能很方便地通过AOI系统获取到某个Area中的所有对象。
AOI有不同的实现算法,各有优劣,我们先建立一个中间层:
struct TriggerNotify {
bool is_add;
std::vector<int> entities;
};
struct AOIState {
int eid;
// 我看到谁
std::vector<int> interests;
// trigger
std::vector<TriggerNotify> notifies;
AOIState(AOIEntity& entity) : eid(entity.eid), interests(entity.interests.begin(), entity.interests.end()) {
notifies.swap(entity.notifies);
}
};
class AOI : public std::enable_shared_from_this<AOI> {
public:
virtual ~AOI() {}
virtual void start() {}
virtual void stop() {}
virtual int add_entity(int eid, float x, float y, float radius) = 0;
virtual void del_entity(int eid) = 0;
virtual void update_entity(int eid, float x, float y) = 0;
// 获取aoi最新状态
virtual std::vector<AOIState> fetch_state() = 0;
// 获取在以(center_x, center_y)为圆心,radius为半径的圆内的所有对象
virtual std::vector<int> get_entities_in_circle(float center_x, float center_y, float radius) = 0;
// 获取eid所关心的所有对象
virtual std::vector<int> get_interests(int eid) = 0;
// 获取关心eid的所有对象
virtual std::vector<int> get_observers(int eid) = 0;
};
我们在space
中应用aoi,根据aoi来指导玩家数据的同步范围:
void Space::update()
{
// 获取最新的aoi状态
std::vector<AOIState> aoi_state = _aoi->fetch_state();
for (auto& state : aoi_state) {
int eid = state.eid;
Player* player = find_player(eid);
if (!player)
continue;
// 同步新进入或退出自己视野的玩家给player
// ...
// 同步视野内玩家的移动给player
space_service::PlayerMovements player_movements;
for (int interest_eid : state.interests) {
Player* p = find_player(interest_eid);
if (p) {
space_service::PlayerMovement* data = player_movements.add_datas();
data->set_eid(interest_eid);
space_service::Movement* new_move = data->mutable_data();
get_movement_data(p, new_move);
}
}
send_proto_msg(player->get_conn(), "sync_movement", player_movements);
}
}
可以看到,此时玩家只会收到自己能看到的其他人的移动数据。
然后,查找某个范围内的玩家,也可以能过aoi来先做一次初筛了:
std::vector<Player*> Space::find_players_in_circle(float cx, float cy, float r)
{
std::vector<Player*> players;
std::vector<int> eids = _aoi->get_entities_in_circle(cx, cy, r);
for (int eid : eids) {
auto iter = _eid_2_player.find(eid);
if (iter != _eid_2_player.end()) {
players.push_back(iter->second);
}
}
return players;
}
std::vector<Player*> Space::find_players_in_sector(float cx, float cy, float ux, float uy, float r, float theta)
{
std::vector<Player*> players = find_players_in_circle(cx, cy, r);
for (Player* player : _players) {
Vector3f pos = player->get_position();
if (is_point_in_sector(cx, cy, ux, uy, r, theta, pos.x, pos.z))
players.push_back(player);
}
return players;
}
最后,call_all
和call_others
两个接口从此变为只发送给看到自己的人:
void Space::call_all(int eid, const std::string& msg_name, const std::string& msg_bytes)
{
Player* player = find_player(eid);
if (player) {
send_raw_msg(player->get_conn(), msg_name, msg_bytes);
}
call_others(eid, msg_name, msg_bytes);
}
void Space::call_others(int eid, const std::string& msg_name, const std::string& msg_bytes)
{
std::vector<int> observers = _aoi->get_observers(eid);
for (int id : observers) {
Player* player = find_player(id);
if (player) {
send_raw_msg(player->get_conn(), msg_name, msg_bytes);
}
}
}
实现
具体的实现算法网上就有很多分析了,这里采用最基础的九宫格实现,不过是多线程版本的:
void GridAOI::start()
{
_aoi_thread = std::thread([ptr = shared_from_this()]() {
std::shared_ptr<GridAOI> grid_ptr = std::static_pointer_cast<GridAOI>(ptr);
grid_ptr->run();
});
}
void GridAOI::stop()
{
{
std::unique_lock<std::mutex> lock(_stop_flag_mutex);
_is_stop = true;
}
_cv.notify_all();
_aoi_thread.join();
}
简单写了个窗口程序验证一下结果的正确性,看起来问题不大:
