目录

如何管理多个Space?

一般情况下,一个服务端应用会包含许多个Space,每个Space可以负责同样的业务,也可以负责不同的业务。同时根据业务类型的不同,把目标实体创建或传送到哪个指定的Space上,也是经常碰到的业务需求。那么如何管理多个Space就变得十分重要。

Tips:

阅读本节的开发者,需了解Entity实体创建的基础知识。在本节中会忽略引擎基础的知识,请参考Entity实体相关的文章。

本节就会围绕管理多个Space展开阐述。

需求罗列:

  1. 统一管理Space空间
  2. 进入指定类型的空间

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,同理。

onSpaceGetCellonSpaceLoseCell是自定义的回调,在Space空间的拥有者实体的系统回调中,通过onGetCellonLoseCell时来调用对应回调。


Space空间拥有者的设计

在上面的阐述中,针对Space空间的拥有者的实现还没有涉及,包括enterleave以及onGetCellonLoseCell。假设我们的拥有者称为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部分。最后在引擎系统回调函数onGetCellonLoseCell中分别调用了SpaceMgr的onSpaceGetCellonSpaceLoseCell,来通知全局唯一的空间管理器。


创建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函数完成真正的空间进入。

至此,按需的空间创建就完成了。本案例中忽略了部分的实体创建的基础知识,请读者自行查询文档。


Copyright © 2018 Yolo Technologies. Publication: 2.0-025. Built: 2018-12-07.