一、介绍
在rpg这类游戏中,有大量的实体存在,实体包括玩家、NPC、怪物、坐骑、宠物等等,每天实体有一个id,这个id是在实体创建的时候生成的,我们称之为uid,通过uid我们可以获取到实体对象。(对于玩家实体它们还有一个id,是在玩家注册账号的时候db生成的id,我们称之为dbid,用于数据库中记录玩家唯一标记。)
二、uid设计
uid是实体的唯一标记,通过uid我们需要查询到实体对象。uid设计重要考虑的是怎样通过uid查询到实体对象。这里有几种设计方案:
方案一:map存储
map的key值为uid,value为实体对象指针。查询时间复杂度lg(n),map底层是红黑数当实体创建或删除的时候红黑树需要进行调整,当实体删除或创建频繁的时候性能消耗也会增大。
方案二:数组索引
uid是服务器自己生成的,那我们可以让uid从1连续递增生成,通过索引搜索效率可以为常数级。当有实体创建的时候可以一次创建一批,供后续实体创建使用,实体删除可以将uid回收用于后续新创建的实体使用。
实现过程,定义一个数组,用于记录当前服务器中创建的实体,通过uid可以直接搜索到实体对象指针,再定义一个列表用于记录空闲的实体,当我们需要一个uid对象的时候,我们去列表中获取一个,如果列表为空,我们将数组进行扩容,并将新创建的空闲实体添加到列表中。
总结,通过方案一和方案二对比,方案二效果跟好。
三、类结构演示
3.1 结构定义
下图为一个实现例子。
- CGlobalServer: 服务器全局对象,服务器的主对象。
- CEntityWorld:实体世界,管理整个服务器的实体。
- IEntity:实体接口。
- ConcreteEntity:一个具体的实体,可能是NPC、怪物、坐骑、宠物等等。
- SEntity:一个实体序列号和实体指针组合,我们可以将其转为一个64位整数,也就是定义的uid。
一个uid定义如下,UID 和 SEntity结构是等价的可以相互转换。
typedef LONGLONG UID;
struct ___UID
{
DWORD dwSerialNo; // 序列号,不可逆,单通过此ID也可以标识对像
DWORD dwHandle; // 对像句柄,可重复的(此成员需放后面,uid小点)
};
// 分解出序列号
#define ANALYZEUID_SERIALNO(uid) (((___UID *)&uid)->dwSerialNo)
// 分解出实体句柄
#define ANALYZEUID_HANDLE(uid) (((___UID *)&uid)->dwHandle)
3.2 uid构建和或取对象
uid构建
一个具体的实体ConcreteEntity,创建完成会把自己添加到实体世界中,也就是调用函数CEntityWorld::Add(),CEntityWorld::Add()会调用BuildUID(),为实体构建一个uid。构建uid过程,先判断m_listUsableEntity中是否还有对象,如果有弹出一个用于当前新创建的实体,如果没有先对数组m_pEntityArray进行扩容(可以一次加1W个),将之前数据添加到新的数组中,并将新创建的对象添加到m_listUsableEntity中,用于新创建的对象。当一个对象使用完毕,将对象uid添加到m_listUsableEntity中用于后续创建的实体对象。
通过uid获取实体对象,直接看代码,直观清晰。
IEntity * CEntityWorld::Get(LONGLONG uid)
{
if(uid == INVALID_UID)
{
return NULL;
}
// 解析
DWORD dwSerialNo = ANALYZEUID_SERIALNO(uid);
DWORD dwHandle = ANALYZEUID_HANDLE(uid);
// 句柄越过了,非法
if(dwHandle >= m_dwHandleMaxCount)
{
return NULL;
}
IEntity * pEntity = m_pEntityArray[dwHandle].pEntity;
if(pEntity == NULL)
{
return NULL;
}
// 要判断序列号
if(m_pEntityArray[dwHandle].dwSerialNo != dwSerialNo)
{
return NULL;
}
return pEntity;
}