主要目的是完成游戏客户端内的事件处理。这里要明确几件事情:
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又多。