主要目的是完成游戏客户端内的事件处理。这里要明确几件事情:
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 <deque>
- #include <boost/any.hpp>
-
- #ifdef __APPLE__
- #include <tr1/functional>
- using std::tr1::function;
- using std::tr1::bind;
- using namespace std::tr1::placeholders;
-
- namespace std {
- template<class _Ty> struct _Remove_reference
- {
- typedef _Ty _Type;
- };
-
- template<class _Ty> inline typename _Remove_reference<_Ty>::_Type&& move(_Ty&& _Arg)
- {
- return ((typename _Remove_reference<_Ty>::_Type&&)_Arg);
- }
- };
- #else
- #include <functional>
- 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
-
- #define PARAM_IMPL_RET(t, it,value) operator t () { t ret = value; ret = boost::any_cast<it>(m_data); return std::move(ret); };
- #else
- #define PARAM_IMPL_RET(t, it, value) operator t () { t ret = value; try { ret = boost::any_cast<it>(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::any m_data;
- };
-
- typedef function<void(uint32, Parameter, Parameter, Parameter, Parameter)> FuncCallback;
- #define LOCK_QUEUE MyLock l(&m_mutex)
- class CommandQueue : public MySingleton<CommandQueue>
- {
- 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<typename Type>
- 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<typename Type1, typename Type2>
- 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<typename Type1, typename Type2, typename Type3>
- 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<typename Type1, typename Type2, typename Type3, typename Type4>
- 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<typename Type>
- 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<typename Type1, typename Type2>
- 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<typename Type1, typename Type2, typename Type3>
- 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<typename Type1, typename Type2, typename Type3, typename Type4>
- 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<Parameter> parameters;
- };
-
- MyMutex m_mutex;
- std::deque<COMMAND_DATA> m_queue;
- std::map<uint32, FuncCallback> 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<typename Type>
- inline void post_command(uint32 dwCommand, Type data)
- {
- CommandQueue::getSingleton().post(dwCommand, data);
- }
-
- template<typename Type1, typename Type2>
- inline void post_command(uint32 dwCommand, Type1 data1, Type2 data2)
- {
- CommandQueue::getSingleton().post(dwCommand, data1, data2);
- }
-
- template<typename Type1, typename Type2, typename Type3>
- inline void post_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
- {
- CommandQueue::getSingleton().post(dwCommand, data1, data2, data3);
- }
-
- template<typename Type1, typename Type2, typename Type3, typename Type4>
- 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<typename Type>
- inline void send_command(uint32 dwCommand, Type data)
- {
- CommandQueue::getSingleton().send(dwCommand, data);
- }
-
- template<typename Type1, typename Type2>
- inline void send_command(uint32 dwCommand, Type1 data1, Type2 data2)
- {
- CommandQueue::getSingleton().send(dwCommand, data1, data2);
- }
-
- template<typename Type1, typename Type2, typename Type3>
- inline void send_command(uint32 dwCommand, Type1 data1, Type2 data2, Type3 data3)
- {
- CommandQueue::getSingleton().send(dwCommand, data1, data2, data3);
- }
-
- template<typename Type1, typename Type2, typename Type3, typename Type4>
- 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又多。