Mosaic Game 的场景管理
场景space是游戏中玩家之间绝大部分玩法活动的逻辑承载空间,特别是玩家间的强实时互动。脱离了场景这个实体的话,游戏服务器的逻辑就只剩下服务service承载的聊天、好友之类的弱实时互动,这样就退化成为了普通互联网服务器的样子。因此强实时场景的存在是游戏服务器和互联网服务器之间最大的差异。在游戏服务器中,一般会有一个场景服务space_service来集中管理所有space的生命周期,同时管理玩家进出场景相关流程。
场景创建流程
游戏中的场景是多种多样的,策划一般会以一个场景编号space_no来方便的区分各个不同的场景。在不同的数据表中,通过使用相同的space_no来配置同一个场景的各种相关数据, 这个space_no就充当了场景配置索引的作用。同时游戏里可以为同一个蓝本的场景建立各自独立的实例,所以在场景管理中,一般会使用唯一的索引space_id来标识服务器内的单个具体场景实例。因此在创建场景实体space_entity的时候,需要提供space_no与space_id:
void Meta(rpc) request_create_space(const utility::rpc_msg& data, std::uint32_t space_no, const std::string& space_id, const std::string& pref_game_id, const json::object_t& init_info);
由于这两个参数是外部传入的,所以有可能是非法的,例如不存在的space_no或者重复的space_id,所以这个函数的开头要做一堆的合法性检查:
if (!m_space_config_data)
{
m_logger->error("fail to create space {} m_space_config_data null ", space_no);
reply_msg.err = "m_space_config_data null";
break;
}
cur_space_sysd = m_space_config_data->get_row(space_no);
if (!cur_space_sysd.valid())
{
m_logger->error("fail to create space {} invalid space no ", space_no);
reply_msg.err = "invalid space no";
break;
}
if (!cur_space_sysd.expect_value(std::string("space_type"), cur_space_type_no))
{
m_logger->error("fail to create space {} space_type empty ", space_no);
reply_msg.err = "invalid space_no";
break;
}
cur_space_type_info = misc::space_type_info_mgr::get_space_type_info(cur_space_type_no);
if (!cur_space_type_info)
{
m_logger->error("fail to create space {} space type info empty for space type {}", space_no, cur_space_type_no);
reply_msg.err = "invalid space_no";
break;
}
if (!cur_space_sysd.expect_value(std::string("map_range"), map_range))
{
m_logger->error("fail to create space {} map_range empty ", space_no);
reply_msg.err = "invalid space_no";
break;
}
if(space_id.empty())
{
m_logger->error("fail to create space {} space_id empty ");
reply_msg.err = "duplicated space_id";
break;
}
if (m_space_types.find(space_id) != m_space_types.end())
{
m_logger->error("fail to create space {} duplicated space id ", space_id);
reply_msg.err = "duplicated space_id";
break;
}
在space_service上使用了一个map来记录每个已经创建了space的space_id对应场景类型信息space_type_info:
struct space_type_info
{
union
{
struct
{
std::uint32_t is_union_space:1; //是否是大世界可分块场景
std::uint32_t is_town_space:1; // 是否是城镇场景
std::uint32_t is_player_dungeon:1; // 是否是单人副本
std::uint32_t is_team_dungeon:1; //是否是组队副本
std::uint32_t is_match_space:1; // 是否是匹配场景
std::uint32_t auto_select_when_empty_id:1; // 空space_id进入时自动选择负载最低的instance
std::uint32_t auto_create_new_heavy_load:1; // 高负载下自动创建新场景
std::uint32_t support_back_return:1; // 是否支持离开后再回来
};
std::uint32_t all_flags = 0;
};
std::uint32_t space_type; // 场景类型
std::uint32_t max_player_load; // 单场景最大玩家数量
};
std::unordered_map<std::string, const misc::space_type_info*> m_space_types;
这里并没有存储space_id到space_no的直接映射,因为所有场景相关接口里都会同时提供space_id,space_no这两个参数,校验这两个参数是否匹配可以通过space_service上的m_spaces_by_no字段来实现,这个字段存储了space_no到space_id集合的映射:
std::unordered_map<std::uint32_t, std::unordered_set<std::string>> m_spaces_by_no;
request_create_space里还有一个非常重要的参数pref_game_id,这个参数代表要将这个场景创建在哪一个进程上,如果没有指定的话,space_service会使用自己的负载均衡策略来选择一个合适的game_id来做填充:
if (dest_game_id.empty())
{
dest_game_id = choose_game_for_space(space_no, cur_space_sysd);
if (dest_game_id.empty())
{
m_logger->error("fail to choose game for space {}", space_no);
reply_msg.err = "cant find allocate game_id";
break;
}
}
else
{
auto cur_game_iter = m_game_loads.find(dest_game_id);
if (cur_game_iter == m_game_loads.end())
{
m_logger->error("fail to find game {} to create space {}", dest_game_id, space_no);
reply_msg.err = "invalid game_id";
break;
}
}
目前的负载均衡策略非常的简陋,直接选取当前负载最低的进程作为结果返回,这里对于正在创建过程中的场景增加10的权重,以避免场景负载延迟上报的影响:
std::string space_service::choose_game_for_space(std::uint32_t space_no, typed_matrix::typed_row space_sysd)
{
float temp_min_load = 10000.0f;
std::string min_game_id;
for (const auto &one_game_info : m_game_loads)
{
auto cur_game_load = one_game_info.second.cur_load + 10.0*one_game_info.second.creating_spaces.size();
if (cur_game_load < temp_min_load)
{
min_game_id = one_game_info.first;
temp_min_load = cur_game_load;
}
}
return min_game_id;
}
在所有的合法性检查都通过之后,开始通知目标进程来创建这个场景:
std::string space_service::do_create_space(std::uint32_t cur_space_no, std::uint32_t cur_space_type, const std::string& pref_space_id, const std::string& dest_game_id, const json::object_t &init_info)
{
auto cur_space_sysd = m_space_config_data->get_row(cur_space_no);
auto cur_game_id = dest_game_id;
if(cur_game_id.empty())
{
choose_game_for_space(cur_space_no, cur_space_sysd);
}
auto cur_space_id = pref_space_id;
if(pref_space_id.empty())
{
cur_space_id = get_server()->gen_unique_str();
}
m_logger->info("try create space no {} with id {}", cur_space_no, cur_space_id);
auto cur_space_type_info = misc::space_type_info_mgr::get_space_type_info(cur_space_type);
std::string cell_space_id;
std::string union_space_id;
json::object_t space_init_info = init_info;
utility::rpc_msg cur_msg;
cur_msg.cmd = "notify_create_space";
cur_msg.args.reserve(10);
if(cur_space_type_info->is_union_space)
{
cell_space_id = get_server()->gen_unique_str();
union_space_id = cur_space_id;
std::array<std::array<double, 3>, 2> map_range;
if(!cur_space_sysd.expect_value(std::string("map_range"), map_range))
{
return {};
}
utility::cell_region::cell_bound cur_map_range;
cur_map_range.left_x = map_range[0][0];
cur_map_range.right_x = map_range[1][0];
cur_map_range.low_z = map_range[0][2];
cur_map_range.high_z = map_range[1][2];
std::unique_ptr<union_space_info> cur_union_space_ptr = std::make_unique<union_space_info>(union_space_id, cur_map_range, cur_space_no, cur_game_id, cell_space_id);
space_init_info["components"]["cell"] = cur_union_space_ptr->cells.encode();
m_union_spaces[union_space_id] = std::move(cur_union_space_ptr);
m_space_types[union_space_id] = cur_space_type_info;
m_logger->info("create cell {} for union space {}", cell_space_id, union_space_id);
}
else
{
cell_space_id = cur_space_id;
}
add_space_load_to_game(cur_space_no, cell_space_id, cur_game_id, union_space_id, cur_space_type_info);
cur_msg.args.push_back(cell_space_id);
cur_msg.args.push_back(cur_space_no);
cur_msg.args.push_back(union_space_id);
cur_msg.args.push_back(space_init_info);
call_space_manager(cur_game_id, cur_msg);
return cell_space_id;
}
这里的add_space_load_to_game会将这个场景加入到此game的正在创建场景集合creating_spaces中,作为一个临时占位的场景负载。因为进程的负载是定期采样的,如果在采样间隔内某个进程是负载最低的,负载均衡策略会将这一期间的所有场景都创建在同一进程上,从而导致此进程负载爆炸。所以为了避免出现短期内单一进程创建的场景太多,优化负载均衡:
void space_service::add_space_load_to_game(std::uint32_t space_no, const std::string &space_id, const std::string &game_id, const std::string &union_space_id, const misc::space_type_info *cur_space_type_info)
{
if (union_space_id.empty())
{
std::unique_ptr<mono_space_info> cur_space_info = std::make_unique<mono_space_info>();
cur_space_info->game_id = game_id;
cur_space_info->space_id = space_id;
cur_space_info->space_no = space_no;
m_mono_spaces[space_id] = std::move(cur_space_info);
m_game_loads[game_id].mono_spaces.insert(space_id);
m_spaces_by_no[space_no].insert(space_id);
}
else
{
// 省略无关代码
}
m_space_types[space_id] = cur_space_type_info;
m_game_loads[game_id].creating_spaces.insert(space_id);
}
在选择最佳game的时候会将这里creating_spaces所带来的预先负载设置为了常量10,无视了场景间的差异,其实更好的方法是在场景表中配置每个space_no对应的预先负载,这样就能更加精确的估算。
通知指定space_server进程创建新space的方式是往这个space_server上的space_manager发送创建场景的请求notify_create_space:
void space_service::call_space_manager(const std::string &game_id, const utility::rpc_msg &msg)
{
get_server()->call_server(this, game_id + utility::rpc_anchor::seperator + "space_manager", msg);
}
这里的space_manager是每个space_server上都会存在的一个单例,space_server启动的时候就会自动初始化:
void manager_base::init_managers(space_server* in_space_server)
{
offline_msg_manager::instance().init(in_space_server);
email_manager::instance().init(in_space_server);
notify_manager::instance().init(in_space_server);
rank_manager::instance().init(in_space_server);
space_manager::instance().init(in_space_server);
}
void space_server::do_start()
{
entity::entity_manager::instance().init();
json_stub::start();
manager_base::init_managers(this);
misc::stuff_utils::init();
global_config_mgr::instance();
}
因此可以使用game_id + utility::rpc_anchor::seperator + "space_manager"的形式来拼接出对应的远程调用地址,因为在entity_manager没有处理这个rpc的情况下,会再往manager_base上尝试分发:
utility::rpc_msg::call_result manager_base::dispatch_rpc(const std::string& dest, const utility::rpc_msg& msg)
{
auto temp_iter = m_managers.find(dest);
if(temp_iter == m_managers.end())
{
return utility::rpc_msg::call_result::dest_not_found;
}
if(!temp_iter->second->support_rpc())
{
return utility::rpc_msg::call_result::rpc_not_found;
}
return temp_iter->second->rpc_owner_on_rpc(msg);
}
utility::rpc_msg::call_result space_server::on_server_rpc_msg(const std::string& dest, const utility::rpc_msg& cur_rpc_msg)
{
auto dispatch_result = entity::entity_manager::instance().dispatch_rpc_msg(dest, cur_rpc_msg);
if(dispatch_result != utility::rpc_msg::call_result::dest_not_found)
{
return dispatch_result;
}
return manager_base::dispatch_rpc(dest, cur_rpc_msg);
}
在space_manager接收到创建新场景的rpc请求之后,就会通过entity_manager来使用指定的参数来创建对应的space_entity:
void space_manager::notify_create_space(const utility::rpc_msg &data, const std::string &space_id, std::uint32_t space_no, const std::string& union_space_id, json::object_t &init_info)
{
m_logger->info("notify_create_space space_id {} space_no {} union_space_id {} init_info {}", space_id, space_no, union_space_id, json(init_info).dump());
std::string create_entity_error;
init_info["space_no"] = space_no;
init_info["union_space_id"] = union_space_id;
auto cur_entity = m_server->create_entity("space_entity", space_id, m_server->gen_online_entity_id(), init_info, create_entity_error);
if (!cur_entity)
{
m_logger->error("fail to create_space id {} with error {}", space_id, create_entity_error);
return;
}
entity::space_entity *cur_space = dynamic_cast<entity::space_entity*>(cur_entity);
m_spaces[space_id] = cur_space;
report_space_created(space_id);
}
void space_manager::report_space_created(const std::string &space_id)
{
utility::rpc_msg cur_msg;
cur_msg.cmd = "report_space_created";
cur_msg.args.push_back(m_server->local_stub_info().name);
cur_msg.args.push_back(space_id);
m_server->call_service( "space_service", cur_msg);
}
创建完成之后再通知space_service,可以走后续流程了。这个后续流程主要是将这个space的状态切换为ready,然后处理等待进入当前场景的所有玩家与队伍,逐个通知其可以进入:
void space_service::report_space_created(const utility::rpc_msg &data, const std::string &game_id, const std::string &space_id)
{
auto cur_space_type_iter = m_space_types.find(space_id);
if (cur_space_type_iter == m_space_types.end())
{
m_logger->error("cant find space {} report space created", space_id);
return;
}
if (!cur_space_type_iter->second->is_union_space)
{
auto cur_space_iter = m_mono_spaces.find(space_id);
if (cur_space_iter->second->game_id != game_id)
{
m_logger->error("space {} game id {} not match", space_id, game_id);
return;
}
if (cur_space_iter->second->ready)
{
m_logger->error("space {} no {} game id {} already ready", space_id, cur_space_iter->second->space_no, game_id);
return;
}
cur_space_iter->second->ready = true;
for (const auto &one_pair : cur_space_iter->second->players)
{
// 之前在等待进入场景的玩家 现在重新开始进入场景
utility::rpc_msg cur_msg;
cur_msg.args.push_back(game_id);
cur_msg.args.push_back(cur_space_iter->second->space_no);
cur_msg.args.push_back(space_id);
cur_msg.args.push_back(std::string());
cur_msg.args.push_back(one_pair.second.enter_info);
cur_msg.cmd = "reply_enter_space";
get_server()->call_server(this, one_pair.second.call_anchor, cur_msg);
}
if (!cur_space_iter->second->team_id.empty())
{
utility::rpc_msg team_forward_msg;
team_forward_msg.cmd = "notify_team_dungeon_created";
team_forward_msg.set_args(cur_space_iter->second->team_id, cur_space_iter->second->space_no, space_id, game_id);
get_server()->call_service("team_service", team_forward_msg);
}
}
return;
}
了解了完整的场景创建流程之后,我们还需要明确场景的创建时机。常规的场景创建时机为按需创建,这种模式主要处理的是单人场景以及组队场景。每次玩家发起进入场景请求时,space_service会以这个场景编号space_no和场景实例space_id开启新场景实例的创建,同时将这个玩家记录在此创建中场景的等待进入玩家列表中。这个场景需要创建在游戏服务器另外的场景进程上,当这个场景创建完成之后会通知回space_service这个新场景实例已经可用,此时通知这个场景等待列表的玩家场景进入得到允许,可以迁入此场景。
在这种模式下,玩家进入一个场景的延迟会比较高,因为场景创建是一个比较消耗CPU的操作,需要加载很多资源和配置表格数据。特别是这个场景比较大,依赖的资源与数据非常多的情况下,玩家进入指定场景的延迟可能会有数秒。所以一般对于主城等大场景,采取的是预先创建的模式,在服务器启动的时候就创建好一定数量的常用场景,这样玩家进入这些场景的时候就可以避免巨大的等待延迟,达到秒切的目的。space_service::init中的代码就是为这个预先创建常用场景服务的,会遍历场景表里的每行配置数据,如果发现配置数据里开启了自动选择同编号随机场景auto_select_when_empty_id的功能,则会开启一个定时器来渐进的创建这些场景:
bool space_service::init(const json::object_t& data)
{
if(!base_service::init(data))
{
return false;
}
auto cur_data_mgr = utility::typed_matrix_data_manager::instance();
if(!cur_data_mgr)
{
return false;
}
m_space_config_data = cur_data_mgr->get("space");
if (!m_space_config_data)
{
m_logger->error("cant get data config for space");
return false;
}
auto space_no_column = m_space_config_data->get_column_idx("space_no");
auto space_type_column = m_space_config_data->get_column_idx("space_type");
if(!space_no_column.valid() || ! space_type_column.valid())
{
return false;
}
auto temp_row = m_space_config_data->begin_row();
while(temp_row.valid())
{
auto cur_row = temp_row;
temp_row = m_space_config_data->next_row(temp_row);
std::uint32_t cur_space_no;
std::uint32_t cur_space_type;
if(!cur_row.expect_value(space_no_column, cur_space_no))
{
continue;
}
if(!cur_row.expect_value(space_type_column, cur_space_type))
{
continue;
}
auto cur_space_type_info = misc::space_type_info_mgr::get_space_type_info(cur_space_type);
if(!cur_space_type_info)
{
continue;
}
if(cur_space_type_info->auto_select_when_empty_id)
{
m_init_spaces_to_create.push_back(std::make_pair(cur_space_no, cur_space_type));
}
}
m_logger->info("init space is {}", serialize::encode(m_init_spaces_to_create).dump());
add_timer_with_gap(std::chrono::milliseconds(5 * 1000), [=]()
{
create_init_spaces();
});
report_ready();
return true;
}
这里加一个计时器是为了等待所有的space_server进程注册过来,避免创建场景时找不到可用的space_server进程。极端情况下,这个5s的超时过后space_server进程还没有注册过来,此时需要继续等待:
void space_service::create_init_spaces()
{
if(m_game_loads.empty())
{
add_timer_with_gap(std::chrono::milliseconds(5 * 1000), [=]()
{
create_init_spaces();
});
return;
}
for(const auto& one_pair: m_init_spaces_to_create)
{
do_create_space(one_pair.first, one_pair.second, std::string{});
}
m_init_spaces_to_create.clear();
}
上面的实现其实也有很大的问题,如果只有一部分的space_server注册过来,这些预创建的场景的压力就全都在少数的space_server进程上了。正常的做法是限定一个space_server的最大负载。如果大于此负载,则后续场景不再创建,继续开启计时器等待,在下次create_init_spaces时继续寻找合适的space_server去消耗m_init_spaces_to_create中剩下的场景。
实际的项目中单space_no的预先创建场景数量并不永远是1,因为单个进程单个场景里的人数承载是有限的,为了处理大量的玩家,一般会给每个space_no来指定需要预先创建的场景的个数,这里的实现只是为了偷懒。
此外预先创建并不只是在服务器启动的时候去执行,还可以在某些需要大量的小场景创建的活动开始之前执行。例如游戏每周五晚上八点会开启某种1v1的匹配活动,每组人员匹配成功之后都会进入一个专属的小场景中进行决斗。由于这个玩法的奖励丰厚,导致参与的玩家非常的多,在八点之后的瞬间就会创建巨量的小场景,此时全服的CPU都会有一个非常明显的上升,出现长时间的卡顿。为了解决这个卡顿问题,我们采取了每周五晚上七点开始慢慢的每隔10s创建一个小场景实例,直到九点钟此玩法结束。这样的慢慢预先创建就达到了削峰的作用,从而解决了瞬间批量创建导致的长时间卡顿。
上面介绍的就是预先创建场景的流程,实际情况下可能会出现玩家人数太多导致场景太拥挤,负载太高的问题。此时我们需要做定期的基于负载均衡的场景数量扩张,如果场景配置数据里开启了auto_create_new_heavy_load的话就会自动执行此流程。这个流程的入口在check_heavy_load_auto_create函数中,这个函数会定期的扫描这些开启了自动扩容的场景里的平均人数负载,如果大于了80%则会自动的创建一个新实例。space_service在初始化的时候会收集这些类型的space,并存储到m_check_load_create_spaces:
bool space_service::init(const json::object_t& data)
{
// 省略很多代码
auto temp_row = m_space_config_data->begin_row();
while(temp_row.valid())
{
auto cur_row = temp_row;
temp_row = m_space_config_data->next_row(temp_row);
std::uint32_t cur_space_no;
std::uint32_t cur_space_type;
if(!cur_row.expect_value(space_no_column, cur_space_no))
{
continue;
}
if(!cur_row.expect_value(space_type_column, cur_space_type))
{
continue;
}
auto cur_space_type_info = misc::space_type_info_mgr::get_space_type_info(cur_space_type);
if(!cur_space_type_info)
{
continue;
}
if(cur_space_type_info->auto_create_new_heavy_load)
{
m_check_load_create_spaces.push_back(std::make_pair(cur_space_no, cur_space_type));
}
}
m_logger->info("auto crate space is {}", serialize::encode(m_check_load_create_spaces).dump())
add_timer_with_gap(std::chrono::milliseconds(5 * 1000), [=]()
{
check_heavy_load_auto_create();
});
report_ready();
return true;
}
void space_service::check_heavy_load_auto_create()
{
// 会定期的扫描这些开启了自动扩容的场景里的平均人数负载,如果大于了`80%`则会自动的创建一个新实例:
add_timer_with_gap(std::chrono::milliseconds(5 * 1000), [=]()
{
check_heavy_load_auto_create();
});
std::vector<std::pair<std::uint32_t, std::uint32_t>> need_create_spaces;
for(auto [cur_space_no, cur_space_type]: m_check_load_create_spaces)
{
auto temp_iter = m_spaces_by_no.find(cur_space_no);
if(temp_iter == m_spaces_by_no.end())
{
continue;
}
const auto& cur_space_ids = temp_iter->second;
int space_instance_count = 0;
int space_player_count = 0;
for(const auto& one_space_id: cur_space_ids)
{
auto cur_mono_space_instance_iter = m_mono_spaces.find(one_space_id);
if(cur_mono_space_instance_iter != m_mono_spaces.end())
{
space_instance_count++;
space_player_count += cur_mono_space_instance_iter->second->players.size();
}
}
auto cur_space_type_info = misc::space_type_info_mgr::get_space_type_info(cur_space_type);
if(cur_space_type_info && space_player_count > (space_instance_count * cur_space_type_info->max_player_load) * 0.8)
{
need_create_spaces.push_back(std::make_pair(cur_space_no, cur_space_type));
}
}
m_logger->info("check_heavy_load_auto_create with result {}", serialize::encode(need_create_spaces).dump());
for(auto one_space_pair: need_create_spaces)
{
do_create_space(one_space_pair.first, one_space_pair.second, std::string{}, std::string{}, json::object_t{});
}
}
场景内实体创建流程
场景创建结束之后这个space_entity就根据自己的space_no对应的配置文件来开启自身独特的逻辑。由于场景作为游戏活动的主要承载容器,根据玩法的不同会执行各种不同的逻辑,因此这些逻辑都放在了space_entity的组件space_component上,space_entity在创建的时候会顺带的初始化这些component:
// bool space_entity::init(const json::object_t& data)
json::object_t components_data;
auto components_data_iter = data.find("components");
if(components_data_iter != data.end())
{
try
{
components_data_iter->second.get_to(components_data);
}
catch(const std::exception& e)
{
m_logger->error("components data not map");
return false;
}
}
if(!add_components<
space_cell_component,
space_navi_component,
space_event_component,
space_spawn_component,
space_match_component,
space_quest_component
>(components_data))
{
m_logger->error("fail to add components ");
return false;
}
其中最基础的逻辑就是创建场景中的所有entity,因为一个空荡荡的场景实在是毫无可玩性,这部分逻辑由space_spawn_component负责,目前只给space_entity设计了两种可以创建的server_entity,分别是陷阱trap_entity和怪物monster_entity。
在space_spawn_component组件启动的时候会从场景的配置文件中加载这些要创建的陷阱与怪物数据主要是其创建的位置、朝向、类型等信息。由于场景中的每个陷阱和怪物都可能带有独特的逻辑,所以我们创建这些server_entity的时候需要标注其对应的数据表的配置行是哪一个,也就是sid字段,代表这个server_entity的配置表流水号:
struct trap_sysd_columns
{
typed_matrix::typed_matrix::column_index spawn;
typed_matrix::typed_matrix::column_index pos;
typed_matrix::typed_matrix::column_index sid;
typed_matrix::typed_matrix::column_index trap_type;
typed_matrix::typed_matrix::column_index trap_radius;
typed_matrix::typed_matrix::column_index trap_height;
typed_matrix::typed_matrix::column_index player_trigger;
typed_matrix::typed_matrix::column_index monster_trigger;
bool valid() const
{
return spawn.valid() && pos.valid() && sid.valid() && trap_type.valid() && trap_radius.valid() && trap_height.valid() && player_trigger.valid() && monster_trigger.valid();
}
bool load(const typed_matrix::typed_matrix* trap_sysd);
};
struct monster_sysd_columns
{
typed_matrix::typed_matrix::column_index spawn;
typed_matrix::typed_matrix::column_index pos;
typed_matrix::typed_matrix::column_index sid;
typed_matrix::typed_matrix::column_index yaw;
typed_matrix::typed_matrix::column_index no;
typed_matrix::typed_matrix::column_index name;
bool valid() const
{
return spawn.valid() && pos.valid() && sid.valid() && no.valid() && name.valid() && yaw.valid();
}
bool load(const typed_matrix::typed_matrix* trap_sysd);
};
private:
class rpc_helper;
trap_sysd_columns m_trap_columns;
monster_sysd_columns m_monster_columns;
const typed_matrix::typed_matrix* m_trap_sysd;
const typed_matrix::typed_matrix* m_monster_sysd;
当space_spawn_component组件被激活的时候开始正式的通过space_traps和space_monster来根据数据来创建server_entity:
bool space_spawn_component::init(const json &data)
{
m_trap_sysd = m_owner->space_sysd("trap");
m_monster_sysd = m_owner->space_sysd("monster");
return m_trap_sysd && m_monster_sysd && m_trap_columns.load(m_trap_sysd) && m_monster_columns.load(m_monster_sysd);
}
void space_spawn_component::activate()
{
m_owner->logger()->info("space_spawn_component activate");
if (!spawn_traps())
{
m_owner->logger()->error("spawn_traps fail");
}
if (!spawn_monsters())
{
m_owner->logger()->error("spawn_monsters fail");
}
}
而根据一行配置数据去创建一个server_entity就是将所有相关配置数据填入初始化参数init_info和创生位置enter_info中,然后调用space_entity上提供的create_entity接口:
json cur_trap_prop;
cur_trap_prop["no"] = temp_trap_type;
cur_trap_prop["sid"] = temp_sid;
cur_trap_prop["trap_height_min"] = temp_trap_height[0];
cur_trap_prop["trap_height_max"] = temp_trap_height[1];
cur_trap_prop["trap_radius"] = temp_trap_radius;
cur_trap_prop["client_visible"] = true;
std::uint64_t cur_entity_flag = 0;
if (temp_player_trigger)
{
cur_entity_flag |= 1ull << std::uint64_t(enums::entity_flag::is_player);
}
if (temp_monster_trigger)
{
cur_entity_flag |= 1ull << std::uint64_t(enums::entity_flag::is_monster);
}
cur_trap_prop["trap_cb_any_flag"] = cur_entity_flag;
json::object_t trap_init_info, trap_enter_info;
trap_init_info["prop"] = cur_trap_prop;
trap_init_info["is_ghost"] = false;
trap_init_info["call_proxy"] = "";
trap_enter_info["pos"] = temp_born_pos;
trap_enter_info["yaw"] = 0;
return m_owner->create_entity("trap_entity", m_owner->gen_entity_id(), trap_init_info, trap_enter_info);
这个space_entity::create_entity就是一个对space_server::create_entity的简单封装,主要目的是为了保证每个被创建的actor_entity都能执行到enter_space:
actor_entity* space_entity::create_entity(const std::string& entity_type, const std::string& entity_id, json::object_t& init_info, const json::object_t& enter_info, std::uint64_t online_entity_id)
{
if(online_entity_id == 0)
{
online_entity_id = gen_online_entity_id();
}
std::string create_entity_error;
auto cur_entity = get_server()->create_entity(entity_type, entity_id, online_entity_id, init_info, create_entity_error);
if(!cur_entity)
{
m_logger->error("fail to create_entity type {} id {} with error {}", entity_type, entity_id, create_entity_error);
return nullptr;
}
auto cur_actor_entity = dynamic_cast<actor_entity*>(cur_entity);
if(!cur_actor_entity)
{
m_logger->error("fail to create actor_entity with entity_type {}", cur_entity->type_name());
get_server()->destroy_entity(cur_entity);
return nullptr;
}
// 省略一些代码
enter_space(cur_actor_entity, enter_info);
return cur_actor_entity;
}
场景进出流程
这里的enter_space负责设置actor_entity的位置,并在space_entity上构建每个类型的查找map,方便快速的根据online_entity_id查找当前场景内的某个actor_entity:
void space_entity::enter_space(actor_entity* cur_entity, const json::object_t& enter_info)
{
std::array<double, 3> cur_enter_pos = {0.0};
double cur_enter_yaw = 0;
try
{
enter_info.at("pos").get_to(cur_enter_pos);
enter_info.at("yaw").get_to(cur_enter_yaw);
}
catch(std::exception& e)
{
m_logger->error("enter info doesnt has pos yaw{}", e.what());
}
// 省略一些非重点代码
m_logger->info("entity {} enter space {} pos {}", cur_entity->entity_id(), entity_id(), json(cur_enter_pos).dump());
cur_entity->set_pos_yaw(cur_enter_pos, cur_enter_yaw);
m_total_entities[cur_entity->m_base_desc.m_type_id][cur_entity->m_base_desc.m_local_entity_id] = cur_entity;
if(cur_entity->is_player())
{
m_players[cur_entity->m_base_desc.m_local_entity_id] = cur_entity;
}
// 省略一些非重点代码
m_actors_by_online_id[cur_entity->online_entity_id()] = cur_entity;
m_entity_enter_counter[cur_entity->m_base_desc.m_type_id]++;
cur_entity->set_space(this);
auto cur_lambda = [cur_entity](space_component* cur_comp)
{
cur_comp->on_enter_space(cur_entity);
};
call_component_interface(cur_lambda);
}
同时这里先执行actor_entity::set_space来通知这个actor_entity来执行场景绑定,并通知这个actor_entity进入了新的场景, actor_entity所有组件执行on_enter_space。当set_space完成之后,再通知当前场景上的所有space_component来接受新的actor_entity进入:
void actor_entity::set_space(space_entity* in_space)
{
if(!m_space)
{
m_space = in_space;
enter_space();
}
else
{
assert(!in_space);
auto pre_space = m_space;
m_space = nullptr;
leave_space(pre_space);
}
}
class actor_component_interface
{
public:
virtual void on_leave_space(space_entity* cur_space)
{
}
virtual void on_enter_space()
{
}
};
void actor_entity::enter_space()
{
m_prop_flags = actor_data_prop_queue::get_actor_property_flags();
auto cur_lambda = [](actor_component* cur_comp)
{
cur_comp->on_enter_space();
};
call_component_interface(cur_lambda);
}
每个具体的actor_entity都可以复写这个enter_space,例如trap_entity就在enter_space的时候创建了两个AOI区域,来接收其他actor_entity的进出陷阱消息:
void trap_entity::enter_space()
{
actor_entity::enter_space();
get_space()->register_sid_entity<trap_entity>(this);
if(is_ghost())
{
return;
}
auto cur_actor_aoi_comp = get_component<actor_aoi_component>();
aoi::aoi_radius_controller cur_aoi_ctrl;
cur_aoi_ctrl.any_flag = m_prop_data.trap_cb_any_flag();
m_logger->info("trap aoi flag is {}", cur_aoi_ctrl.any_flag);
cur_aoi_ctrl.need_flag = m_prop_data.trap_cb_need_flag();
cur_aoi_ctrl.forbid_flag = m_prop_data.trap_cb_forbid_flag();
cur_aoi_ctrl.radius = m_prop_data.trap_radius();
cur_aoi_ctrl.min_height = m_prop_data.trap_height_min();
cur_aoi_ctrl.max_height = m_prop_data.trap_height_max();
cur_aoi_ctrl.max_interest_in = 30;
cur_actor_aoi_comp->add_aoi_radius(cur_aoi_ctrl, [this](actor_entity* other, bool is_enter)
{
if(is_enter)
{
if(m_aoi_in_cb)
{
m_aoi_in_cb(other);
}
}
else
{
if(m_aoi_leave_cb)
{
m_aoi_leave_cb(other->entity_id(), other->aoi_idx(), *other->get_call_proxy());
}
}
}, static_type_name());
}
上面介绍的是由场景负责创建的actor_entity的进入场景流程,由于玩家实体player_entity并不是由场景创建的,因此可以跳过开头的space_entity::create_entity流程,直接切入到space_entity::enter_space来执行,后面会在玩家进出场景中介绍。
当单次陷阱被触发或者怪物被杀死时,他们会主动的调用space_entity上销毁自己的接口:
void space_entity::on_entity_killed(actor_entity* target, actor_entity* killer)
{
if(!target->is_player())
{
// 处理非玩家的死亡逻辑
get_server()->destroy_entity(target);
return;
}
// 省略非重点代码
}
这里的destroy_entity会调用到server_entity::deactivate,如果当前是actor_entity,则会判断自身是否已经在场景内,如果有的话先执行leave_space操作:
void space_server::destroy_entity(entity::server_entity* cur_entity)
{
m_logger->info("deactive entity {} with type {}", cur_entity->entity_id(), cur_entity->m_base_desc.m_type_name);
cur_entity->deactivate();
m_entities_to_destroy.push_back(cur_entity);
}
void actor_entity::deactivate()
{
auto cur_space = get_space();
if(cur_space)
{
cur_space->leave_space(this);
}
clear_components();
m_dispatcher.clear();
m_misc_dispatcher.clear();
m_prop_dispatcher.clear();
m_migrate_in_finish_dispatcher.clear();
server_entity::deactivate();
}
这里的space_entity::leave_space刚好是之前enter_space的逆操作,会优先通知space_component这个actor_entity的离开,然后再执行actor_entity::set_space(nullptr)来通知到这个actor_entity上的所有actor_component来执行on_leave_space操作,最后再清除当前space_entity上对这个actor_entity的所有记录:
bool space_entity::leave_space(actor_entity* cur_entity)
{
if(!cur_entity)
{
return false;
}
if(cur_entity->get_space() != this)
{
return false;
}
auto cur_lambda = [cur_entity](space_component* cur_comp)
{
cur_comp->on_leave_space(cur_entity);
};
call_component_interface(cur_lambda);
m_aoi_manager->remove_pos_entity(aoi::aoi_pos_idx{cur_entity->aoi_idx()});
cur_entity->set_space(nullptr);
auto cur_entity_id = cur_entity->entity_id();
// 省略非重点代码
m_actors_by_online_id.erase(cur_entity->online_entity_id());
m_entity_leave_counter[cur_entity->m_base_desc.m_type_id]++;
// 省略非重点代码
return true;
}
场景任务流程
不同的场景提供了不同的游戏体验,玩家、怪物、陷阱等实体都是这些游戏体验中的角色。但是体验中光有角色是远远不够的,好需要有剧本,在剧本中来制定整个体验中各个角色所承担的任务,以及任务之间的关联。由于游戏场景中的策划快速铺量和定制化的需求,这些任务的关联不会直接在代码里实现,而是策划指定一些基础的任务规则需求,然后策划在配置数据中利用这些规则来创建具体的任务流程实例,并增加一些逻辑判定来处理任务之间的关联。这就是基于配表的场景流程,在space_entity中提供了一个space_quest_component组件来对这些任务规则做支持,为了统一管理各项规则,这里给所有的任务规则建立了一个基类space_quest:
class space_quest
{
public:
typed_matrix::typed_row m_sysd;
space_quest_component* m_quest_component;
space_quest(space_quest_component* in_quest_component, typed_matrix::typed_row in_sysd)
: m_sysd(in_sysd)
, m_quest_component(in_quest_component)
{
}
virtual bool enter()
{
return true;
}
virtual void leave()
{
}
virtual ~space_quest()
{
}
protected:
void change_to_next(std::uint32_t next_quest_id);
};
这里的space_quest里的m_sysd对应了场景任务表里的一行配置数据,每行配置数据都有一个流水号quest_id来关联,当这个流程被激活的时候会触发其enter()函数来开启自定义逻辑,当流程结束的时候调用leave()函数来做一些清理工作,流程之间的跳转则需要借助change_to_next来执行:
void space_quest::change_to_next(std::uint32_t next_quest_id)
{
m_quest_component->change_to_quest(next_quest_id);
}
void space_quest_component::change_to_quest(std::uint32_t next_quest_id)
{
auto pre_quest = m_current_quest;
m_current_quest = nullptr;
if(pre_quest)
{
pre_quest->leave();
delete pre_quest;
}
auto cur_row = m_quest_sysd->get_row(next_quest_id);
if(!cur_row.valid())
{
return;
}
std::string cur_quest_type;
if(!cur_row.expect_value("quest_type", cur_quest_type))
{
m_owner->logger()->error("cant find quest_type for row {}", next_quest_id);
return;
}
if(cur_quest_type == "wait_seconds")
{
m_current_quest = new wait_second_quest(this, cur_row);
}
else if(cur_quest_type == "kill_all_monsters")
{
m_current_quest = new kill_all_monsters_quest(this, cur_row);
}
// 省略很多具体的quest子类
else if(cur_quest_type == "final")
{
m_current_quest = new final_quest(this, cur_row);
}
if(!m_current_quest)
{
m_owner->logger()->error("fail to create quest of type {}", cur_quest_type);
return;
}
m_owner->logger()->info("enter quest type {} id {}", cur_quest_type, next_quest_id);
if(!m_current_quest->enter())
{
m_owner->logger()->error("enter quest fail type {} id {}", cur_quest_type, next_quest_id);
}
// 先enter 再设置quest 这样等到客户端接收到quest id改变之后 相关的数据已经设置好了
m_owner->set_quest_id(next_quest_id);
}
在space_quest_component上有m_current_quest字段来记录当前正在执行的任务流程,当要切换任务流程的时候,会先调用之前任务流程的leave,并delete来释放内存。然后再通过读表来获取新流程的类型并创建新的space_quest实例,并调用其enter函数,这样流程的切换就完成了。space_quest_component在激活的时候会默认强制切换到流水号为1的任务上:
bool space_quest_component::init(const json& data)
{
m_quest_sysd = m_owner->space_sysd("quest");
return true;
}
void space_quest_component::activate()
{
if(m_owner->is_cell_space())
{
return;
}
change_to_quest(1);
}
但是流程什么时候切换还是得依靠space_quest的具体子类逻辑去执行,space_quest_component无法控制。就以最简单的wait_seconds来说,需要在对应的配置数据里提供好等待时间delay和下一个任务的流水号next:
class wait_second_quest: public space_quest
{
public:
using space_quest::space_quest;
bool enter() override
{
std::uint32_t next_sid;
float delay_seconds;
if(!m_sysd.expect_value("next", next_sid) || !m_sysd.expect_value("delay", delay_seconds))
{
return false;
}
m_quest_component->get_owner()->add_timer_with_gap(std::chrono::milliseconds(int(delay_seconds*1000)), [this, next_sid]()
{
change_to_next(next_sid);
});
return true;
}
};
而对于比较复杂的space_quest,则需要在space_entity上增加各种事件监听函数,来获取当前子任务的进度。就以kill_all_monsters_quest来说,这个任务需要击杀指定流水号集合里的所有怪物,因此自身需要提供一个m_monster_sids来存储还需要击杀的怪物流水号,同时在enter的时候根据配表来初始化这个集合,并在space_entity上注册sid_monster_killed这个事件来对接on_monster_killed函数,从而更新m_monster_sids,并判定当前任务的完成以及执行后续任务的跳转:
class kill_all_monsters_quest: public space_quest
{
std::unordered_set<std::uint32_t> m_monster_sids;
std::uint32_t m_next_sid;
utility::listen_handler<std::string> m_listen_handler;
private:
void on_monster_killed(std::uint32_t monster_sid)
{
if(m_monster_sids.erase(monster_sid))
{
auto cur_space_data_entity = m_quest_component->get_owner()->get_space_data_entity();
for(const auto& one_pair: cur_space_data_entity->prop_data().quest_monsters())
{
if(one_pair.second == monster_sid)
{
cur_space_data_entity->prop_proxy().quest_monsters().erase(one_pair.first);
break;
}
}
}
if(m_monster_sids.empty())
{
change_to_next(m_next_sid);
}
}
public:
using space_quest::space_quest;
bool enter() override
{
if(!m_sysd.expect_value("next", m_next_sid) || !m_sysd.expect_value("monsters", m_monster_sids))
{
return false;
}
std::function<void(const std::string&, const json&)> cur_lambda = [this](const std::string& event, const json& detail)
{
if(detail.is_number_unsigned())
{
on_monster_killed(detail.get<std::uint32_t>());
}
};
m_listen_handler = m_quest_component->get_owner()->dispatcher().add_listener(std::string("sid_monster_killed"), cur_lambda);
return true;
}
void leave() override
{
m_quest_component->get_owner()->dispatcher().remove_listener(m_listen_handler);
}
};
当这个kill_all_monsters_quest彻底完成的时候,其leave函数就会删除之前注册的sid_monster_killed事件处理handler,避免其被delete之后还接受新的怪物被击杀事件,引发crash。
在space_quest_component中提供了一些基础的流程定义,后续有需要的话可以非常方便的进行扩展,根据之前的项目经验,大型MMO一般100个流程规则基本就可以覆盖了:
if(cur_quest_type == "wait_seconds")
{
m_current_quest = new wait_second_quest(this, cur_row);
}
else if(cur_quest_type == "kill_all_monsters")
{
m_current_quest = new kill_all_monsters_quest(this, cur_row);
}
else if(cur_quest_type == "kill_one_monster")
{
m_current_quest = new kill_one_monster_quest(this, cur_row);
}
else if(cur_quest_type == "trig_one_trap")
{
m_current_quest = new wait_one_trap_trig_quest(this, cur_row);
}
else if(cur_quest_type == "trig_all_traps")
{
m_current_quest = new wait_all_traps_trig_quest(this, cur_row);
}
else if(cur_quest_type == "fill_all_traps")
{
m_current_quest = new wait_traps_fill_quest(this, cur_row);
}
else if(cur_quest_type == "wait_one_event")
{
m_current_quest = new wait_one_event_quest(this, cur_row);
}
else if(cur_quest_type == "wait_all_events")
{
m_current_quest = new wait_all_events_quest(this, cur_row);
}
else if(cur_quest_type == "final")
{
m_current_quest = new final_quest(this, cur_row);
}
在副本流程逻辑彻底完成之后,m_current_quest应该会切换到final_quest上,这个space_quest什么都不做,不执行任何的跳转,代表终止流程:
class final_quest: public space_quest
{
public:
using space_quest::space_quest;
};
场景销毁流程
场景流程完成之后,服务器就需要回收这个场景所占据的所有资源。例如在space_entity的leave_space逻辑中,会记录当前剩下的玩家数量,如果剩下的玩家数量变成了0,也会开启一个自动销毁逻辑:
bool space_entity::leave_space(actor_entity* cur_entity)
{
// 省略一些代码
if(cur_entity->is_player() && m_total_entities[cur_entity->m_base_desc.m_type_id].empty())
{
add_auto_destroy_timer();
}
// 省略一些代码
return true;
}
const std::uint32_t m_auto_desctroy_gap_seconds = 60;
void space_entity::add_auto_destroy_timer()
{
if(m_auto_destroy_timer.valid())
{
return;
}
if(!m_space_type_info->is_town_space && !m_space_type_info->is_union_space)
{
m_auto_destroy_timer = add_timer_with_gap(std::chrono::milliseconds(m_auto_desctroy_gap_seconds * 1000), [this]()
{
auto_destroy();
});
}
}
这里设置为了所有玩家离开之后,60秒倒计时销毁:
void space_entity::auto_destroy()
{
m_auto_destroy_timer.reset();
if(!m_players.empty())
{
// 有玩家 暂时不销毁
return;
}
// 如果场景内没有玩家 则尝试自动销毁
m_logger->warn("space_entity::auto_destroy");
utility::rpc_msg cur_request_msg;
cur_request_msg.cmd = "request_destroy_space";
cur_request_msg.args.push_back(entity_id());
cur_request_msg.from = *get_call_proxy();
get_server()->call_service("space_service", cur_request_msg);
}
这里并不执行真正的销毁逻辑,而是向space_service发起notify_destroy_space这个rpc来通知此场景进程上的space_manager执行场景销毁逻辑。这个notify_destroy_space有一个前提条件,此space内已经没有玩家:
void space_service::request_destroy_space(const utility::rpc_msg &data, const std::string &space_id)
{
auto cur_space_type_iter = m_space_types.find(space_id);
utility::rpc_msg cur_msg;
do
{
if (cur_space_type_iter == m_space_types.end())
{
cur_msg.err = "invalid space";
break;
}
if (!cur_space_type_iter->second->is_union_space)
{
auto cur_space_iter = m_mono_spaces.find(space_id);
if (cur_space_iter == m_mono_spaces.end())
{
cur_msg.err = "invalid space";
break;
}
if (!cur_space_iter->second->players.empty())
{
cur_msg.err = "space has player";
break;
}
}
else
{
// 暂时省略另外一个分支
}
} while (0);
cur_msg.cmd = "reply_destroy_space";
cur_msg.args.push_back(space_id);
get_server()->call_server(this, data.from, cur_msg);
if (cur_msg.err.empty())
{
destroy_space_impl(space_id, cur_space_type_iter->second);
}
else
{
m_logger->error("request_destroy_space space_id {} err {}", space_id, cur_msg.err);
}
}
void space_service::destroy_space_impl(const std::string &space_id, const misc::space_type_info *space_type)
{
m_logger->info("destroy_space_impl {}", space_id);
if (!space_type->is_union_space)
{
auto cur_space_iter = m_mono_spaces.find(space_id);
if (cur_space_iter == m_mono_spaces.end())
{
return;
}
if (!cur_space_iter->second->players.empty())
{
return;
}
auto cur_game_iter = m_game_loads.find(cur_space_iter->second->game_id);
if (cur_game_iter == m_game_loads.end())
{
return;
}
m_spaces_by_no[cur_space_iter->second->space_no].erase(cur_space_iter->first);
utility::rpc_msg cur_msg;
cur_msg.cmd = "notify_destroy_space";
cur_msg.args.push_back(space_id);
auto cur_game_id = cur_space_iter->second->game_id;
call_space_manager(cur_game_id, cur_msg);
m_mono_spaces.erase(cur_space_iter);
cur_game_iter->second.mono_spaces.erase(space_id);
}
else
{
// 暂时省略另外一个分支
}
}
这里的space_manager::notify_destroy_space执行的就很暴力了,通知指定场景内的所有actor执行退出逻辑,然后执行这个space_entity的自毁:
void space_manager::notify_destroy_space(const utility::rpc_msg &data, const std::string &space_id)
{
auto cur_iter = m_spaces.find(space_id);
if (cur_iter == m_spaces.end())
{
m_logger->error("fail to destroy id {} ", space_id);
return;
}
cur_iter->second->clear_actors();
m_server->destroy_entity(cur_iter->second);
m_spaces.erase(cur_iter);
}
这里的clear_actors区分了一下是否是玩家类型,如果不是玩家类型则在退出之后直接销毁自身:
void space_entity::clear_actors()
{
m_logger->warn("clear_actors");
for(auto& one_type_ent_vec: m_total_entities)
{
std::vector<actor_entity*> temp_actors;
for(auto one_entity_pair: one_type_ent_vec)
{
auto cur_entity = one_entity_pair.second;
if(!cur_entity)
{
continue;
}
temp_actors.push_back(cur_entity);
}
for(auto one_actor: temp_actors)
{
leave_space(one_actor);
if(!one_actor->is_player() && !one_actor->is_exact_type<space_data_entity>())
{
get_server()->destroy_entity(one_actor);
}
}
}
}
对于有些类型的场景来说这个60秒的等待时间可能太长了,特别是一些高频创建和销毁的场景,典型例子就是匹配场景。为了加速场景资源的回收,在比赛结束finish_match中会触发一个十秒的销毁倒计时, 内部会通知space_service来执行request_countdown_destroy:
void space_match_component::finish_match()
{
m_owner->cancel_timer(m_match_finish_timer);
m_match_finish_timer.reset();
m_is_match_finish = true;
m_owner->get_space_data_entity()->prop_proxy().match_finish().set(true);
std::uint32_t cur_winner_faction = m_faction_num;
// 省略一些代码
utility::rpc_msg finish_msg;
finish_msg.cmd = "report_match_finish";
finish_msg.set_args(m_owner->match_uid(), cur_winner_faction, delta_scores);
m_owner->call_service("match_service", finish_msg);
utility::rpc_msg countdown_msg;
countdown_msg.cmd = "request_countdown_destroy";
std::uint32_t countdown_ts = 10; // 10s之后自动销毁
countdown_msg.set_args(m_owner->entity_id(), countdown_ts);
m_owner->call_service("space_service", countdown_msg);
m_owner->get_space_data_entity()->prop_proxy().destroy_ts().set(utility::timer_manager::now_ts() + 1000 * countdown_ts);
}
这个request_countdown_destroy负责在space_service上开启一个销毁计时器,时间到了之后直接执行之前介绍的destroy_space_impl:
void space_service::request_countdown_destroy(const utility::rpc_msg& data, const std::string& space_id, std::uint32_t countdown_seconds)
{
m_logger->info("request_countdown_destroy space_id {} countdown_seconds {}", space_id, countdown_seconds);
auto cur_space_type_iter = m_space_types.find(space_id);
if(cur_space_type_iter == m_space_types.end())
{
return;
}
auto cur_space_type_info = cur_space_type_iter->second;
if(cur_space_type_info->is_union_space)
{
return;
}
auto cur_mono_space_iter = m_mono_spaces.find(space_id);
if(cur_mono_space_iter == m_mono_spaces.end())
{
return;
}
kick_players_impl(space_id, cur_space_type_info);
add_timer_with_gap(std::chrono::milliseconds(countdown_seconds * 1000), [=]()
{
kick_players_impl(space_id, cur_space_type_info);
destroy_space_impl(space_id, cur_space_type_info);
});
}
这里在开启计时器之间和计时器到期之后都会执行一下kick_players_impl来通知踢出当前场景里的所有玩家,因为异步过程中可能会出现玩家传送到当前场景的情况,所以这里剔除两次来保证destroy_space_impl执行的时候场景内玩家已经空了。