之前开发游戏的时候,为了能够在任意地方都能访问到数据,一般的做法是建立一个数据池,然后把想要的数据全部放到数据池类中。数据池的写法一般是建一个名为DataPool的单例类,通常情况下对单例的实现一般是这样的
DataPool.h
#pragma once
class DataPool{
public:
static DataPool* getInstance();
};
DataPool.cpp
#include <stdio.h>
#include "DataPool.h"
DataPool* g_instance = NULL;
DataPool* DataPool::getInstance(){
if(NULL == g_instance){
g_instance = new DataPool();
}
return g_instance;
}
为了方便演示数据池的工作方式,以下用游戏中常会涉及到用户账户信息和登录信息进行举例。通常情况下会建立两个类,这边暂时定为AccountInfo和LoginInfo。简单实现一下,类似下面:
用户信息类AccountInfo
AccountInfo.h
#pragma once
class AccountInfo
{
public:
AccountInfo(void);
~AccountInfo(void);
};
AccountInfo.cpp
#include "AccountInfo.h"
AccountInfo::AccountInfo(void){
}
AccountInfo::~AccountInfo(void){
}
登录信息类LoginInfo
LoginInfo.h
#pragma once
class LoginInfo
{
public:
LoginInfo(void);
~LoginInfo(void);
};
LoginInfo.cpp
#include "LoginInfo.h"
LoginInfo::LoginInfo(void){
}
LoginInfo::~LoginInfo(void){
}
既然这两个类建好了,那么下一步就是把它们丢到DataPool中,所以DataPool的代码也相应的更改
#pragma once
#include "AccountInfo.h"
#include "LoginInfo.h"
class DataPool{
public:
static DataPool* getInstance();
public:
AccountInfo m_AccountInfo;
LoginInfo m_LoginInfo;
};
这边说的丢到DataPool中,其实就是把这两个类的实例作为DataPool的成员变量,并且把访问方式定位public,以后要访问的时候一般是
DataPool* pool = DataPool::getInstance();
AccountInfo& account = pool->m_AccountInfo;
LoginInfo& login = pool->m_LoginInfo;
也就是说所有的数据都要通过DataPool的单例访问,这样AccountInfo和LoginInfo作为DataPool的成员变量,也就拥有了单例的性质。同时也不用去管AccountInfo和LoginInfo的内存释放,因为他们的是以栈上对象的方式来声明的,其生命周期是随着DataPool的单例来管控的。DataPool什么时候释放,它们也跟着什么时候释放。
当然这样做法问题也是非常多的,比方说如果一个不小心把引用符号&去掉了,那么数据就会被拷贝一份。想象一下下面的写法:
LoginInfo login = pool->m_LoginInfo;//这边LoginInfo不是声明为LoginInfo&方式的,所以LoginInfo的数据就被拷贝
还有一个更让人头疼的问题是,由于把所有的数据都丢到DataPool中,那么相应的也得把这些数据类的头文件也包含到DataPool的头文件中,像上面的
#pragma once
//看到了没,需要这两个头文件
#include "AccountInfo.h"
#include "LoginInfo.h"
class DataPool{
public:
static DataPool* getInstance();
public:
AccountInfo m_AccountInfo;
LoginInfo m_LoginInfo;
};
包含头文件本身没什么问题。问题是DataPool在开发过程中会被很多人修改,一般会有3到5个人在改它。而游戏中基本上每个业务逻辑类都要用到它,这样就造成了一个问题,DataPool随便一修改,相应的会有上百个文件也一起被编译。在没用联合编译工具的情况下,最可观的等待编译的时间是半个小时吧。
这些都是以前用在游戏开发的实现方案,一般是谁用谁苦逼。为此,需要对其进行改进。改进的技巧是用到了类成员模板函数以及typeid这个运行时运算符。
typeid这个运算符能够根据类型来返回类型的名称,举个例子
const char* name = typeid(AccountInfo).name();
printf(name);//输出结果将是class AccountInfo
name = typeid(LoginInfo).name();
printf(name);//输出结果将是class LoginInfo
接着看看DataPool改进后的样子
#pragma once
#include <stdio.h>
#include <typeinfo>
#include <map>
#include <string>
using namespace std;
class DataPool{
public:
static DataPool* getInstance();
public:
template<class T> T* get(){
const char* name = typeid(T).name();
std::map<std::string, void*>::iterator target = m_mapInstance.find(name);
if(target != m_mapInstance.end()){
return (T*)target->second;
}
else{
T* instance = new T();
m_mapInstance.insert(make_pair(name, instance));
return instance;
}
return NULL;
}
protected:
std::map<std::string, void*> m_mapInstance;
};
它的用法是
DataPool* pool = DataPool::getInstance();
LoginInfo* info = pool->get<LoginInfo>();
LoginInfo* info2 = pool->get<LoginInfo>();
AccountInfo* account = pool->get<AccountInfo>();
在上面例子中info和info2这两个指针所指的对象是一样的,因为DataPool中对相同类型的数据进行了缓存。这样就可以在不改动DataPool的情况下来全局访问自己想要的数据,只要你通过get这个类成员模板函数。
现在看看get()模板函数的实现,这个函数的用途就是根据模板参数T以及typeid来取得该模板参数的类型名称
const char* name = typeid(T).name();//正如前面对typeid讲解的那样,这边将根据T的类型来返回T所对应的类型名称
然后再在map中寻找之前这个模板参数的是否有缓存过,有的话直接用之前的实例。也就是这段代码
std::map<std::string, void*>::iterator target = m_mapInstance.find(name);
if(target != m_mapInstance.end()){
return (T*)target->second;
}
如果没有的,那么就创建一个实例,然后把它加入到map中
else{
T* instance = new T();
m_mapInstance.insert(make_pair(name, instance));//map的key为类型名称,value为分配出来的实例。
return instance;
}
通过以上改进之后,DataPool就不在依赖于特定数据的头文件了,同时也让DataPool固定下来,不会有大改动。自此,改进完成,有需要的各位可以试试。