群组与队伍

在游戏这个这个虚拟空间里玩家可以自由进行探索,方便的体验四时风物,历史人情。但是游戏能提供的体验内容总是有限的,多人在线游戏中必须不断的更新新内容来维持玩家在游戏内的活跃度。但是策划设计新场景任务的速度是永远都赶不上玩家的探索速度的,故意的提高全探索全收集的难度来减缓内容消耗速度又会造成玩家在多次尝试之后被劝退。因此。在多人在线游戏中,丰富的社群系统是不可或缺的。这些社交系统负责让玩家在等待内容更新的期间能够持续的在游戏内与其他玩家进行互动。主流的多人在线游戏的社群有很多种形式,例如群组、队伍、门派、帮派、师徒、势力、结义、姻缘、邻里等。在MosaicGame中也实现了其中最基本的两种形式:群组与队伍,因为这两种最基础也最广泛。至于其他类型的社群这里就不去涉及了,牵涉到了太多的具体的业务逻辑,同时结构上其实与群组并没有多大的差别,因此对这些不做介绍。

群组

群组结构定义

群组作为所有社群的基础,需要承担两个极其重要的社群职责:

  1. 群组成员结构的维系,主要是处理群组的创建删除以及组内人员的进出
  2. 群组的状态同步,主要是将群组内的可见状态推送到组内成员中

MosaicGame中使用了group_service来管理群组,每一个群组都用一个结构体group_resource来描述:

class group_resource
{
public:
	
	property::group::group_data_item m_prop;
	spiritsaway::property::top_msg_queue m_prop_queue;
	spiritsaway::property::prop_record_proxy<property::group::group_data_item> m_prop_proxy;
	std::vector<id_anchor_pair> m_online_id_anchors;
	mutable std::vector<std::string> m_temp_online_anchors;
	std::vector<std::string> m_all_online_anchors;
	misc::group_impl_handler m_handler;
	std::vector<std::uint8_t> m_dirty_fields;
	std::vector<std::uint8_t> m_db_fields;
};

群组内的状态完全用property::group::group_data_item m_prop这个property_item来表征:

class Meta(property) group_data_item : public spiritsaway::property::property_bag_item<std::uint32_t>
{
public:
	Meta(property(sync_self, save_db)) std::string m_name;

	Meta(property(sync_self, save_db)) group_players m_players;
	// 只需要给 leader进行同步
	Meta(property(sync_self, sync_leader, save_db)) group_applys m_recieve_applys;
	Meta(property(sync_self, save_db)) group_invites m_send_invites;
	Meta(property(sync_self, save_db)) std::string m_leader_id;
	Meta(property(sync_self, save_db)) std::map<std::string, json> m_common_data;
	Meta(property(sync_self, save_db)) bool m_allow_apply = false;
	Meta(property(sync_self, save_db)) bool m_only_leader_invite = false;
	Meta(property(sync_self, save_db)) std::uint32_t m_group_size = 0;
	Meta(property(sync_self, save_db)) std::uint64_t m_last_chat_seq = 0;

};

在这个group_data_item中,m_players也是一个property_bag,存储了队伍内的玩家数据。目前除了聊天之外没有其他业务逻辑使用群组,因此这里的玩家成员信息是非常简陋的,只记录了玩家的pid,进组时间以及是否在线,:

class Meta(property) group_player_item: public spiritsaway::property::property_bag_item<std::string>
{
public:
	Meta(property(sync_self, save_db)) std::uint64_t m_enter_ts = 0;
	Meta(property(sync_self)) bool m_online = false;
};
using group_players = spiritsaway::property::property_bag<group_player_item>;

其中在线状态并不需要存库,因此在线状态的property_flag没有save_db

群组状态修改

进入一个群组有三种方式:创建时自动进入、邀请进入、申请进入。默认情况下组内任意成员都可以邀请其他非当前组的成员进入当前群组,不过这里有一个m_only_leader_invite字段来控制是否只允许群组所有者才能邀请。玩家可以向任意群组发出入组申请,这些申请只能被群组的所有者进行审批。因为入组申请其他组员完全没有处理权限。所以group_data_item中的m_recieve_applys有一个比较特殊的地方,这个字段的property_flag里出现了一个与众不同的sync_leader字段。这个字段的作用是通知属性同步系统这个字段只需要给当前群组的所有者进行同步,因为只有所有者才有权去修改这个字段。

离开一个群组有两种方式:自愿退出或者被所有者强制移除。这里实现起来都比较简单,但是有一个需要注意的地方:如果群组的所有者自愿退出了,那么需要在剩余的组员中寻找一个作为新的所有者,如果没有剩余组员则彻底删除这个群组。此外还有一个强制解散当前群组的指令,这个解散指令只有群组所有者才有权发起。

所有的群组操作都是由玩家从客户端界面发起,RPC经过对应的服务端entity处理过后再发送给group_service去做最终的裁定。在这个操作传递链中,会经过客户端玩家、服务端玩家、群组服务三轮操作合法性检查。常规的实现里为了图方便可能就是把类似甚至相同的代码复制三遍,这样的实现在规则修改的时候很容易出现有其他地方漏改的情况。因此对于群组的相关接口的参数合法性检查被统一到了一个group_check_handler上,这个group_check_handler是一个非常轻量的对象,创建的时候只需要传入当前群组的group_data_item属性:

class group_check_handler
{
public:
	const property::group::group_data_item& m_group_data;
public:
	group_check_handler(const property::group::group_data_item& in_group_data);

	enums::group_errcode dismiss(const std::string& action_player_id);
	enums::group_errcode kick(const std::string& dest_player_id, const std::string& action_player_id);
	enums::group_errcode exit(const std::string& action_player_id);
	enums::group_errcode apply(const property::group::group_apply_item& apply_player_info);
	enums::group_errcode handle_apply(const std::string& apply_player_id, bool is_confirm, const std::string& action_player_id);
	enums::group_errcode retract_apply(const std::string& apply_player_id);
	enums::group_errcode invite(const std::string& dest_player_id, const std::string& action_player_id);
	enums::group_errcode retract_invite(const std::string& dest_player_id, const std::string& action_player_id);
	enums::group_errcode accept_invite(const property::group::group_player_item& apply_player_info);
	enums::group_errcode change_leader(const std::string& dest_player_id, const std::string& action_player_id);
	enums::group_errcode set_allow_apply(bool is_confirm, const std::string& action_player_id);
	enums::group_errcode set_allow_invite(bool is_confirm, const std::string& action_player_id);
	enums::group_errcode update_member_info(const json& new_info, const std::string& action_player_id);
	enums::group_errcode change_name(const std::string& new_name, const std::string& action_player_id);
	enums::group_errcode update_common_info(const std::string& data_key, const json& new_info, const std::string& action_player_id);

	void check_apply_expire(std::uint64_t now_ts, std::vector<std::string>& expired_player_ids);
	void check_invite_expire(std::uint64_t now_ts, std::vector<std::string>& expired_player_ids);
};

每个接口都需要传入接口调用人与相关参数,同时返回这个接口的相关错误码,当发现相关参数不合法时,将拒绝这个接口的执行,并将相关错误信息传递到客户端。下面就是一个强制移除成员的RPC接口例子,完整的说明了一个群组接口在服务端的检查之后的转发流程,其他接口的代码结构基本与此例子类似:

void player_group_component::group_kick(const utility::rpc_msg& msg, std::uint32_t group_id, const std::string& dest_player_id)
{
	enums::group_errcode cur_err = enums::group_errcode::ok;
	do
	{
		auto temp_group_data = m_player->prop_data().m_group.m_group_datas.get(group_id);
		if(!temp_group_data)
		{
			cur_err = enums::group_errcode::invalid_group_id;
			break;
		}
		auto temp_group_check_handler = misc::group_check_handler(*temp_group_data);
		cur_err = temp_group_check_handler.kick(dest_player_id, m_owner->entity_id());

	} while (false);
	if(cur_err != enums::group_errcode::ok)
	{
		utility::rpc_msg reply_msg;
		reply_msg.set_args(std::uint8_t(enums::group_action::kick), group_id, msg.args, std::uint32_t(cur_err));
		reply_msg.cmd = "group_action_reply";
		m_player->call_client(reply_msg);
		return;
	}
	group_call_service(msg);
}

如果检查失败会通过group_action_reply这个通用的群组RPC结果通知接口传递到客户端,这里的第一个参数也是一个枚举类型enums::group_action,用来表明当前执行的操作是哪个。中间的两个参数负责记录要操作的群组以及相关的操作参数,最后的参数就是错误码。如果错误码等于enums::group_errcode::ok,则将这个操作转发到group_service上,再次做一轮检查,检查通过之后才能执行相关效果:

void group_service::group_kick(const utility::rpc_msg& msg, std::uint32_t dest_group_idx, const std::string& dest_player_id, const std::string& action_player_id)
{
	enums::group_errcode cur_err = enums::group_errcode::ok;
	group_resource* dest_group = nullptr;
	do
	{
		auto temp_group_iter = m_group_resources.find(dest_group_idx);
		if(temp_group_iter == m_group_resources.end())
		{
			cur_err = enums::group_errcode::invalid_group_id;
			break;
		}
		dest_group = temp_group_iter->second.get();
		cur_err = dest_group->m_handler.kick(dest_player_id, action_player_id);
		if(cur_err != enums::group_errcode::ok)
		{
			break;
		}
	} while (false);

	if(cur_err == enums::group_errcode::ok)
	{
		on_player_leave_group(dest_player_id, dest_group, std::uint8_t(enums::group_action::kick));
		std::vector<json> temp_args;
		temp_args.push_back(dest_player_id);
		group_sync_props(dest_group, std::uint8_t(enums::group_action::kick), std::move(temp_args), {});
	}
	auto temp_player_iter = m_player_infos.find(action_player_id);
	if(temp_player_iter == m_player_infos.end())
	{
		return;
	}
	
	group_action_reply(temp_player_iter->second.anchor, std::uint8_t(enums::group_action::kick), dest_group_idx, msg, std::uint32_t(cur_err));

}

这里的dest_group->m_handler就不是之前提到的group_check_handler了,而是group_impl_handler。这个group_impl_handler内部会先调用group_check_handler来检查参数合法性,如果合法则执行相关操作,修改存储在group_service上的对应group_data_item属性:

enums::group_errcode group_impl_handler::kick(const std::string& dest_player_id, const std::string& action_player_id)
{
	auto cur_err = group_check_handler::kick(dest_player_id, action_player_id);
	if(cur_err != enums::group_errcode::ok)
	{
		return cur_err;
	}
	m_group_proxy.players().erase(dest_player_id);
	return cur_err;
}

由于复用了group_check_handler的所有代码,因此此处的代码量其实很少。

群组属性同步

group_check_handler减轻了很多代码维护上的工作量,group_impl_handler只需要维护好属性修改即可。但是目前group_impl_handler修改的只是存储在group_service上的一个group_data_item,修改之后我们还需要将这些修改通知到群组内所有的服务端玩家。因此对于group_service上的每一个group_data_item,都会有一个封装其修改与同步操作的group_resource,内部使用一个spiritsaway::property::top_msg_queue m_prop_queue;来暂存所有的属性同步消息:

group_resource::group_resource(group_service& in_service, const std::uint32_t& group_id, const std::string& group_name, const property::group::group_player_item& leader_info, const std::uint32_t group_sz)
: m_prop_queue(group_prop_flags(), true, true)
, m_prop_proxy(m_prop, m_prop_queue, spiritsaway::property::property_record_offset(), spiritsaway::property::property_flags{ spiritsaway::mosaic_game::property::property_flags::mask_all }, 0)
, m_handler(m_prop_proxy)
{
	m_prop.m_id = group_id;
	m_prop.m_name = group_name;
	m_prop.m_leader_id = leader_info.m_id;
	m_prop.m_group_size = group_sz;
	m_prop_proxy.players().insert(leader_info);
	// 直接清空属性同步队列
	m_prop_queue.dump();
}
group_resource::group_resource(property::group::group_data_item&& in_prop)
: m_prop(std::move(in_prop))
, m_prop_queue(group_prop_flags(), true, true)
, m_prop_proxy(m_prop, m_prop_queue, spiritsaway::property::property_record_offset(), spiritsaway::property::property_flags{ spiritsaway::mosaic_game::property::property_flags::mask_all }, 0)
, m_handler(m_prop_proxy)
{
	// 直接清空属性同步队列
	m_prop_queue.dump();
}

每次处理的群组action引发了属性修改之后,外部需要执行group_sync_props将这些属性同步消息广播到所有在线的群组内玩家:

void group_service::group_sync_props(group_resource* cur_group_ptr, std::uint8_t cur_action_id, std::vector<json>&& action_args, const std::string& except_id)
{
	auto cur_prop_deltas = cur_group_ptr->m_prop_queue.dump();
	if(cur_prop_deltas.empty())
	{
		m_logger->info("cur_action_id {} action_args {} prop_delta empty", cur_action_id, json(action_args).dump());
		return;
	}
	std::vector<json> prop_delta_jsons;
	prop_delta_jsons.reserve(cur_prop_deltas.size());
	bool has_leader_prop = false;
	for(auto& one_prop_info: cur_prop_deltas)
	{
		if(cur_group_ptr->add_dirty_field(one_prop_info.offset.top()))
		{
			m_dirty_groups.emplace(cur_group_ptr->m_prop.m_id, utility::timer_manager::now_ts());
		}
		if(one_prop_info.flag.value & spiritsaway::mosaic_game::property::property_flags::sync_leader)
		{
			has_leader_prop = true;
			
		}
		else
		{
			std::vector<json> team_prop_json;
			team_prop_json.reserve(4);
			team_prop_json.push_back(one_prop_info.offset.value());
			team_prop_json.push_back(std::uint8_t(one_prop_info.cmd));
			team_prop_json.push_back(one_prop_info.flag.value);
			team_prop_json.push_back(std::move(one_prop_info.data));
			prop_delta_jsons.push_back(std::move(team_prop_json));
			
		}
	}
	std::vector<json> rpc_args;
	rpc_args.reserve(3);
	rpc_args.push_back(cur_action_id);
	rpc_args.push_back(action_args);
	rpc_args.push_back(std::move(prop_delta_jsons));
	group_broadcast(cur_group_ptr, "group_prop_delta", rpc_args, except_id, has_leader_prop);
	if(has_leader_prop)
	{
		prop_delta_jsons.clear();
		for(auto& one_prop_info: cur_prop_deltas)
		{
			std::vector<json> team_prop_json;
			team_prop_json.reserve(4);
			team_prop_json.push_back(one_prop_info.offset.value());
			team_prop_json.push_back(std::uint8_t(one_prop_info.cmd));
			team_prop_json.push_back(one_prop_info.flag.value);
			team_prop_json.push_back(std::move(one_prop_info.data));
			prop_delta_jsons.push_back(std::move(team_prop_json));
		}
		rpc_args.back() = std::move(prop_delta_jsons);
		group_call_leader(cur_group_ptr, "group_prop_delta", std::move(rpc_args));
	}
}

这个属性同步接口有一半的内容基本是重复的,主要是为了处理m_recieve_applys这个只有群组拥有者才可见的属性的同步问题,也就是上面代码中的has_leader_prop部分。

当在线玩家通过group_prop_delta接收到了最新的群组修改数据之后,需要一个自动的机制将所有的修改在自身身上回放。其实这个回放过程与我们之前做的服务端属性同步到客户端后回放的流程基本类似,不同的地方在于客户端使用了prop_replay_proxy,而服务端使用的是prop_record_proxy:

void player_group_component::group_prop_delta(const utility::rpc_msg& msg, std::uint32_t group_id, std::uint8_t group_action_id, const std::vector<json>& action_args, const std::vector<json>& prop_deltas)
{
	m_owner->logger()->info("group_prop_delta group_id {} receive action_id {} args {}, deltas {}", group_id, magic_enum::enum_name(enums::group_action(group_action_id)), msg.args[2].dump(), msg.args[3].dump());
	misc::group_prop_sync_event cur_prop_sync_event{false, group_id, action_args};
	m_owner->dispatcher().dispatch(enums::group_action(group_action_id), cur_prop_sync_event);
	auto cur_group_proxy_opt = m_player->prop_proxy().group().group_datas().get(group_id);
	if(!cur_group_proxy_opt.has_value())
	{
		assert(false);
		return;
	}
	for(const auto& one_prop: prop_deltas)
	{
		std::uint64_t offset;
		std::uint8_t cmd;
		std::uint64_t flag;
		json prop_data;
		if(!serialize::decode_multi(one_prop, offset, cmd, flag, prop_data))
		{
			m_owner->logger()->error("fail to decode team prop delta {}", one_prop.dump());
			continue;
		}
		auto cur_cmd_enum = spiritsaway::property::property_cmd(cmd);

		cur_group_proxy_opt.value().replay(spiritsaway::property::property_record_offset(offset).to_replay_offset(), cur_cmd_enum, prop_data);
	}
	cur_prop_sync_event.is_finish = true;
	m_owner->dispatcher().dispatch(enums::group_action(group_action_id), cur_prop_sync_event);
	
}

不过这里用了这个prop_record_proxy上的一个比较高级的用法replay,即回放另外一个prop_record_proxy修改的同时,会将修改的信息再传一份到prop_msg_queue中,并最终通过prop_msg_queue再同步到客户端。这样就实现了group_data_itemgroup_service端修改后的所有在线群组成员的服务端和客户端的属性修改同步。

群组数据加载

群组服务是我们目前接触到的第一个游戏局外系统,局外系统相对于局内系统有一个非常大的不同,即其状态是需要持久化的。玩家下线后再上线需要看到其最后的修改结果,同时游戏服务器关服再开服要保证前后的数据是一致的。为了将这些局外数据进行持久化,我们这里需要将这些数据编码之后存储到外部数据库中,然后在游戏启动之后再从数据库中恢复出这些数据。所以在group_service的启动阶段会去数据库中加载所有的群组数据。由于群组数量可能非常多,单次数据库查询的结果会非常大,可能导致网络层将这个数据丢弃。因此在加载的时候执行的是分批加载,每次加载一个固定的数量batch_num。为了配合这样的分批加载,每个group_data_item上的id字段就会当作计算批次的依据:

const std::string& group_service::group_counter_field()
{
	static std::string counter_field = "id";
	return counter_field;
}

为了知道什么时候加载完成,需要首先获取当前所有的群组编号的最大值,这个最大值存储在通用的counter数据库中:

bool group_service::init(const json::object_t& data)
{
	m_service_state = service_state::invalid;
	if(!base_service::init(data))
	{
		return false;
	}
	server::unique_counter_manager::instance().get_current_counter(group_db_name(), [cur_server = m_service_server, cur_service_id = m_base_desc.m_global_id, this](const std::string& query_err, std::uint64_t result_counter)
	{
		if(!cur_server->check_service_active(cur_service_id))
		{
			return;
		}
		on_query_counter_back(query_err, result_counter);
	});
	m_service_state = service_state::query_counter;
	return true;
}

counter查询回调回来之后,会利用一个辅助类型collection_loader_manager来托管分批加载的流程,业务只需要提供一个最终完成的回调即可:

void group_service::on_query_counter_back(const std::string& query_err, std::uint64_t result_counter)
{
	m_logger->info("on_query_counter_back err {} result_counter {}", query_err, result_counter);
	if(!query_err.empty())
	{
		m_logger->error("on_query_counter_back fail with err {}", query_err);
		m_service_state = service_state::fail;
		return;
	}
	m_logger->info("on_query_counter_back with counter {}", result_counter);
	if(m_service_state != service_state::query_counter)
	{
		m_logger->error("on_query_counter_back while state is {}", int(m_service_state));
		return;
	}
	if(result_counter == 0)
	{
		m_service_state = service_state::ready;
		report_ready();
		return;
	}
	m_service_state = service_state::load_db;
	server::collection_load_params cur_load_param;
	cur_load_param.collection_name = group_db_name();
	cur_load_param.counter_field = group_counter_field();
	cur_load_param.counter_max = result_counter;
	cur_load_param.batch_num = 100;
	m_load_db_sid = server::collection_loader_manager::instance().request_load_collection(cur_load_param, [this](const std::string& db_err ,const json::array_t& result_datas)
	{
		on_load_db_back(db_err, result_datas);
	});

}

collection_loader_manager的使用提供collection_load_params结构体来明确要加载的数据库、最大流水号、批次设置、流水号字段,这样内部就会循环回调的形式来执行数据的分批全量加载:

std::uint32_t collection_loader_manager::request_load_collection(const collection_load_params& load_param, callback_type load_callback)
{
	if(load_param.counter_max == 0)
	{
		return 0;
	}
	m_load_sid++;
	auto cur_sid = m_load_sid;
	loading_result temp_loading_result;
	temp_loading_result.param = load_param;
	temp_loading_result.load_callback = load_callback;
	m_loading_results[cur_sid] = temp_loading_result;
	m_loading_results[cur_sid].collection_data.reserve(load_param.batch_num);
	start_next_load(cur_sid);
	return cur_sid;
}

void collection_loader_manager::start_next_load(std::uint32_t cur_load_sid)
{
	auto temp_iter = m_loading_results.find(cur_load_sid);
	if(temp_iter == m_loading_results.end())
	{
		return;
	}
	auto cur_db_callback = [this, cur_load_sid](const json& db_reply)
	{
		this->get_data_callback(cur_load_sid, db_reply);
	};
	tasks::db_task_desc::base_task cur_task_base(tasks::db_task_desc::task_op::find_multi, std::string{}, "", temp_iter->second.param.collection_name);


	json counter_seq_query;
	counter_seq_query["$gte"] = temp_iter->second.m_next_seq_to_load;
	counter_seq_query["$lt"] = std::min(temp_iter->second.param.counter_max + 1, temp_iter->second.m_next_seq_to_load + temp_iter->second.param.batch_num);
	json collection_query;
	collection_query[temp_iter->second.param.counter_field] = counter_seq_query;
	auto  cur_find_task = tasks::db_task_desc::find_task::find_multi(cur_task_base, collection_query, temp_iter->second.param.batch_num);

	m_server->call_db(cur_find_task->to_json(),  cur_db_callback);
}

这里的get_data_callback会判断当前返回数据的批次号是否已经达到了最大批次号,如果达到了就执行数据完全加载的回调,否则就开启下一个批次的加载:

// void collection_loader_manager::get_data_callback(std::uint32_t cur_load_sid, const json& db_reply)
_logger->info("get_data_callback for cur_load_sid {} collection {} next_seq_to_load {} update with size {}", cur_load_sid, temp_iter->second.param.collection_name, temp_iter->second.m_next_seq_to_load, temp_iter->second.collection_data.size());
temp_iter->second.m_next_seq_to_load += temp_iter->second.param.batch_num;
if(temp_iter->second.m_next_seq_to_load > temp_iter->second.param.counter_max)
{
	m_logger->info("get_data_callback for cur_load_sid {} collection {} finished with num {}", cur_load_sid, temp_iter->second.param.collection_name, temp_iter->second.collection_data.size());
	auto final_data = std::move(temp_iter->second.collection_data);
	auto cur_callback = std::move(temp_iter->second.load_callback);
	m_loading_results.erase(temp_iter);
	cur_callback(error, final_data);
	return;
}
start_next_load(cur_load_sid);

群组数据存库

同时为了避免游戏的突然崩溃造成存档丢失,局外系统的数据一般会在修改后以一定频率执行一下对外部数据库的同步。如果是充值记录等重要的数据,基本都需要立即写数据库,等到数据库落库回调回来之后才能执行后续的逻辑。但是对于群组数据这种不是非常重要的数据,则不需要在每次修改之后就立即同步。因为群组数据的修改频率会非常的高,每次都直写数据库会给数据库增加非常大的负载压力,因此这里对于群组数据的持久化采用的是定期存库的策略。在group_service上会开启一个计时器来执行存库:

m_check_save_timer = add_timer_with_gap(std::chrono::milliseconds(m_check_save_gap_ms), [this]()
{
	check_save();
});

group_service上使用了一个unordered_map<uint32, uint64> m_dirty_groups来记录所有需要存库的群组数据以及其修改时间戳。由于同一时刻需要存库的群组数据可能比较多,为了避免大量群组数据的编码发送触发cpu的瞬间升高以及网络拥堵,这个check_save函数内部会将修改时间最早的m_check_save_num个群组数据来执行存库,剩下的未存库数据需要等到下一次check_save再执行判断:

void group_service::check_save()
{
	m_sorted_dirty_groups.clear();

	m_sorted_dirty_groups.reserve(m_dirty_groups.size());
	for(const auto& one_pair: m_dirty_groups)
	{
		std::pair<std::uint64_t, std::uint32_t> new_pair; // first是修改时间戳 second是群组的id
		new_pair.first = one_pair.second;
		new_pair.second = one_pair.first;
		m_sorted_dirty_groups.push_back(new_pair);
	}
	std::uint32_t final_save_num = m_sorted_dirty_groups.size();
	if(m_sorted_dirty_groups.size() > m_check_save_num)
	{
		std::nth_element(m_sorted_dirty_groups.begin(), m_sorted_dirty_groups.begin() + m_check_save_num, m_sorted_dirty_groups.end());
		final_save_num = m_check_save_num;
	}
	// 省略真正执行存库的代码
}

每次存库都将整个group_data_item都执行一次encode的代价有点大,在有些属性压根没有修改的情况下,这样的操作既浪费cpu又浪费流量,所以我们这里使用一个数组来m_dirty_fields记录哪些需要存库的字段被修改了,存库的时候只需要对这些字段执行encode即可:

for(std::uint32_t i = 0;i<final_save_num;i++)
{

	auto cur_group_idx = m_sorted_dirty_groups[i].second;
	
	auto temp_iter = m_group_resources.find(cur_group_idx);
	if(temp_iter == m_group_resources.end())
	{
		continue;
	}
	temp_iter->second->m_handler.clear_expired_applys_and_invites(utility::timer_manager::now_ts(), temp_pid_buffer);
	
	spiritsaway::property::property_flags cur_save_db_flag;
	cur_save_db_flag.value = property::property_flags::save_db;
	auto cur_group_json = temp_iter->second->m_prop.encode_fields_with_flag(temp_iter->second->m_dirty_fields, cur_save_db_flag, false);
	if(cur_group_json.empty())
	{
		continue;
	}
	// 只存储diff的字段
	temp_iter->second->m_dirty_fields.clear();
	auto cur_db_calback = [this](const json& db_reply)
	{
		this->on_save_group_db_back(db_reply);
	};
	auto cur_db_callback_id = m_callback_mgr.add_callback(cur_db_calback);
	tasks::db_task_desc::base_task cur_task_base(tasks::db_task_desc::task_op::update_one, std::string{}, std::to_string(cur_db_callback_id.value()), group_db_name());
	json query_doc, db_doc;
	query_doc[group_counter_field()] = cur_group_idx;
	db_doc["$set"] = std::move(cur_group_json);
	auto cur_update_task = tasks::db_task_desc::update_task::update_one(cur_task_base, query_doc, db_doc, false);

	get_server()->call_db(cur_update_task->to_json(), this, cur_db_callback_id);

	m_dirty_groups.erase(cur_group_idx);
}

m_dirty_fields的维护是通过add_dirty_field函数来做的,需要在属性有修改之后使用add_dirty_field过滤掉这些不需要存库的字段,同时记录一下哪些需要存库的字段被修改了:

bool group_resource::add_dirty_field(std::uint8_t cur_field, const spiritsaway::property::property_flags& prop_flag)
{
	if(!(prop_flag.value & spiritsaway::mosaic_game::property::property_flags::save_db))
	{
		return false;
	}
	auto temp_iter = std::find(m_dirty_fields.begin(), m_dirty_fields.end(), cur_field);
	if(temp_iter == m_dirty_fields.end())
	{
		m_dirty_fields.push_back(cur_field);
		return true;
	}
	return false;
}

每次group_data_item修改之后group_sync_props内都检查一下相关字段的prop_flags里是否有save_db这个标记,如果有的话则加入到m_dirty_groups中,记录群组的标识符以及修改时间戳,这里的emplace接口会保证对应流水号已经在m_dirty_groups中时不会去更新时间戳:

for(auto& one_prop_info: cur_prop_deltas)
{
	if(cur_group_ptr->add_dirty_field(one_prop_info.offset.top(), one_prop_info.flag))
	{
		m_dirty_groups.emplace(cur_group_ptr->m_prop.m_id, utility::timer_manager::now_ts());
	}
	// 省略其他代码
}

队伍

队伍可以看作一个固定了成员上限的小型群组,因此在组队相关逻辑这里依照群组的实现模式构造了team_check_handlerteam_impl_handler来实现队伍创建、退出、邀请、申请等相关操作的检查与执行,同时team_service上也利用了prop_record_proxy配合team_broadcast来执行队伍相关属性的广播同步。所以读者可以在team_service,player_team_component上看到很多之前介绍过的群组相关代码的影子。

但是队伍相对于群组来说又有两个非常明显的差异:

  1. 队伍只是在服务器运行期间才存在的,队伍成员全都下线之后队伍自动解散,因此team_service上队伍数据不需要执行数据库的读取与存库操作,同时玩家身上的队伍数据team_prop也没有save_db相关字段。

  2. 一个玩家在同一时间内最多会有一个对应的队伍,而一个玩家可以同时归属于多个群组,所以队伍服务team_service上需要记录所有在线人员对应的队伍信息,同时玩家身上的team_prop只有一个,

队伍除了成员管理、聊天等群组通用的功能之外,还有组队投票、组队撮合等队伍独有功能,这两点需要着重说一下。

组队撮合

正常来说一个玩家如果想进入一个队伍,要么目标队伍中有人向自己发出了邀请,要么自己往目标队伍发生了申请。这两个功能的实现基本与群组中的对应功能一致,所以这里就不去展开。不过队伍这个小型群组有个比较特殊的地方,就是他的临时性。因为一个队伍组建起来一般都是为了一个临时性的目标,例如完成日常的组队任务、组队副本、组队PK等这些强行要求组队的玩法。而完成了这些玩法之后,对应的队伍一般都会进行解散,这就是所谓的野团。为了提升游戏里的社交,策划设计的组队玩法会非常多,所以玩家的入队和退队频率是很高的,这些队伍的创建销毁也十分频繁。但是不同玩家在不同时间点的组队需求是不一样的,可能A想去做组队副本M,而B想去做组队副本N,组完队之后再去商量下一个目标是什么就会吵起来。然后玩家如果想从大厅里寻找一个特定任务的队伍,需要遍历当前的所有空闲队伍列表,一个个的去询问队长当前的目标是否与自己相匹配,这种体验非常的差。因此一般来说会在队伍属性里添加一个target字段,代表当前队伍的阶段性目标是什么,这样就可以方便玩家寻找队伍时来过滤掉大量无关队伍。同时很多组队任务并不是集齐任意的指定数量的玩家就可以通过的,例如很多副本里需要队伍中同时存在战法牧这三种角色才能勉为其难的通关,如果全是战士或者全是奶妈则基本没有机会。然后不同的副本对应的战斗强度不一样,队伍成员的战斗力过低会大大的拖累任务的完成,甚至导致任务的失败。所以队伍需要能够自定义的准入条件,包括上述描述到的目标、职业、等级、装等、战力、修为、进度等要素,来限制掉一些不符合要求的入队申请。在mosaic_game里目前提供了下面的三个字段来做一些过滤:

class Meta(property) team_prop
{
public:
	Meta(property(sync_clients, sync_redis)) std::string m_id;
	Meta(property(sync_self, sync_redis)) std::string m_target; // 队伍的目标,例如组队副本、组队PK等
	Meta(property(sync_self, sync_redis)) std::vector<std::uint32_t> m_sects_need; // 队伍限定的新入队玩家的职业
	Meta(property(sync_self, sync_redis)) std::uint32_t m_level_need = 0; // 队伍限定的新入队玩家的等级
	// 省略很多字段
};

然后在往目标队伍发出申请的时候,需要带上自己的相关信息team_player_item,来辅助是否满足队伍设置的相关限制条件:


class Meta(property) team_player_item: public spiritsaway::property::property_slot_item<std::uint64_t>
{
public:
	Meta(property(sync_self, sync_redis)) std::string m_nickname;
	Meta(property(sync_self, sync_redis)) std::string m_pid; // 存库用id
	Meta(property(sync_self, sync_redis)) std::uint32_t m_sect = 0;
	Meta(property(sync_self, sync_redis)) std::uint32_t m_level = 0;
	Meta(property(sync_self, sync_redis)) std::uint32_t m_space_no = 0;
	Meta(property(sync_self, sync_redis)) std::string m_space_id;
	Meta(property(sync_self)) std::string m_anchor;
	Meta(property(sync_self, sync_redis)) bool m_client_online = true;
	Meta(property(sync_self)) std::uint64_t m_appply_ts = 0;
	#ifndef __meta_parse__
	#include "team/team_player_item.generated.inch"
	#endif
};

enums::team_errcode team_check_handler::apply(const property::team::team_player_item& apply_player_info)
{
	const auto cur_apply_player_oid = apply_player_info.id();
	if(m_team_data.recieve_applys().get(cur_apply_player_oid))
	{
		return enums::team_errcode::already_during_apply;
	}
	if(m_team_data.players().full())
	{
		return enums::team_errcode::team_full;
	}
	if(!m_team_data.allow_apply())
	{
		return enums::team_errcode::apply_not_allowed;
	}
	if(m_team_data.recieve_applys().index().size() >= enums::team_max_apply_sz)
	{
		return enums::team_errcode::applys_too_much;
	}
	if(m_team_data.match_info().match_index())
	{
		return enums::team_errcode::during_match;
	}
	if(m_team_data.locked())
	{
		return enums::team_errcode::team_locked;
	}
	if(!m_team_data.m_sects_need.empty())
	{
		if(std::find(m_team_data.m_sects_need.begin(), m_team_data.m_sects_need.end(), apply_player_info.sect()) == m_team_data.m_sects_need.end())
		{
			return enums::team_errcode::sect_not_match;
		}
	}
	if(apply_player_info.level() < m_team_data.level_require())
	{
		return enums::team_errcode::level_not_match;
	}
	return enums::team_errcode::ok;
}

同时team_service上提供一个根据目标和等级来查询合适队伍的接口,这样方便客户端去查询合适的队伍并发送申请入队请求:

void team_service::team_fetch_teams(const utility::rpc_msg& msg, const std::string& team_target, std::uint32_t max_num,  std::uint32_t player_level, const std::uint64_t action_player_oid)
{
	auto cur_player_iter = m_player_infos.find(action_player_oid);
	if(cur_player_iter == m_player_infos.end())
	{
		return;
	}
	auto cur_target_iter = m_teams_by_target.find(team_target);
	std::vector<const team_resource*> result_team_ptrs;
	result_team_ptrs.reserve(8);
	if(cur_target_iter != m_teams_by_target.end())
	{
		for(const auto& cur_team_ptr: cur_target_iter->second)
		{
			if(!cur_team_ptr->m_prop.m_allow_apply)
			{
				continue;
			}
			if(cur_team_ptr->m_prop.m_target != team_target)
			{
				continue;
			}
			if(cur_team_ptr->m_prop.m_level_need > player_level)
			{
				continue;
			}
			if(cur_team_ptr->m_prop.m_players.full())
			{
				continue;
			}
			result_team_ptrs.push_back(cur_team_ptr);
			if(result_team_ptrs.size() >= max_num)
			{
				break;
			}
		}
	}
	
	std::vector<json> result_team_infos;
	result_team_infos.reserve(result_team_ptrs.size());
	for(auto one_team_ptr: result_team_ptrs)
	{
		result_team_infos.push_back(one_team_ptr->m_prop.encode_with_flag(spiritsaway::property::property_flags{mosaic_game::property::property_flags::sync_redis}, true, false));
	}

	std::vector<json> rpc_args;
	rpc_args.push_back(std::move(result_team_infos));

	team_call_player(cur_player_iter->second.anchor, "team_fetch_teams_back", std::move(rpc_args));
}

这里为了快速过滤target字段,所以在team_service上维护了一个m_teams_by_target字段,用这个map来根据目标来快速查询相关队伍集合:

std::unordered_map<std::string, std::unordered_set<const team_resource*>> m_teams_by_target;

由于这个搜索合适的队伍是一个比较频繁的操作,且每个队伍都需要打包sync_redis的字段,这就会带来不少的客户端服务端之间的通信流量。更好的方法是将m_teams_by_target推送到对外服务的redis集群中,这样客户端就可以直接从redis中查询到合适的队伍,而不需要再去服务端查询。

组队投票

组队活动中有些时候任务流程的继续进行需要征求队伍成员的同意,虽然可以通过队伍内聊天来执行相关信息的沟通与收集,但是很多时候这种交互流程效率很低,因为涉及到打字沟通和人工统计这两个步骤。考虑到此时队员的回复一般都比较简单,基本都是是否同意这样的信息,所以队伍系统这边就引入了投票机制,来加速这个信息收集流程的处理。

由于一个队伍里可能同时存在多个进行中的投票活动,因为这个投票数据在team_prop上设置为了一个背包,key为投票的字符串唯一idvalue为一个team_vote_item对象,用来存储单个投票相关的信息:

class Meta(property) team_vote_item: public spiritsaway::property::property_bag_item<std::string>
{
public:
	Meta(property(sync_self)) std::uint32_t m_vote_type = 0;
	Meta(property(sync_self)) std::uint64_t m_expire_ts;
	Meta(property(sync_self)) std::vector<std::uint64_t> m_vote_players; //sorted oids
	Meta(property(sync_self)) json m_extra_info;
	Meta(property(sync_self)) std::vector<team_vote_choice> m_vote_choices;
	#ifndef __meta_parse__
	#include "team/team_vote_item.generated.inch"
	#endif
public:
	bool has_vote_player(const std::uint64_t& oid) const
	{
		return std::binary_search(m_vote_players.begin(), m_vote_players.end(), oid);
	}

	bool has_voted(const std::uint64_t& oid) const
	{
		for(const auto& one_choice: m_vote_choices)
		{
			if(one_choice.oid == oid)
			{
				return true;
			}
		}
		return false;
	}
};
using team_vote_bag = spiritsaway::property::property_bag<team_vote_item>;

class Meta(property) team_prop
{
public:
	// 省略其他字段
	Meta(property(sync_self)) team_vote_bag m_team_votes;
public:
	std::uint64_t get_vote_win_oid(const team_vote_item& cur_vote_item) const;
};

由于这些投票数据只需要自己可见,不需要暴露给redis,所以属性同步flag设置为了sync_self,而不是sync_redis

team_vote_itemm_vote_players字段存储所有有资格投票的队伍成员id,这个字段在投票开始时就会被初始化,后续不会再改变。同时m_vote_choices存储所有已经投票信息team_vote_choice,这个结构体存储了队伍成员id、他们的投票结果以及对应的投票时间戳。

struct team_vote_choice
{
	std::uint64_t oid; // 在线id
	std::uint64_t ts;
	std::uint32_t value;
};

team_vote_itemm_vote_type字段用来存储投票的类型,目前有两种类型:

enum class team_vote_type
{
	invalid_vote = 0,
	unanimous_vote,
	fixed_players_vote,
};
  • 全员一致投票:vote_type = unanimous_vote,要求所有队员都来参加,投票结果为1表示同意,0表示拒绝,一旦任意一个队员投票结果为0,则投票以失败结束,所有成员都赞成时投票以成功结束。

  • 部分成员投票:vote_type = fixed_players_vote,限制为指定的成员集合参加,指定集合内的玩家都必须投票,当所有指定集合内的玩家都投票完毕后,投票结束。

bool team_check_handler::check_vote_finish(const property::team::team_vote_item& cur_vote)
{
	switch (cur_vote.vote_type())
	{
	case std::uint32_t(enums::team_vote_type::unanimous_vote):
	{
		if(cur_vote.vote_choices().size() == m_team_data.players().index().size())
		{
			return true;
		}
		for(const auto& one_choice: cur_vote.vote_choices())
		{
			if(one_choice.value == 0)
			{
				return true;
			}
		}
		return false;
	}
	
	case std::uint32_t(enums::team_vote_type::fixed_players_vote):
	{
		// all remain players has vote
		std::uint32_t remain_player_sz = 0;
		for(const auto& one_player: m_team_data.players().index())
		{
			if(std::binary_search(cur_vote.vote_players().begin(), cur_vote.vote_players().end(), one_player.first))
			{
				remain_player_sz++;
			}
		}
		if(!remain_player_sz)
		{
			return true;
		}
		for(const auto& one_choice: cur_vote.vote_choices())
		{
			if(m_team_data.players().get(one_choice.oid))
			{
				remain_player_sz--;
			}
		}
		return remain_player_sz==0;
	}
	default:
	{
		return true;
	}
	}
}

team_service上的投票接口并没有针对具体的投票类型来做区分,每个操作都只有一个接口:

  • 创建投票接口 void team_create_vote(const utility::rpc_msg& msg, const json& cur_vote_info,const std::uint64_t action_player_oid),这里的cur_vote_info其实就是team_vote_item对象的json表示:
property::team::team_vote_item cur_vote;
if(!cur_vote.decode(cur_vote_info))
{
	team_action_reply(cur_player_iter->second.anchor, cur_action_id, msg, std::uint32_t(enums::team_errcode::invalid_param));
	return;
}
  • 参与投票接口 void team_cast_vote(const utility::rpc_msg& msg, const std::string& vote_id, const json& cur_choice_info, const std::uint64_t action_player_oid),这里的cur_choice_info其实就是team_vote_choice对象的json表示:
property::team::team_vote_choice cur_vote_choice;
if(!cur_vote_choice.decode(cur_choice_info))
{
	team_action_reply(cur_player_iter->second.anchor, cur_action_id, msg, std::uint32_t(enums::team_errcode::invalid_param));
	return;
}

一旦有一个队员成功的创建了自己的投票结果,服务端就会在cast_vote之后调用erase_vote_when_end检查当前投票是否可以结束,如果结束了则可以删除指定的投票数据:

bool team_impl_handler::erase_vote_when_end(const std::string& vote_id, property::team::team_vote_item& detail)
{
	auto cur_vote_ptr = m_team_data.team_votes().get(vote_id);
	if(!cur_vote_ptr)
	{
		return false;
	}
	if(team_check_handler::check_vote_finish(*cur_vote_ptr))
	{
		detail = *cur_vote_ptr;
		m_team_proxy.team_votes().erase(vote_id);
		return true;
	}
	return false;
}

void team_service::team_cast_vote(const utility::rpc_msg& msg, const std::string& vote_id, const json& cur_choice_info, const std::uint64_t action_player_oid)
{
	// 省略之前的检查 和 投票代码 
	// 接下来开始判断投票是否结束 如果结束了 则删除投票数据
	if(cur_err == enums::team_errcode::ok)
	{
		property::team::team_vote_item cur_vote_detail;
		if(cur_team_ptr->m_handler.erase_vote_when_end(vote_id, cur_vote_detail))
		{
			std::vector<json> temp_sync_args;
			temp_sync_args.push_back(vote_id);
			team_sync_props(cur_team_ptr, std::uint8_t(enums::team_action::vote_finish), std::move(temp_sync_args));
			// 省略其他代码
		}
	}
}

这里的team_sync_props负责将当前队伍的数据进行增量同步到所有的在线玩家。当数据同步到玩家身上时,会利用team_prop_delta来执行数据的回放:

void player_team_component::team_prop_delta(const utility::rpc_msg& msg, std::uint8_t team_action_id, const std::vector<json>& action_args, const std::vector<json>& prop_deltas)
{
	m_owner->logger()->info("team_prop_delta receive action_id {} args {}, deltas {}", magic_enum::enum_name(enums::team_action(team_action_id)), msg.args[1].dump(), msg.args[2].dump());
	misc::team_prop_sync_event cur_prop_sync_event{false, action_args};
	m_owner->dispatcher().dispatch(enums::team_action(team_action_id), cur_prop_sync_event);
	for(const auto& one_prop: prop_deltas)
	{
		std::uint64_t offset;
		std::uint8_t cmd;
		std::uint64_t flag;
		json prop_data;
		if(!serialize::decode_multi(one_prop, offset, cmd, flag, prop_data))
		{
			m_owner->logger()->error("fail to decode team prop delta {}", one_prop.dump());
			continue;
		}
		auto cur_cmd_enum = spiritsaway::property::property_cmd(cmd);
		spiritsaway::property::property_record_offset cur_record_offset{offset};
		m_player->prop_proxy().team().replay(cur_record_offset.to_replay_offset(), cur_cmd_enum, prop_data);
		
	}
	cur_prop_sync_event.is_finish = true;
	m_owner->dispatcher().dispatch(enums::team_action(team_action_id), cur_prop_sync_event);
	
}

注意这里会前后执行两次team_prop_sync_eventdispatch,第一次是在属性修改之前,第二次是在属性修改之后。两次dispatchis_finish字段分别是falsetrue,然后这里的事件id是带类型的枚举enums::team_action,这里会根据不同的team_action来触发不同的事件。

player_team_component在初始化的时候就会注册vote_finish事件的监听器,当收到vote_finish这个事件时,会调用player_team_component::team_event_listener来处理,如果事件是vote_finishis_finishfalse,则会调用on_team_vote_finish来处理投票结果,此时相关的属性还没有被修改,可以通过这些属性来结算当前的投票结果:

bool player_team_component::init(const json& data)
{
	m_player = dynamic_cast<player_entity*>(m_owner);
	if(!m_player)
	{
		return false;
	}
	m_player->login_dispatcher().add_listener(&player_team_component::on_login, this);
	m_player->logout_dispatcher().add_listener(&player_team_component::on_logout, this);

	m_owner->dispatcher().add_listener(enums::team_action::vote_abort, &player_team_component::team_event_listener, this);
	m_owner->dispatcher().add_listener(enums::team_action::vote_finish, &player_team_component::team_event_listener, this);
	m_owner->dispatcher().add_listener(enums::team_action::vote_expire, &player_team_component::team_event_listener, this);
	m_team_handler = std::make_unique<misc::team_check_handler>(m_player->prop_data().team());
	return true;
}

void player_team_component::team_event_listener(const utility::enum_type_value_pair& ev_cat, const misc::team_prop_sync_event& detail)
{
	if(ev_cat.enum_type == utility::type_hash::hash<enums::team_action>())
	{
		switch (ev_cat.enum_value)
		{
		// 省略其他分支
		case std::uint32_t(enums::team_action::vote_finish):
		{
			if(detail.is_finish)
			{
				return;
			}
			std::vector<std::string> vote_ids;
			if(!serialize::decode(detail.sync_action_args, vote_ids))
			{
				return;
			}
			for(const auto& one_vote_id: vote_ids)
			{
				on_team_vote_finish(one_vote_id);
			}
			break;
		}
		
		default:
			break;
		}
	}
}

on_team_vote_finish中,会根据投票的类型来处理投票结果,这里主要是根据vote_type来判断投票的类型,然后根据投票的结果来触发不同的事件,一致性投票成功的时候发出unanimouse_vote_suc事件,失败的时候发出unanimouse_vote_fail事件, 而部分成员投票结束的时候发出player_win_vote事件:

void player_team_component::on_team_vote_finish(const std::string& vote_id)
{
	auto cur_vote_ptr = m_player->prop_data().team().team_votes().get(vote_id);
	if(!cur_vote_ptr)
	{
		return;
	}
	m_owner->logger()->info("on_team_vote_finish {} with info {}", vote_id, cur_vote_ptr->encode().dump());
	switch (cur_vote_ptr->m_vote_type)
	{
	case std::uint32_t(enums::team_vote_type::unanimous_vote):
	{
		bool suc = true;
		for(const auto& one_choice: cur_vote_ptr->vote_choices())
		{
			if(one_choice.value == 0)
			{
				suc = false;
				break;
			}
		}
		if(suc)
		{
			m_owner->dispatcher().dispatch(enums::team_vote_events::unanimouse_vote_suc, vote_id);
		}
		else
		{
			m_owner->dispatcher().dispatch(enums::team_vote_events::unanimouse_vote_fail, vote_id);
		}
		return;
	}
	case std::uint32_t(enums::team_vote_type::fixed_players_vote):
	{
		
		if(m_player->prop_data().team().get_vote_win_oid(*cur_vote_ptr) == m_owner->online_entity_id())
		{
			m_owner->logger()->info("{} win vote {} extra {}", m_owner->online_entity_id(), vote_id, cur_vote_ptr->extra_info().dump());
			m_owner->dispatcher().dispatch(enums::team_vote_events::player_win_vote, vote_id);
		}
		return;

	}
	
	default:
		break;
	}
}

这里的get_vote_win_oid会从已经投票的结果中获取当前优先级最高的作为结果,常见于队伍内需要分配一些稀有物品的时候,例如组队副本的装备掉落。策划会要求队员们进行一个roll点投票来决定谁能拿到这个物品。每个队员只能投一次票,服务端会随机生成对应的roll点数。最后系统会统计出所有队员的投票结果来决定物品的归属。

std::uint64_t team_prop::get_vote_win_oid(const team_vote_item& cur_vote_item) const
{
	std::uint64_t win_player_oid = 0;
	std::uint32_t win_score = 0;
	std::uint64_t win_ts = std::numeric_limits<std::uint64_t>::max();
	for(const auto& one_choice: cur_vote_item.vote_choices())
	{
		if(!players().get(one_choice.oid))
		{
			continue;
		}
		if(one_choice.value > win_score)
		{
			win_score = one_choice.value;
			win_ts = one_choice.ts;
			win_player_oid = one_choice.oid;
		}
		else if(one_choice.value == win_score)
		{
			if(win_ts > one_choice.ts)
			{
				win_ts = one_choice.ts;
				win_player_oid = one_choice.oid;
			}
		}
	}
	return win_player_oid;
}

组队副本进入投票

如果一组队伍想进入一个组队副本,客户端玩家会发起一个request_create_team_dungeon的请求,如果检查通过,就会发起一个全员一致性投票,这里会设置好extra_info字段,存储要进入的场景信息,同时对应的投票id被强制指定为team_dungeon,因为同一个时刻只允许进入一个组队副本:

void player_space_component::request_create_team_dungeon(const utility::rpc_msg& msg, std::uint32_t space_no)
{
	const auto& cur_team_prop = m_player->prop_data().team();
	const auto& cur_space_prop = m_player->prop_data().space();
	utility::rpc_msg reply_msg;
	// 省略条件检查相关代码
	reply_msg.cmd = "reply_create_team_dungeon";
	reply_msg.args.push_back(space_no);
	m_player->call_client(reply_msg);
	if(!reply_msg.err.empty())
	{
		return;
	}
	m_player->prop_proxy().space().last_team_dungeon_request_ts().set(utility::timer_manager::now_ts());

	auto cur_team_comp = m_player->get_component<player_team_component>();
	if(!cur_team_comp)
	{
		return;
	}
	json dungeon_vote_extra;
	dungeon_vote_extra["space_no"] = space_no;
	dungeon_vote_extra["space_id"] = m_owner->gen_unique_str();
	cur_team_comp->team_create_vote_impl("team_dungeon", std::uint32_t(enums::team_vote_type::unanimous_vote), dungeon_vote_extra);
}

然后当玩家身上通过team_prop_delta接收到unanimouse_vote_suc通知的时候,会判断当前是否是组队副本的进入投票,如果是则会解析出其中的参数space_nospace_id,填充对应的参数往space_service上发起一个request_create_space的请求,来创建一个新的场景:

void player_space_component::on_team_dungeon_vote_finish(const utility::enum_type_value_pair& ev_cat, const std::string& vote_id)
{
	if(ev_cat.enum_value != std::uint32_t(enums::team_vote_events::unanimouse_vote_suc) || vote_id != "team_dungeon")
	{
		return;
	}
	const auto& cur_team_prop = m_player->prop_data().team();
	const auto& cur_space_prop = m_player->prop_data().space();
	if(cur_team_prop.leader_oid() != m_owner->online_entity_id())
	{
		return;
	}
	if(cur_space_prop.entering_space_no())
	{
		return;
	}
	const auto cur_space = m_owner->get_space();
	if(!cur_space || !cur_space->space_type_info()->is_town_space)
	{
		return;
	}
	const auto& cur_vote_info = cur_team_prop.team_votes().get("team_dungeon")->extra_info();
	std::uint32_t dest_space_no = 0;
	std::string dest_space_id;
	try
	{
		cur_vote_info.at("space_no").get_to(dest_space_no);
		cur_vote_info.at("space_id").get_to(dest_space_id);
	}
	catch(const std::exception& e)
	{
		m_owner->logger()->error("on_team_dungeon_vote_finish read vote space info fail with {}", e.what());
		return;
	}
	
	utility::rpc_msg create_space_msg;
	json::object_t space_init_info;
	space_init_info["team_id"] = cur_team_prop.id();
	create_space_msg.cmd = "request_create_space";
	create_space_msg.set_args(dest_space_no, dest_space_id, std::string(), space_init_info);
	m_owner->call_service("space_service", create_space_msg);
}

space_service成功的创建好了对应的组队副本之后,会在对应的上报处理接口space_service::report_space_created里将这个信息发送到team_service:

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);

team_service收到这个通知之后,会往这个队伍内所有的在线玩家发送一个enums::team_action::dungeon_created事件,来通知场景创建好了,同时将场景的space_idspace_no填充到队伍的team_dungeon_space_idteam_dungeon_space_no字段里:

void team_service::notify_team_dungeon_created(const utility::rpc_msg& msg, const std::string& tid, std::uint32_t space_no, const std::string& space_id , const std::string& game_id)
{
	auto cur_team_iter = m_team_resources.find(tid);
	if(cur_team_iter == m_team_resources.end())
	{
		return;
	}
	cur_team_iter->second->m_prop_proxy.team_dungeon_space_id().set(space_id);
	cur_team_iter->second->m_prop_proxy.team_dungeon_space_no().set(space_no);
	team_sync_props(cur_team_iter->second.get(), std::uint8_t(enums::team_action::dungeon_created), {});
}

最后当这个属性修改同步到玩家身上的时候,就会发起一个request_enter_space的请求,来进入到组队副本的场景里,注意此时的is_finishtrue,表示属性同步完成,因为只有属性同步完成之后team_dungeon_space_idteam_dungeon_space_no才会被填充好:

void player_space_component::on_team_dungeon_created(const utility::enum_type_value_pair& ev_cat, const misc::team_prop_sync_event& detail)
{
	if(ev_cat.enum_value != std::uint32_t(enums::team_action::dungeon_created) || !detail.is_finish)
	{
		return;
	}
	const auto& cur_team_prop = m_player->prop_data().team();
	const auto& cur_space_prop = m_player->prop_data().space();
	if(cur_space_prop.entering_space_no())
	{
		return;
	}
	const auto cur_space = m_owner->get_space();
	if(!cur_space || !cur_space->space_type_info()->is_town_space)
	{
		return;
	}
	const auto& dest_space_id = cur_team_prop.team_dungeon_space_id();
	auto dest_space_no = cur_team_prop.team_dungeon_space_no();
	utility::rpc_msg enter_msg;
	enter_msg.cmd = "request_enter_space";
	json::object_t enter_info;
	enter_msg.set_args(dest_space_no, dest_space_id, enter_info);
	m_owner->rpc_owner_on_rpc(enter_msg);
}

上述就是组队副本的完成进入流程。

组队副本进入投票

道具奖励Roll点投票

在组队副本中,击杀一些怪物的时候会随机的掉落一些奖励,为了决定每个奖励的归属,副本会自动的发起一个Roll点投票,这个Roll点投票的发起是在monster_death_component::on_killed_by里,会构造一个notify_team_dungeon_monster_kill_rewardrpc来通知team_service,注意到这里的team_roll_reward是一个数组,代表一个怪物可能掉落多个物品奖励:

void monster_death_component::on_killed_by(actor_entity* killer, const std::string& killer_proxy, const hit_record& hit_info, double hit_dmg)
{
	
	auto cur_monster_sysd = m_monster->monster_sysd();
	auto cur_space = m_owner->get_space();
	if(cur_space->space_type_info()->is_team_dungeon)
	{
		std::vector<std::uint32_t> team_roll_reward;
		cur_monster_sysd.expect_value(std::string("team_roll_reward"), team_roll_reward);
		if(!team_roll_reward.empty())
		{
			utility::rpc_msg team_reward_msg;
			team_reward_msg.cmd = "notify_team_dungeon_monster_kill_reward";
			team_reward_msg.set_args(cur_space->team_id(), cur_space->space_no(), cur_space->entity_id(), team_roll_reward);
		}
	}
	// 省略其他代码
}

team_service::notify_team_dungeon_monster_kill_reward里会根据rewards里的道具编号,来创建一个或者多个team_vote_item,并将这个投票项发送到队伍内的所有玩家,玩家在投票的时候会根据reward_no来判断哪个道具是自己投票的目标:

void team_service::notify_team_dungeon_monster_kill_reward(const utility::rpc_msg& msg, const std::string& tid, std::uint32_t space_no, const std::string& space_id, std::vector<std::uint32_t>& rewards)
{
	auto cur_team_iter = m_team_resources.find(tid);
	if(cur_team_iter == m_team_resources.end())
	{
		return;
	}
	auto cur_team_ptr = cur_team_iter->second.get();
	for(auto one_reward: rewards)
	{
		property::team::team_vote_item cur_vote;
		cur_vote.m_expire_ts = utility::timer_manager::now_ts() + 30 * 1000;
		cur_vote.m_extra_info["reward_no"] = one_reward;
		cur_vote.m_extra_info["space_id"] = space_id;
		cur_vote.m_extra_info["space_no"] = space_no;
		cur_vote.m_id = get_server()->gen_unique_str();
		cur_vote.m_vote_type = std::uint32_t(enums::team_vote_type::fixed_players_vote);
		cur_vote.m_vote_players.reserve(cur_team_ptr->m_prop.m_players.index().size());
		for(const auto& one_pair: cur_team_ptr->m_prop.m_players.index())
		{
			cur_vote.m_vote_players.push_back(one_pair.first);
		}

		auto cur_err = cur_team_ptr->m_handler.create_vote(cur_vote, cur_team_ptr->m_prop.leader_oid());
		if(cur_err == enums::team_errcode::ok)
		{
			std::vector<json> temp_sync_args;
			team_sync_props(cur_team_ptr, std::uint32_t(enums::team_action::create_vote), std::move(temp_sync_args));
		}
	}
}

player_actorplayer_dungeon_component会在初始化的时候注册对player_win_vote的事件监听。当收到player_win_vote事件时,会调用player_dungeon_component::on_player_win_vote来处理,在这个函数中会根据投票的结果来判断哪个玩家应该获得投票的奖励,这里会从team_vote_item创建时就初始化好的extra_info里拿出reward_no字段,作为道具进行发送:

bool player_dungeon_component::init(const json& data)
{
	m_player = dynamic_cast<player_entity*>(m_owner);
	if(!m_player)
	{
		return false;
	}
	m_owner->dispatcher().add_listener(enums::team_vote_events::player_win_vote, &player_dungeon_component::event_listener, this);
	return true;
}

void player_dungeon_component::event_listener(const utility::enum_type_value_pair& ev_cat, const std::string& detail)
{
	if(ev_cat == utility::enum_type_value_pair(enums::team_vote_events::player_win_vote))
	{
		auto cur_vote_info = m_player->prop_data().team().team_votes().get(detail);
		if(!cur_vote_info)
		{
			return;
		}
		const auto& cur_vote_extra = cur_vote_info->extra_info();
		auto temp_iter = cur_vote_extra.find("reward_no");
		if(temp_iter == cur_vote_extra.end() || !temp_iter->is_number_unsigned())
		{
			return;
		}
		auto cur_stuff_comp = m_player->get_component<player_stuff_component>();
		m_owner->logger()->info("get stuff reward {} from vote {}", cur_vote_extra.dump(), detail);
		cur_stuff_comp->add_stuff(temp_iter->get<std::uint32_t>(), 1);

	}
}

上述就是组队副本里的道具奖励Roll点投票的流程。

组队副本道具奖励投票