所谓的屏是指在一定范围内的实体的总称,一般是使用9屏(屏的单位大小是网格)。使用9屏意思是视野范围是9屏,每个屏是1/2屏幕的宽高的大小,别人看到的和自己看到的都是各自9屏内的数据,包括其内的各种实体,如npc 、怪物、场景道具等(特殊的话还有些特效和动态的阻挡如机关)。
1、屏大小
每屏的大小一般设置为4分之1的整个屏幕的大小,就是一半的整屏幕的长和宽。
所以对于页游来说的话,对于一般屏幕是1440*768的显示屏,使用的每个小格子为32*32 像素,一个屏的大小可以设置成屏幕大小的1/4,即23(1440/32/2=22.5)*14(900/32/2=14)格子。
地图的大小一般可以设置成1024 * 1024 个格子的大小(要修改成屏格子大小的整数,宽是24整数,高是13整数)。
地图大小限制:
const nPos MAP_MAX_POS(1024,1024);
//x轴最大的格子坐标
const uint16 MAP_SCREEN_X = (MAP_MAX_POS.x + SCREEN_GRID_WIDTH -1)/SCREEN_GRID_WIDTH;
//y轴最大的格子坐标
const uint16 MAP_SCREEN_Y = (MAP_MAX_POS.y + SCREEN_GRID_HEIGHT -1)/SCREEN_GRID_HEIGHT;
//格子数最大的总数
const uint32 MAP_SCREEN_MAX = MAP_SCREEN_X * MAP_SCREEN_Y + 1;
2、场景服务器屏索引
功能:
使用场景屏索引的有场景玩家、场景npc、场景道具。
数据结构:
所有类型物件的索引(SceneEntrySet all[SceneObject_MAX],所有类型物件的数组,SceneObject_MAX 是所有类型的枚举,)
所有类型物件的按屏索引的物件集合的关联容器(screenMapIndex _posi_index[SceneObject_MAX]; typedef std::map<uint32, SceneEntrySet> screenMapIndex; )
(方便唯一性检查)
所有类型物件的按屏索引的物件序列的关联容器 (screenMapVec _posi_index_vec[SceneObject_MAX];//typedef std::map<uint32, SceneObject_Vec> screenMapVec;) (方便遍历)
具体实现:
(1)屏索引的说明
屏索引可以查找到该索引对应的实体,视野同步时把场景服务器屏索引的数据同步到网关,再发到客户端。
场景索引类nIndexScene ,可以让场景类继承场景索引类。
索引的场景物件类型,SceneObjectType是所有的实体类型的枚举。
物件类型:
enum SceneObjectType
{
SceneObject_Invalid,/**< 无效类型 */
SceneObject_Player,/**< 玩家角色 */
SceneObject_NPC,/**< NPC */
SceneObject_Item,/**< 场景道具 */
SceneObject_MAX
};
(2)屏索引的容器类型
(2-1)根据屏(坐标哈希值)的屏索引容器(stl的map)的数组
容器1
screenMapVec _posi_index_vec[SceneObject_MAX];//typedef std::map<uint32, SceneObject_Vec> screenMapVec; (其中screen就是场景的屏坐标,类型是uint16(unsigned short))
容器2
screenMapIndex _posi_index[SceneObject_MAX];//typedef std::map<uint32, SceneEntrySet> screenMapIndex; 这个容器的作用只是为了保证_posi_index_vec数据的唯一性
其中:
SceneEntrySet 是实体集合(使用stl的set实现),因为每个实体都是指针,为了避免重复。
SceneObject_Vec 是实体列表(使用stl的vector实现),为的是遍历的效率。使用时会使用SceneObject_Vec
屏相关的实体集合就组成了某种实体的屏索引容器(screenMapIndex _posi_index[SceneObject_MAX])。
为了遍历的效率,使用了SceneObject_Vec 这个stl的vector实现的容器来实现的容器,实际上是SceneEntrySet里面的数据的数据冗余。
容器3
还有实体集合all(SceneEntrySet all[SceneObject_MAX])来冗余一张地图上的所有的类型的实体,主要用来全地图实体处理(例如全地图的实体处理,如所有玩家零点重置,物品超时处理等,这些可以放在场景循环里面实现)。
同类场景物件的所有的场景物件对象集合的数组,所有物件的索引,根据物件类型来存储到set 的列表。
SceneEntrySet all[SceneObject_MAX];
3、场景服务器的添加屏索引
每添加一个新的场景物品到场景就要插入物品到屏索引。
参数SceneEntryT *entry, const nPos &newPos 分别是场景物件和物件的位置。
一个场景位置转换为场景的屏(哈希):
inline screen nPos::getscreen() const
{
return ((*this) != nInvalidPos) ? (MAP_SCREEN_X*(this->y/SCREEN_GRID_HEIGHT) + (this->x/SCREEN_GRID_WIDTH)) : nInvalidscreen;
}
添加物件到场景的屏索引的相关的容器
template< class MapT, class GatewayConnT, class SceneEntryT>
bool nIndexScene< MapT, GatewayConnT, SceneEntryT>::add_sceneEntry(SceneEntryT *entry, const nPos &newPos)
{
if(entry->inserted == true)
{
assert_debug(false && "已经添加");
g_log->error("向屏索引插入 %s 失败 (%u,%u)", entry->name.c_str(), newPos.x, newPos.y);
return false;
}
nPos old = entry->getPos();
//新加入地图
if(!entry->setPos(newPos))
{
assert_debug(false && "设置位置失败");
g_log->error("向屏索引插入 %s 失败 (%u,%u)", entry->name.c_str(), newPos.x, newPos.y);
return false;
}
SceneObjectType type = entry->getType();
screen newscreen = newPos.getscreen();
//位置索引(map)是根据位置(位置的哈希,x,y组成的)索引的场景物件的列表
_pos_index[newPos.hash()].push_back(entry);
entry->setEntryBlock();
if(_posi_index[type][newscreen].insert(entry).second)
{
_posi_index_vec[type][newscreen].push_back(entry);
}
//在全局索引中添加
all[type].insert(entry);//物件类型的屏索引
entry->inserted=true;
entry->onAddToSceneIndex();//获取9屏的信息,只有玩家类型的才会获取,其他的是空操作
return true;
}
同步玩家的屏信息到网关,同步9屏信息到客户端,刷新有效屏
void scene_player::onAddToSceneIndex()
{
MSG::Scene::stFreshScreenIndexMapSceneCmd send;
send.dwMapTempID = this->scene->id;
send.dwScreen = getscreen();
send.dwplayerTempID = this->id;
send.dwSceneType = (uint32)this->scene->sceneType;
sendmsgToGateway(&send, sizeof(send));//发送新的屏信息到网关,需要修改网关的屏信息
scene->freshEffectscreen(nInvalidscreen, getscreen());//刷新有效屏
sendMeToNine();//把自己的信息发送给九屏的玩家
sendNineToMe();//发送9屏的信息给玩家自己
}
4、场景服务器的有效屏
有效屏是在玩家刷新玩家屏索引引发的视野范围内(9屏)的屏转为有效屏。单个场景的有效屏分为10批(自定义的)。有效屏主要设计来为ai优化使用的,只有有效屏上的npc才计算ai,每次使用其中一个批次的有效屏。
(1)刷新有效屏
template< class MapT, class GatewayConnT, class SceneEntryT>
inline void nIndexScene<MapT, GatewayConnT, SceneEntryT>::freshEffectscreen(const screen &oldscreen, const screen &newscreen)
{
if(oldscreen != nInvalidscreen)//原来已经插入到屏索引的屏需要被删除掉
{
const screen_vector &pv = this->get_view(oldscreen);
for(screen_vector::const_iterator it = pv.begin(); it != pv.end(); ++it)
{
PosiEffectMap_iter iter = posiEffect[(*it)%MAX_NPC_GROUP].find(*it);
if(iter != posiEffect[(*it)%MAX_NPC_GROUP].end() && iter->second > 0)
{
assert_debug(iter->second > 0);
iter->second--;
if(iter->second == 0)
{
++_npcloop;
posiEffect[(*it)%MAX_NPC_GROUP].erase(iter);
}
}
}
}
if(newscreen != nInvalidscreen)//加入新的合法的
{
const screen_vector &pv = this->get_view(newscreen);//获取新的屏(为中心)的9屏的视野上的所有屏
for(screen_vector::const_iterator it = pv.begin(); it != pv.end(); ++it)
{
//posiEffect 有效屏,分10批(是ai的切片处理),每批的索引是屏,索引的值是引起该屏的玩家的个数(没有玩家在该屏的视野就不需要执行ai)
posiEffect[(*it)%MAX_NPC_GROUP][*it]++;//MAX_NPC_GROUP 10 ,之所以要把屏索引分成10批是因为ai计算时,每次场景(或者副本循环)只需要计算其中一批的npc的ai,这样可以节省百分之90的效率
++_npcloop;//在执行场景循环时,如果刷新了屏索引的,就要打断该循环的ai的执行,不然计算就不正确
}
}
}
(2)遍历场景服务器的有效屏
场景循环需要执行的:
遍历场景的(或副本)的npc的有效屏索引来执行npc的ai
template<class MapT, class GatewayConnT, class SceneEntryT>
void nIndexScene<MapT, GatewayConnT, SceneEntryT>::execAllOfEffectNpcScreen(const uint32 group, callback<SceneEntryT> &callback)
{
_npcloop = 0;
uint32 which = group % MAX_NPC_GROUP;
PosiEffectMap_iter iter = posiEffect[which].begin();
for( ;iter != posiEffect[which].end() ; ++iter)
{
const SceneObject_Vec &pimi_vec = _posi_index_vec[SceneObject_NPC][iter->first];//iter->first 是有效屏的屏坐标,可以获取上面的所有npc类型的场景物件,来执行npc的ai
int size = pimi_vec.size();
for(int i = 0; i < size; ++ i)
{
if(!callback.invoke(pimi_vec[i]) || _npcloop)//执行有误或者有效屏被刷新就要打断这次的场景循环的npc循环(场景服务器的db连接线程会登录玩家到场景服务器,所以会异步刷新有效屏,因此需要打断被更新了的有效屏循环)
{
return;
}
}
}
}
5、场景服务器和网关服务器的屏索引同步
使用屏索引有个需要注意的是上面介绍的都只是在场景的屏数据的管理,在网关也要维护一份冗余的屏索引数据, 如果要广播同步9屏数据,
只需要在需要发送的数据上外加场景id和屏索引(screen)就可以实现对该九屏的同步,网关会对该范围内(该屏为中心的9屏内)的玩家实体发送消息。
(1)网关的屏索引
功能:
网关的屏索引容器只用来处理网关玩家的屏,不处理npc和场景道具的(他们的广播也是使用网关玩家的屏)。
数据结构:
网关有网关地图屏索引管理器(SceneMapIndex mapIndex,按地图id索引地图屏索引管理器gateway_screen_index*)。
地图屏索引管理器有屏索引容器(Screen2PlayerSet index,屏索引玩家集合,typedef __gnu_cxx::hash_map<uint32, PlayerSet> Screen2PlayerSet;)。
地图屏索引管理器有场景角色容器(PlayerSet all,指定场景的所有网关玩家的集合,typedef std::set<gateway_player*, std::less<gateway_player *>, __gnu_cxx::__pool_alloc<gateway_player *> > PlayerSet;)
具体实现:
(1-1)网关的屏索引类型
第一步:场景id索引网关屏索引管理类
地图索引容器
typedef std::map<uint32, gateway_screen_index*> SceneMapIndex;
地图索引容器迭代
typedef SceneMapIndex::iterator SceneMapIndex_iter;
地图索引
SceneMapIndex mapIndex;
第二步:屏索引该屏上的玩家集合(网关屏索引管理类里)
屏角色容器
typedef std::set<gateway_player*, std::less<gateway_player *>, __gnu_cxx::__pool_alloc<gateway_player *> > PlayerSet;
屏编号索引
typedef __gnu_cxx::hash_map<uint32, PlayerSet> Screen2PlayerSet;
map索引容器
Screen2PlayerSet index;
btw:
屏索引管理类含该场景上的所有网关玩家的玩家集合
PlayerSet all
(1-2)网关更新屏索引
接收到场景发来的屏索引更新消息
stFreshScreenIndexMapSceneCmd* revMsg = (stFreshScreenIndexMapSceneCmd*)pMsg;
gateway_player* pPlayer = (gateway_player*)g_user_mgr.get_player_by_id(revMsg->dwplayerTempID);
if(pPlayer)
{
pPlayer->lock(); <span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px;">//每个玩家的网关屏索引更新需要加锁屏索引,每个网关角色有个互斥锁</span>
pPlayer->scenetype = (MSG::CopySceneType)revMsg->dwSceneType;
if(pPlayer->scene == this)
{
if ((uint32)-1 == revMsg->dwScreen)
{
removeIndex(pPlayer);//如果是非法屏就移除网关玩家的网关屏索引
pPlayer->mapid = 0;
pPlayer->scene = NULL;
}
else
{
freshIndex(pPlayer,revMsg->dwScreen);//typedef std::map<uint32, gateway_screen_index*> SceneMapIndex;
}
}
pPlayer->unlock();
}
刷新网关屏索引
bool scene_client::freshIndex(gateway_player *player,const uint32 screen)
{
if(!player->mapid) { return false; }
_wrlock.rdlock();//每个场景连接类有读写锁
SceneMapIndex_iter iter = mapIndex.find(player->mapid);
if(iter != mapIndex.end())
{
iter->second->refresh(player,screen);
}
else//没有该玩家所在的地图的(场景的实例id)
{
g_log->debug("角色【%s,%u】刷新屏索引时未找到副本地图[%u,%s],(%u)", player->name.c_str(), player->id, \
this->id,this->name.c_str(),player->mapid);
mapIndex[player->mapid] = new gateway_screen_index();// 建立新的地图索引,player->mapid是场景的实例id
mapIndex[player->mapid]->refresh(player,screen);
g_log->error("未找到副本地图(%u)注册一个", player->mapid);
_wrlock.unlock();
return true;
}
_wrlock.unlock();
return true;
}
刷新屏到网关场景管理的里屏索引容器
bool gateway_screen_index::refresh(gateway_player *e, const uint32 newIndex)
{
if(e==NULL) return false;
//-2 表示删除状态,不可以被场景添加
//-1 表示等待添加状态
if(e->getIndexKey() == (uint32)-2 && newIndex != (uint32)-1) return false;
if(e->inserted)
{
//已经加入地图索引,只是在屏之间来回切换
//debug_log("[已经加入地图索引,只是在屏之间来回切换");
bool ret=false;
wrlock.wrlock();
PlayerSet &pimi = index[e->getIndexKey()];
PlayerSet::const_iterator it = pimi.find(e);
if (it != pimi.end())
{
//debug_log("[地图索引]切换: %u, %u", e->getIndexKey(), newIndex);
ret=true;
pimi.erase(it);
index[newIndex].insert(e);
e->setIndexKey(newIndex);
}
wrlock.unlock();
return ret;
}
else if (newIndex != (uint32)-1)//屏是合法的
{
//在全局索引中添加
if (all.insert(e).second)//(1)全局玩家索引
{
//debug_log("[地图索引]加入: %u, %u",e->getIndexKey(), newIndex);
//新加入地图索引
wrlock.wrlock();
index[newIndex].insert(e);//(2)屏玩家索引
wrlock.unlock();
}
else
{
debug_log("[地图索引]加入失败,已经在索引中: %u, %u",e->getIndexKey(), newIndex);
}
e->inserted=true;
}
e->setIndexKey(newIndex);
//e->debug("屏索引x:%d,%d,%d",e->setIndexKey(newIndex),e->getIndexKey(),newIndex);
return e->inserted;
}
(2)网关转发九屏消息
(2-1)网关接收场景角色请求发送九屏消息
stNineExceptMeForwardSceneCmd *revCmd=(stNineExceptMeForwardSceneCmd *)ptrMsg;
SceneMapIndex_iter iter = mapIndex.find(revCmd->sceneid);
if(iter != mapIndex.end())
{
iter->second->sendmsgToNineExceptMe(revCmd->screen, revCmd->exceptid, revCmd->data, revCmd->datasize());
}
发送到除自己以外的所有九屏上的玩家
void gateway_screen_index::sendmsgToNineExceptMe(const uint32 posi, const uint32 exceptme_id, const void *pMsg, const int msgLen)
{
SendNineExecExceptMe exec(exceptme_id, pMsg , msgLen);
const screen_vector &pv = get_view(posi);//获取9屏
screen_vector::const_iterator it = pv.begin(), ed = pv.end();
for(; it != ed; ++it)
{
traverse_every_screen(*it , exec);//遍历该屏的玩家发送消息
}
}
(2-2)网关遍历屏索引上的网关玩家集合
template <class NPC>
void gateway_screen_index::traverse_every_screen(const uint32 screen,callback<NPC> &cb)
{
wrlock.rdlock();
Screen2PlayerSet::iterator iter = index.find(screen);//屏上的玩家集合
if(iter != index.end())
{
PlayerSet &set = iter->second;//屏上的玩家
PlayerSet::iterator it = set.begin(), ed = set.end();
for(; it != ed; ++it)
{
cb.invoke(*it);
}
}
wrlock.unlock();
}
(2-3)网关玩家发送消息
struct SendNineExecExceptMe : public callback<gateway_player>
{
const uint32 _exceptme_id;
const void *_cmd;
uint32 _cmdLen;
uint32 _type;
char _name[MAX_NAME_LEN];
int _sendLen;
t_StackCmdQueue cmd_queue;
SendNineExecExceptMe(const uint32 exceptme_id, const void *cmd , const int cmdLen):_exceptme_id(exceptme_id),_cmd(cmd),_cmdLen(cmdLen)
{
}
bool invoke(gateway_player *player)
{
if (_exceptme_id != player->id)
player->sendmsgToMe(_cmd , _cmdLen);
return true;
}
};