基于id的游戏客户端事件分发(消息队列)

主要目的是完成游戏客户端内的事件处理。这里要明确几件事情:

1、同样一个方案在小项目中可能是漂亮的解决方案,但是在大项目中就是很不灵活的处理方法。游戏客户端无论如何都是小项目,不会像windows一样几千位工程师负责上千个dll,一起开发一个操作系统。所以,对我的需求而言,把模块分的很细,很独立意义不是很大,点到即止。

2、这样一个事件分发机制主要是处理业务逻辑对gui的控制,或者是不同的界面之间的交互。比如服务器下发一个消息给客户端,然后客户端要刷新物品界面;或者是竞技场界面打开后,通知打开玩家列表界面等等。单纯的点击一个按钮,然后响应按钮事件不需要用到这个功能。

3、这个模块就像ios的通知中心一样,可以最大程度的解耦合,与此对应的还有一个方案是基于接口观察者模式事件通知。我个人更倾向于id的方案,如果是接口的话,一方面要维护接口的一致性,另一方面要经常使用到c++最恶心,最容易出问题的多继承(我碰到过好几个非常隐晦的bug就是多继承内存排布引发的)。


基本实现思路是这样的:

1、先实现一个委托机制以方便的实现函数回调,这里我用的是fast_delegate,也可以选择boost::function。

2、写一个CommandQueueMgr来管理所有的id(一个枚举,由开发者进行统一维护,多个开发者可以分配多个id段,也可以写在各自的头文件里面以避免冲突),并提供注册事件,解除注册,响应事件等功能

3、每个需要事件响应的模块(界面)自己在初始化的时候进行注册(如果没有统一的入口,就写个静态变量来调用初始化函数)。这样就有了一个id和函数的对应关系。

4、原本需要调用函数的地方,执行PostMessage函数,发送一个id事件。调用者根本不需要知道被调用者是什么,如果这个id有对应函数就执行,否则就忽略(也可以写上log)。

5、根据实际需要可以写一个异步的PostMessage和一个同步的SendMessage

6、PostMessage可以只支持一个int参数,就像windows一样,这样会更加清晰。也可以支持多个参数,这样就要求开发者在写id的时候把对应参数的注释写好,否则会很混乱


#pragma once

#include 
#include 

#ifdef __APPLE__
#include 
using std::tr1::function;
using std::tr1::bind;
using namespace std::tr1::placeholders;

namespace std {
    template struct _Remove_reference
    {
        typedef _Ty _Type;
    };
    
    template inline typename _Remove_reference<_Ty>::_Type&& move(_Ty&& _Arg)
    {
        return ((typename _Remove_reference<_Ty>::_Type&&)_Arg);
    }
};
#else
#include 
using std::function;
using std::bind;
using namespace std::placeholders;
#endif

#include "MySingleton.h"
#include "MyThread.h"



class Parameter
{
public:
	#define PARAM_IMPL_INIT(t) Parameter(t para) { m_data = para; }

#ifdef WIN32
	// windows下面类型转换失败抛出异常
	#define PARAM_IMPL_RET(t, it,value) operator t () { t ret = value;  ret = boost::any_cast(m_data);  return std::move(ret); };
#else
	#define PARAM_IMPL_RET(t, it, value) operator t () { t ret = value; try { ret = boost::any_cast(m_data); } catch (...) {} return std::move(ret); };
#endif
	
	PARAM_IMPL_INIT(int);
	PARAM_IMPL_INIT(long long);
	PARAM_IMPL_INIT(double);
	PARAM_IMPL_INIT(const std::string&);
	PARAM_IMPL_INIT(boost::any);

	PARAM_IMPL_RET(bool, int, false);
	PARAM_IMPL_RET(char, int, 0);
	PARAM_IMPL_RET(unsigned char, int, 0);
	PARAM_IMPL_RET(short, int, 0);
	PARAM_IMPL_RET(unsigned short, int, 0);
	PARAM_IMPL_RET(int, int, 0);
	PARAM_IMPL_RET(unsigned int, int, 0);
	PARAM_IMPL_RET(long, int, 0);
	PARAM_IMPL_RET(unsigned long, int, 0);
	PARAM_IMPL_RET(long long, long long, 0);
	PARAM_IMPL_RET(unsigned long long, long long, 0);
	PARAM_IMPL_RET(float, double, 0.0f);
	PARAM_IMPL_RET(double, double, 0.0);
	PARAM_IMPL_RET(std::string, std::string, "");

	Parameter(const char* param)
	{
		m_data = std::string(param);
	}

	operator boost::any() {
		return std::move(m_data);
	}
private:
//	boost::variant m_data;
	boost::any m_data;
};

typedef function FuncCallback;
#define LOCK_QUEUE MyLock l(&m_mutex)
class CommandQueue : public MySingleton
{
public:
	CommandQueue() {};
	~CommandQueue() {};

	void registerHandler(uint32 command, FuncCallback callback)
	{
		m_eventMap[command] = callback;
	}

	void unRegisterHandler(uint32 command)
	{
		 auto itr = m_eventMap.find(command);
		 if (itr != m_eventMap.end())
		 {
			 m_eventMap.erase(itr);
		 }
	}

	void post(uint32 dwCommand)
	{
		COMMAND_DATA cmdData;
		cmdData.dwCommand = dwCommand;

		LOCK_QUEUE;
		m_queue.push_back(std::move(cmdData));
	}

	template
	void post(uint32 dwCommand, Type data)
	{
		COMMAND_DATA cmdData;
		cmdData.dwCommand = dwCommand;
		cmdData.parameters.push_back(std::move(data));

		LOCK_QUEUE;
		m_queue.push_back(std::move(cmdData));
	};

	template
	void post(uint32 dwCommand, Type1 data1, Type2 data2)
	{
		COMMAND_DATA cmdData;
		cmdData.dwCommand = dwCommand;
		cmdData.parameters.push_back(std::move(data1));
		cmdData.parameters.push_back(std::move(data2));

		LOCK_QUEUE;
		m_queue.push_back(std::move(cmdData));
	};

	template
	void post(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
	{
		COMMAND_DATA cmdData;
		cmdData.dwCommand = dwCommand;
		cmdData.parameters = {data1, data2, data3};

		LOCK_QUEUE;
		m_queue.push_back(std::move(cmdData));
	};

	template
	void post(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3, Type4 data4)
	{
		COMMAND_DATA cmdData;
 		cmdData.dwCommand = dwCommand;
		cmdData.parameters = {data1, data2, data3, data4};

		LOCK_QUEUE;
		m_queue.push_back(cmdData);
	};

	void send(uint32 dwCommand)
	{
		auto itr = m_eventMap.find(dwCommand);
		if (itr != m_eventMap.end()) {
			itr->second(dwCommand, 0, 0, 0, 0);
		}
	};

	template
	void send(uint32 dwCommand, Type data)
	{
		auto itr = m_eventMap.find(dwCommand);
		if (itr != m_eventMap.end()) {
			itr->second(dwCommand, data, 0, 0, 0);
		}
	};

	template
	void send(uint32 dwCommand, Type1 data1, Type2 data2)
	{
		auto itr = m_eventMap.find(dwCommand);
		if (itr != m_eventMap.end()) {
			itr->second(dwCommand, data1, data2, 0, 0);
		}
	};

	template
	void send(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
	{
		auto itr = m_eventMap.find(dwCommand);
		if (itr != m_eventMap.end()) {
			itr->second(dwCommand, data1, data2, data3, 0);
		}
	};

	template
	void send(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3, Type4 data4)
	{
		auto itr = m_eventMap.find(dwCommand);
		if (itr != m_eventMap.end()) {
			itr->second(dwCommand, data1, data2, data3, data4);
		}
	};

	void dispatchAll()
	{
		LOCK_QUEUE;
		while (!m_queue.empty()) {
			COMMAND_DATA& cmdData = m_queue.front();
			int size = cmdData.parameters.size();
			auto itrCall = m_eventMap.find(cmdData.dwCommand);
			switch (size)
			{
			case 0:
				itrCall->second(cmdData.dwCommand, 0, 0, 0, 0);
				break;
			case 1:
				itrCall->second(cmdData.dwCommand, cmdData.parameters[0], 0, 0, 0);
				break;
			case 2:
				itrCall->second(cmdData.dwCommand, cmdData.parameters[0], cmdData.parameters[1], 0, 0);
				break;
			case 3:
				itrCall->second(cmdData.dwCommand, cmdData.parameters[0], cmdData.parameters[1], cmdData.parameters[2], 0);
				break;
			case 4:
				itrCall->second(cmdData.dwCommand, cmdData.parameters[0], cmdData.parameters[1], cmdData.parameters[2], cmdData.parameters[3]);
				break;
			}

			m_queue.pop_front();
		}
	}

private:
	struct COMMAND_DATA {
		uint32 dwCommand;
		std::vector parameters;
	};

	MyMutex m_mutex;							// 递归锁,处理多线程异步消息抛送
	std::deque m_queue;			// post异步command处理队列(都在主线程处理,每帧结束的时候执行)
	std::map m_eventMap;	// 注册的命令和回调函数映射
};

inline void reg_command(uint32 command, FuncCallback callback)
{
	CommandQueue::getSingleton().registerHandler(command, callback);
}

inline void unreg_command(uint32 command)
{
	CommandQueue::getSingleton().unRegisterHandler(command);
}

inline void post_command(uint32 dwCommand)
{
	CommandQueue::getSingleton().post(dwCommand);
}

template
inline void post_command(uint32 dwCommand, Type data)
{
	CommandQueue::getSingleton().post(dwCommand, data);
}

template
inline void post_command(uint32 dwCommand, Type1 data1, Type2 data2)
{
	CommandQueue::getSingleton().post(dwCommand, data1, data2);
}

template
inline void post_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
{
	CommandQueue::getSingleton().post(dwCommand, data1, data2, data3);
}

template
inline void post_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3, Type4 data4)
{
	CommandQueue::getSingleton().post(dwCommand, data1, data2, data3, data4);
}




inline void send_command(uint32 dwCommand)
{
	CommandQueue::getSingleton().send(dwCommand);
}

template
inline void send_command(uint32 dwCommand, Type data)
{
	CommandQueue::getSingleton().send(dwCommand, data);
}

template
inline void send_command(uint32 dwCommand, Type1 data1, Type2 data2)
{
	CommandQueue::getSingleton().send(dwCommand, data1, data2);
}

template
inline void send_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
{
	CommandQueue::getSingleton().send(dwCommand, data1, data2, data3);
}

template
inline void send_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3, Type4 data4)
{
	CommandQueue::getSingleton().send(dwCommand, data1, data2, data3, data4);
}


这个东西的好处就是完全解耦合,写起来也非常方便,用到什么功能就添一个id就好了。假设服务器下发一个消息通知物品改变了,那么我们需要刷新物品界面。这个时候定义一个id CMD_REFRESH_GOODS ,然后在界面的初始化代码里面注册这个id (reg_command(CMD_REFRESH_GOODS, std::bind(&MyGoodsDialog::Refresh, this))),Refresh函数可以是任意形式,任意参数。消息处理代码里面只需要调用 send_command(CMD_REFRESH_GOODS),就可以完成界面刷新,无论是界面还是逻辑都不需要包含或继承任何东西,它们只需要知道这个id就可以了。 如果有多个消息会刷新界面那也非常简单,在各自的消息里面调用send_command代码就可以了。


这里也顺便说一下我对游戏客户端各模块依赖关系的看法,说真心话,游戏客户端真的是小项目。那么作为这个小项目,没有必要过分关注耦合,模块化之类的东西。凡是能让我的代码变得整洁,能让我写代码写的顺手方便的功能就是好功能,即便是约束也乐于遵守。但是如果为了模块化,而让我写代码时写个Impl,再写个provider,那会让人恶心透了,事实证明,有些模块化纯粹是杞人忧天。有些复用性的想法纯粹是自作多情,比如这种--你看我的代码模块化的多好,逻辑模块一行代码不用改就可以用到其他项目。我想说,如果你的游戏赚钱了,我们要再做一个类似的项目,那么我就算把你的ui模块也搬过来又有什么问题。如果我们要做的是一个新的项目,那么我还是要从那一坨代码中找到我想要的可以复用的东西,那还不如让代码变得简单,直接些,我更容易理解。

作为游戏客户端,有三个主要模块就足够了,逻辑模块、渲染模块、ui模块,所有跟引擎打交道的地方都停留在渲染模块,渲染模块是对引擎的再封装,即便有少量东西扩散到ui模块也应该只停留在ui控件内部。逻辑模块只负责并且负责完全的逻辑,这也是为什么逻辑层不能引入ui元素的原因。

逻辑层的东西就是一个一个的管理类,负责游戏的各个业务逻辑。 渲染层是对引擎的再封装,比如封装一个人物渲染类,负责渲染人物(逻辑层里还应该有一个人物类来处理业务逻辑,比如姓名、帮派,这个是组合关系)。 ui层就是一个一个的界面。 渲染层封装好后可以几个月不动,逻辑层和ui层一一对应,完成一个业务逻辑就是逻辑层添加一个管理类以及ui层添加几个对话框。他们之间相对独立,有交互就靠上面提到的事件中心来调度。

这样一个事件中心,看着是非常简单的东西,但是却是整个客户端的基础架构,一个好的架构可以让人更加快速的完成功能(而当架构搭建好后,90%的时间都是在写业务逻辑和界面逻辑),而不好的架构可能让我们写代码又慢,bug又多。

你可能感兴趣的:(基于id的游戏客户端事件分发(消息队列))