如何管理多个Space?
如何管理多个Space?
一般情况下,一个服务端应用会包含许多个Space,每个Space可以负责同样的业务,也可以负责不同的业务。同时根据业务类型的不同,把目标实体创建或传送到哪个指定的Space上,也是经常碰到的业务需求。那么如何管理多个Space就变得十分重要。
提示
阅读本节的开发者,需了解Entity实体创建的基础知识。在本节中会忽略引擎基础的知识,请参考Entity实体相关的文章。
本节就会围绕管理多个Space展开阐述。
需求罗列
- 统一管理Space空间
- 进入指定类型的空间
SpaceMgr统一管理所有空间
根据Space空间的介绍和例子,我们知道,为了方便管理一个Space只需管理对应的实体即可,如我们例子中的MySpace实体。那我们有一个Entity要进入某个指定的Space空间,必须要知道对应Space的cellEntityCall,那么就需要对所有Space进行管理,我们可称之为SpaceManager,通过其管理当前有哪些Space以及这些Space是什么类型。下面给一个例子抛砖引玉一下:
class SpaceMgr(KBEngine.Entity):
"""
这是一个脚本层封装的空间管理器,在第一个Baseapp上进行创建。
"""
def __init__(self):
KBEngine.Entity.__init__(self)
# _spaceUTypeDic是一个字典,用于用户请求进入某个类型场景时,根据类型来返回进入哪个scene
# key是spaceUType,value是space拥有者的EntityCall
self._spaceUTypeDic = {}
# _spaces是一个字典,用于根据spaceKey快速寻找到space拥有者实体。由于一个场景类型可能对应多个场景,所以需要一个key保证空间对象的唯一性
# key是spaceKey,value是space拥有者的EntityCall
self._spaces = {}
# 向全局共享数据中注册这个管理器的EntityCall以便在所有逻辑进程中可以方便的访问
KBEngine.globalData["SpaceMgr"] = self
def enterSpace(self, avatarEntityCall, spaceUType):
"""
def中申明的方法
某个玩家请求进入到某空间
:param avatarEntityCall: 玩家的entityCall
:param spaceUType: 空间类型
:return:
"""
# 根据空间类型,得到对应Space的EntityCall。例子中不存在副本概念,所以一种类型对应一个空间
space = self._spaceUTypeDic[spaceUType]
if space is None:
ERROR_MSG("SpaceMgr::enterSpace: space with utype(%i) is none" % (spaceUType))
return
space.enter(avatarEntityCall)
def leaveSpace(self, avatarId, spaceKey):
"""
def中申明的方法
某个玩家请求离开某空间
:param avatarId: 用户实体id
:param spaceKey: 空间的唯一id
:return:
"""
space = self._spaces[spaceKey]
if space is None:
ERROR_MSG("SpaceMgr::leaveSpace: spaceKey(%i) to leave is none!" % (spaceKey))
return
space.leave(avatarId)
def onSpaceGetCell(self, key, spaceUType, spaceEntityCall):
"""
当Space空间拥有者的实体在cell上创建成功后调用
:param key: 空间唯一id
:param spaceUType: 空间类型
:param spaceEntityCall: 空间的entityCall
:return:
"""
DEBUG_MSG("SpaceMgr::onSpaceGetCell: spaceUType=%i" % spaceUType)
self._spaceUTypeDic[spaceUType] = spaceEntityCall
self._spaces[key] = spaceEntityCall
def onSpaceLoseCell(self, key, spaceUType):
"""
在Space空间拥有者的实体在cell上的部分Lose后调用
:param key:
:param spaceUType:
:return:
"""
DEBUG_MSG("SpaceMgr::onSpaceLoseCell: spaceUType=%i" % spaceUType)
del self._spaceUTypeDic[spaceUType]
del self._spaces[key]
在初始化中,我们申明了_spaceUTypeDic和_spaces,前者是针对场景类型的字典,后者是针对场景id的字典。接着使用globalData把SpaceMgr作为全局共享数据,以便在所有逻辑进程中可以方便的访问。
当有实体要进入某个类型的空间时,调用enterSpace。通过字典_spaceUTypeDic找到该空间类型对应的Space空间的拥有者对象(该例子中假设不存在副本概念,所以一个空间类型对应一个Space实体拥有者),并调用它的enter方法(在下面会对enter方法的实现进行讲解),通知它有个玩家进来了。方法:leaveSpace,同理。
而onSpaceGetCell和onSpaceLoseCell是自定义的回调,在Space空间的拥有者实体的系统回调中,通过onGetCell和onLoseCell时来调用对应回调。
Space空间拥有者的设计
在上面的阐述中,针对Space空间的拥有者的实现还没有涉及,包括enter、leave以及onGetCell和onLoseCell。假设我们的拥有者称为SpaceOwner,其Base部分的代码如下:
class SpaceOwner(KBEngine.Space):
"""
base上,一个空间拥有者实体
"""
def __init__(self):
KBEngine.Space.__init__(self)
# 玩家的字典,key是实体id,value是玩家的entitycall
self._avatarDic = {}
def enter(self, avatarEntityCall):
"""
有人进入本空间
:param avatarEntityCall: 玩家的entitycall
:return:
"""
# 把目标实体放入SpaceOwner的cell所在空间
avatarEntityCall.createCellEntity(self.cell)
# 加入列表
self._avatarDic[avatarEntityCall.id] = avatarEntityCall
def leave(self, avatarId):
"""
有人离开本空间
:param avatarId:玩家的实体id
:return:
"""
avatarEntityCall = self._avatarDic[avatarId]
# 移除列表
del self._avatarDic[avatarId]
# 销毁目标实体关联的cell实体
if avatarEntityCall is not None:
if avatarEntityCall.cell is not None:
avatarEntityCall.destroyCellEntity()
# ======引擎系统回调======
def onGetCell(self):
"""
entity的cell部分被创建成功
"""
DEBUG_MSG("SpaceOwner::onGetCell: id=%i, uType=%i" % (self.id, self.uType))
# 通知SpaceMgr,本房间已经创建完毕
KBEngine.globalData["SpaceMgr"].onSpaceGetCell(self.id, self.uType, self)
def onLoseCell(self):
"""
entity的cell部分实体丢失
"""
DEBUG_MSG("SpaceOwner::onLoseCell: id=%i, uType=%i" % (self.id, self.uType))
# 通知SpaceMgr,本房间已经Lose
KBEngine.globalData["SpaceMgr"].onSpaceLoseCell(self.id, self.uType)
从例子代码中可以看到,使用一个SpaceOwner来管理对应的Space空间,并且维护了房间内的玩家字典self._avatarDic。当玩家进入时,enter被调用,把玩家的cell部分创建到本空间中去,并加入到字典中。当玩家离开时,leave被调用,移除字典,销毁玩家的cell部分。最后在引擎系统回调函数onGetCell和onLoseCell中分别调用了SpaceMgr的onSpaceGetCell和onSpaceLoseCell,来通知全局唯一的空间管理器。
创建SpaceMgr
因为在刚才的设计中,SpaceMgr是全服务器唯一的,所以其必须只创建一次。一般我们会在BaseApp就绪时,并且通过isBootstrap的参数确保是第一个启动的Baseapp,此时是最佳的创建时机。
修改BaseApp下的kbemain.py入口函数,见如下代码:
def onBaseAppReady(isBootstrap):
"""
KBEngine method.
baseapp已经准备好了
@param isBootstrap: 是否为第一个启动的baseapp
@type isBootstrap: BOOL
"""
INFO_MSG('onBaseAppReady: isBootstrap=%s, appID=%s, bootstrapGroupIndex=%s, bootstrapGlobalIndex=%s' % \
(isBootstrap, os.getenv("KBE_COMPONENTID"), os.getenv("KBE_BOOTIDX_GROUP"),
os.getenv("KBE_BOOTIDX_GLOBAL")))
if isBootstrap:
# 第一个baseapp就绪时,创建Space管理器实体
KBEngine.createEntityLocally("SpaceMgr", {})
这样在服务器或服务器组启动时,只会创建出一个SpaceMgr并保存在GlobalData中,保证了全局唯一性。当某个玩家想要进入类型=1的空间时,只需如下调用函数即可:
KBEngine.globalData["SpaceMgr"].enterSpace(avatarEntityCall, 1)
其中,avatarEntityCall是该玩家的EntityCall对象。非常方便。
创建不同类型的空间
有了SpaceMgr,但是在该管理器中没有注册任何类型的空间(本例中叫做SpaceOwner)。这里给出两种常见的方案:
1、在服务器启动时就知道一共有哪些类型,则一次性都创建完毕。适合于类似游戏大厅、主城这种固定的空间。
2、按需创建房间,即在用户或玩家请求进入某一个未创建的空间时,进行创建。当然这种方案会存在这样一种情况,就是用户或玩家需要等待该空间创建完毕后才可以进入。
1、固定空间的创建
由于本例中SpaceOwner是继承于KBEngine.Space的,所以在创建其base部分后,就会自动创建对应的空间,在空间创建完毕后,SpaceOwner的cell部分会就创建完毕了,此刻会回调onGetCell,最后通知到SpaceMgr。一气呵成。所以,我们只需在base上create房间实体对象即可。比如在BaseApp就绪时(base的入口脚本kbemain.py中)添加SpaceOwner的创建代码如下:
if isBootstrap:
# 第一个baseapp就绪时,创建Space管理器实体
KBEngine.createEntityLocally("SpaceMgr", {})
space_param = {
'uType': 1
}
KBEngine.createEntityLocally("SpaceOwner", space_param)
直接在启动时,创建类型=1的房间,接下来会自动进行处理,完成固定空间的创建。
2、按需创建空间
许多时候,空间是按需动态创建的,我们来举个例子:
我们先把刚才base的入口脚本kbemain.py的修改还原成如下:
if isBootstrap:
# 第一个baseapp就绪时,创建Space管理器实体
KBEngine.createEntityLocally("SpaceMgr", {})
修改SpaceMgr中的enterSpace函数:
def enterSpace(self, avatarEntityCall, spaceUType):
"""
def中申明的方法
某个玩家请求进入到某空间
:param avatarEntityCall: 玩家的entityCall
:param spaceUType: 空间类型
:return:
"""
# 根据空间类型,得到对应Space的EntityCall。例子中不存在副本概念,所以一种类型对应一个空间
space = self._spaceUTypeDic[spaceUType]
if space is None:
INFO_MSG("SpaceMgr::enterSpace: space with utype(%i) is none. Try to create one" % (spaceUType))
param = {
'uType': spaceUType
}
new_space = KBEngine.createEntityLocally('SpaceOwner', param)
# 加入等待要进入该房间的EntityCall列表
new_space.addWaitToEnter(avatarEntityCall)
return
space.enter(avatarEntityCall)
可以看到,当进入的空间类型对应的空间不存在时,创建一个本地实体SpaceOwner,并给予目标的空间类型,最后调用SpaceOwner.addWaitToEnter(avatarEntityCall)通知SpaceOwner有一个实体是等待进入本空间的。
我们再来看看SpaceOwner中addWaitToEnter是如何实现的,以及SpaceOwner对应的代码调整:
def addWaitToEnter(self, avatarEntityCall):
"""
当空间还没创建完毕时,把目标实体加入到等待进入房间的列表中
:param avatarEntityCall: 要进入房间的目标实体
:return:
"""
self._waitToEnterList.append(avatarEntityCall)
# ======引擎系统回调======
def onGetCell(self):
"""
entity的cell部分被创建成功
"""
DEBUG_MSG("SpaceOwner::onGetCell: id=%i, uType=%i" % (self.id, self.uType))
# 通知SpaceMgr,本房间已经创建完毕
KBEngine.globalData["SpaceMgr"].onSpaceGetCell(self.id, self.uType, self)
# 处理排队要进入本空间的列表
for entityCall in self._waitToEnterList:
self.enter(entityCall)
# 清空
self._waitToEnterList = []
可以看到函数addWaitToEnter维护了一个等待进入的实体列表。并且在onGetCell时,即真正等空间的cell也创建完毕后,把刚才等待进入的列表遍历一遍,并调用了SpaceOwner.enter函数完成真正的空间进入。
至此,按需的空间创建就完成了。本案例中忽略了部分的实体创建的基础知识,请读者自行查询文档。