KBEngine warring项目源码阅读(三) 实体文件与Account处理

上一篇开始,我们就提到了一个概念,并且进行了初步的运用,这个概念就是实体。

KBE中的实体是一个很重要的概念,可以说,有了实体就有了一切。

我们首先接着上一章的内容,来看Account.def对应的实体定义。

<root>
    <Properties>
        <characters>
            <Type>            AVATAR_INFOS_LIST        </Type>
            <Flags>            BASE                </Flags>
            <Default>                        </Default>
            <Persistent>        true                </Persistent>
        </characters>

        <lastSelCharacter>
            <Type>            DBID                </Type>
            <Flags>            BASE_AND_CLIENT            </Flags>
            <Default>        0                </Default>
            <Persistent>        true                </Persistent>
        </lastSelCharacter>
        
        <activeCharacter>
            <Type>            MAILBOX                </Type>
            <Flags>            BASE                </Flags>
        </activeCharacter>
        
        <lastClientIpAddr>
            <Type>            UINT32                </Type>
            <Flags>            BASE                </Flags>
            <Default>        0                </Default>
        </lastClientIpAddr>
    </Properties>

    <ClientMethods>
        <onReqAvatarList>
            <!-- http://www.kbengine.org/cn/docs/programming/entitydef.html 
                Utype参数是可选的
                属性的自定义协议ID,如果客户端不使用KBE配套的SDK来开发,客户端需要开发跟KBE对接的协议,
                开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
                自身规则所生成的协议ID, 这个ID必须所有def文件中唯一
                这里只是一个演示,demo客户端并没有用到
            -->
            <Utype> 10003 </Utype> 
            <Arg>    AVATAR_INFOS_LIST    </Arg>
        </onReqAvatarList>

        <onCreateAvatarResult>
            <Utype> 10005        </Utype>
            <Arg>    UINT8         </Arg>
            <Arg>    AVATAR_INFOS     </Arg>
        </onCreateAvatarResult>

        <onRemoveAvatar>
            <Arg>    DBID        </Arg>
        </onRemoveAvatar>
    </ClientMethods>

    <BaseMethods>
        <reqAvatarList>
            <Exposed/>
            <Utype> 10001 </Utype>
        </reqAvatarList>

        <reqCreateAvatar>
            <Exposed/>
            <Utype> 10002 </Utype>
            <Arg>    UINT8    </Arg>    <!-- roleType -->
            <Arg>    UNICODE    </Arg>    <!-- name -->
        </reqCreateAvatar>

        <selectAvatarGame>
            <Exposed/>
            <Utype> 10004 </Utype>
            <Arg>    DBID    </Arg>    <!-- dbid -->
        </selectAvatarGame>

        <reqRemoveAvatar>
            <Exposed/>
            <Arg>    UNICODE    </Arg>    <!-- name --> 
        </reqRemoveAvatar>

        <reqRemoveAvatarDBID>
            <Exposed/>
            <Arg>    DBID        </Arg>    <!-- dbid --> 
        </reqRemoveAvatarDBID>
    </BaseMethods>

    <CellMethods>
    </CellMethods>

</root>

在看这个文件的时候,初学者往往一脸懵逼,常见的疑问有以下几种:

1.实体文件是怎么定义的?

2.实体是怎样存在的?

3.实体中用到的类型是怎样的?

4.实体中Flag的定义是什么?

5.实体的节点之间的RPC是如何进行的?

我们来一个个的解答这些问题

实体文件的定义

大量内容拷贝自官方文档:http://kbengine.org/cn/docs/programming/entitydef.html

什么时候需要定义实体:

需要进行数据存储。
能够方便的远程访问。
需要引擎管理和监控, 例如: AOI、Trap、等等。
当灾难发生后服务端可以自动进行灾难的恢复。

什么时候需要定义实体的属性:

需要进行数据存储。
实体被迁移后数据仍然有效(仅cellapp会迁移实体,比如跳转场景)。
当灾难发生后服务端可以自动进行灾难的恢复。

什么时候需要定义实体的方法:

能够方便的远程访问。

一份标准的实体文件格式:

<root>
    // 该实体的父类def
    // 这个标签只在Entity.def中有效,如果本身就是一个接口def则该标签被忽略
    <Parent>    Avatar        </Parent>

    // 易变属性同步控制
    <Volatile>
        // 这样设置则总是同步到客户端
        <position/>
        
        // 没有显式的设置则总是同步到客户端
        <!-- <yaw/> -->

        // 设置为0则不同步到客户端
        <pitch> 0 </pitch>
        
        // 距离10米及以内同步到客户端
        <roll> 10 </roll>
    </Volatile>

    // 注册接口def,类似于C#中的接口
    // 这个标签只在Entity.def中有效,如果本身就是一个接口def则该标签被忽略
    <Implements>
        // 所有的接口def必须写在entity_defs/interfaces中
        <Interface>    GameObject        </Interface>
    </Implements>

    <Properties>
        // 属性名称
        <accountName>
            // 属性类型
            <Type>            UNICODE                </Type>
            
            // (可选)
            // 属性的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
            // 开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
            // 自身规则所生成的协议ID, 这个ID必须所有def文件中唯一
            <Utype>            1000                </Utype>

            // 属性的作用域 (参考下方:属性作用域章节)
            <Flags>            BASE                </Flags>
            
            // (可选)
            // 是否存储到数据库 
            <Persistent>        true                </Persistent>
            
            // (可选)
            // 存储到数据库中的最大长度 
            <DatabaseLength>     100                </DatabaseLength>
            
            // (可选, 不清楚最好不要设置)
            // 默认值 
            <Default>        kbengine            </Default>
            
            // (可选)
            // 数据库索引, 支持UNIQUE与INDEX
            <Index>            UNIQUE                </Index>
        </accountName>
        
        ...
        ...
    </Properties>

    <ClientMethods>
        // 客户端暴露的远程方法名称
        <onReqAvatarList>
            // 远程方法的参数
            <Arg>            AVATAR_INFOS_LIST        </Arg>

            // (可选)
            // 方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
            // 开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
            // 自身规则所生成的协议ID, 这个ID必须所有def文件中唯一
            <Utype>            1001                </Utype>
        </onReqAvatarList>

        ...
        ...
    </ClientMethods>

    <BaseMethods>
        // Baseapp暴露的远程方法名称
        <reqAvatarList>
            // (可选)
            // 定义了此标记则允许客户端调用,否则仅服务端内部暴露
            <Exposed/> 

            // (可选)
            // 方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
            // 开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
            // 自身规则所生成的协议ID, 这个ID必须所有def文件中唯一
            <Utype>            1002                </Utype>
        </reqAvatarList>
        
        ...
        ...
    </BaseMethods>

    <CellMethods>
        // Cellapp暴露的远程方法名称
        <hello>
            // (可选)
            // 定义了此标记则允许客户端调用,否则仅服务端内部暴露
            <Exposed/> 

            // (可选)
            // 方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
            // 开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
            // 自身规则所生成的协议ID, 这个ID必须所有def文件中唯一
            <Utype>            1003                </Utype>
        </hello>
    </CellMethods>

</root>

我们对应来解析account.def文件,有4个属性:

characters,lastSelCharacter,activeCharacter,lastClientIpAddr

根据这四个属性,我们来解答剩下的几个问题

实体是怎样存在的

实体在kbe引擎中是内存中存在的,需要持久化的字段是快照的形式定期同步到数据库的。

开发者并不需要了解怎么把游戏内容写到数据库,引擎会自己完成这一切。

这么做的一个很大的好处是引擎给解决了io瓶颈,说实话自己用redis做缓存+mysql持久化,很容易出错,也容易出现脏数据,最后效率还不一定怎么样。

实体的持久化底层可以参考C++代码中db_interface中的entity_table文件,这里就不复制黏贴了

实体中用到的类型是怎样的

在accout.def的四个字段中,用到了AVATAR_INFOS_LIST,DBID,MAILBOX,UNIT32这几种类型,那么这几种类型分别是什么呢?

脚本的基础类型请参考:http://kbengine.org/cn/docs/programming/alias.html

脚本自带的类型有以下几种:

[Name]			[Size]

UINT8			1
UINT16			2
UINT32			4
UINT64			8

INT8			1
INT16			2
INT32			4
INT64			8

FLOAT			4
DOUBLE			8

VECTOR2			12
VECTOR3			16
VECTOR4			20

STRING			N
UNICODE			N
PYTHON			N
PY_DICT			N
PY_TUPLE		N
PY_LIST			N
MAILBOX			N
BLOB			N

UINT32很容易理解,DBID我们点开entity_defs/alias.xml,也很容易找到对应的定义,其实是一个UNIT64类型的整数。

Mailbox是什么呢,API文档里是这么解释的

脚本层与实体远程交互的常规手段(其他参考:allClientsotherClientsclientEntity)。
Mailbox对象在C++底层实现非常简单,它只包含了实体的ID、目的地的地址、实体类型、Mailbox类型。当用户请求一次远程交互时,底层首先能够通过实体类型找到实体定义的描述,
通过实体定义的描述对用户输入的数据进行检查,如果检查合法那么底层将数据打包并发往目的地,目的地进程根据协议进行解包最终调用到脚本层。

通俗的将, mailbox其实就是一个实体的远程指针, 只有实体在其他进程时才可能会有这样的指针存在。
你想在其他进程访问某个实体, 只有你拥有它的指针你才可以有途径访问他, 而访问的方法必须在def中定义。

现在到AVATAR_INFOS_LIST这个类型,这个类型是用户自定义的类型。

官方文档关于自定义的类型可以参考:http://kbengine.org/cn/docs/programming/customtypes.html

允许用户重定义底层数据结构在内存中存在的形式,这样能够便于用户在内存访问复杂的数据结构,甚至能够提高代码执行的效率。 所有数据类型中只有FIXED_DICT能够被用户重定义,C++底层只能识别这个类型为FIXED_DICT, 在进行识别时会依次检查字典中的key与value, C++底层通常都不会去干涉内存里存储的是什么, 但当进行网络传输和存储操作时,C++会从脚本层获取数据, 用户如果重定义了内存中的存在形式,那么在此时只要能恢复原本的形式则底层依然能够正确的识别。

在entity_defs/alias.xml找到这个类型

    <AVATAR_INFOS_LIST>    FIXED_DICT
        <implementedBy>AVATAR_INFOS.avatar_info_list_inst</implementedBy>
        <Properties>
            <values>
                <Type>    ARRAY <of> AVATAR_INFOS </of>    </Type>
            </values>
        </Properties>
    </AVATAR_INFOS_LIST>    

我们打开user_type/AVATAR_INFOS.py,更详细定义如下

# -*- coding: utf-8 -*-
import KBEngine
import GlobalConst
from KBEDebug import * 

class TAvatarInfos(list):
    """
    """
    def __init__(self):
        """
        """
        list.__init__(self)
        
    def asDict(self):
        data = {
            "dbid"            : self[0],
            "name"            : self[1],
            "roleType"        : self[2],
            "level"            : self[3],
            "data"            : self[4],
        }
        
        return data

    def createFromDict(self, dictData):
        self.extend([dictData["dbid"], dictData["name"], dictData["roleType"], dictData["level"], dictData["data"]])
        return self
        
class AVATAR_INFOS_PICKLER:
    def __init__(self):
        pass

    def createObjFromDict(self, dct):
        return TAvatarInfos().createFromDict(dct)

    def getDictFromObj(self, obj):
        return obj.asDict()

    def isSameType(self, obj):
        return isinstance(obj, TAvatarInfos)

avatar_info_inst = AVATAR_INFOS_PICKLER()

class TAvatarInfosList(dict):
    """
    """
    def __init__(self):
        """
        """
        dict.__init__(self)
        
    def asDict(self):
        datas = []
        dct = {"values" : datas}

        for key, val in self.items():
            datas.append(val)
            
        return dct

    def createFromDict(self, dictData):
        for data in dictData["values"]:
            self[data[0]] = data
        return self
        
class AVATAR_INFOS_LIST_PICKLER:
    def __init__(self):
        pass

    def createObjFromDict(self, dct):
        return TAvatarInfosList().createFromDict(dct)

    def getDictFromObj(self, obj):
        return obj.asDict()

    def isSameType(self, obj):
        return isinstance(obj, TAvatarInfosList)

avatar_info_list_inst = AVATAR_INFOS_LIST_PICKLER()

因为kbe的C++部分只支持自定义FIXED_DICT类型,所以所有自定义类型在进行网络传输和存储操作的时候其实都是FIXED_DICT类型,用户需要自己实现自定义类型的序列化getDictFromObj和反序列化函数createObjFromDict函数。

所以AVATAR_INFOS_LIST类型其实是一个dbid为主键的字典类型,存储着玩家角色列表。

实体中Flag的定义是什么

 flag定义其实是属性的作用域,官方API给了一个列表来说明属性的作用域

[类型]			[ClientEntity]		[BaseEntity]		[CellEntity]
BASE			-			S			-
BASE_AND_CLIENT		C			S			-
CELL_PRIVATE		-			-			S
CELL_PUBLIC		-			-			SC
CELL_PUBLIC_AND_OWN	C			-			SC
ALL_CLIENTS		C(All Clients)		-			SC
OWN_CLIENT		C			-			S
OTHER_CLIENTS		C(Other Clients)	-			SC

S与SC或者C都代表属性包含这个部分,不同的是S代表属性的源头,C代表数据由源头同步,SC代表实体的real部分才是源头,ghosts部分也是被同步过去的

但我个人认为这个表其实不是很容易理解,ppt里的图片反而更容易理解一些

BASE:

BASE_AND_CLIENT:

CELL_PRIVATE:

CELL_PUBLIC:

CELL_PUBLIC_AND_OWN:

ALL_CLIENTS:

OWN_CLIENT:

OTHER_CLIENTS:

实体的节点之间的RPC是如何进行的

在绑定了mailbox之后,前后端的通讯是相当简单的。前段调用后端

BaseCall(func,new object[0]{Arg1,Arg2...})

CellCall(func,new object[0]{Arg1,Arg2...})

就可以了。

后端调用前端也很随意

client.func(Arg1,Arg2...)

底层如何通讯的我们可以拿BaseCall作为示例讲解一下。

找到插件中的mailbox.cs

namespace KBEngine
{
      using UnityEngine; 
    using System; 
    using System.Collections; 
    using System.Collections.Generic;
    
    /*
        实体的Mailbox
        关于Mailbox请参考API手册中对它的描述
        https://github.com/kbengine/kbengine/tree/master/docs/api
    */
    public class Mailbox 
    {
        // Mailbox的类别
        public enum MAILBOX_TYPE
        {
            MAILBOX_TYPE_CELL = 0,        // CELL_MAILBOX
            MAILBOX_TYPE_BASE = 1        // BASE_MAILBOX
        }
        
        public Int32 id = 0;
        public string className = "";
        public MAILBOX_TYPE type = MAILBOX_TYPE.MAILBOX_TYPE_CELL;
        
        private NetworkInterface networkInterface_;
        
        public Bundle bundle = null;
        
        public Mailbox()
        {
            networkInterface_ = KBEngineApp.app.networkInterface();
        }
        
        public virtual void __init__()
        {
        }
        
        bool isBase()
        {
            return type == MAILBOX_TYPE.MAILBOX_TYPE_BASE;
        }
    
        bool isCell()
        {
            return type == MAILBOX_TYPE.MAILBOX_TYPE_CELL;
        }
        
        /*
            创建新的mail
        */
        public Bundle newMail()
        {  
            if(bundle == null)
                bundle = Bundle.createObject();
            
            if(type == Mailbox.MAILBOX_TYPE.MAILBOX_TYPE_CELL)
                bundle.newMessage(Message.messages["Baseapp_onRemoteCallCellMethodFromClient"]);
            else
                bundle.newMessage(Message.messages["Base_onRemoteMethodCall"]);
    
            bundle.writeInt32(this.id);
            
            return bundle;
        }
        
        /*
            向服务端发送这个mail
        */
        public void postMail(Bundle inbundle)
        {
            if(inbundle == null)
                inbundle = bundle;
            
            inbundle.send(networkInterface_);
            
            if(inbundle == bundle)
                bundle = null;
        }
    }
    
} 

 可以看到,所谓的cellcall和basecall只是发了两个不同的消息到后端而已,分别是Baseapp_onRemoteCallCellMethodFromClient和Base_onRemoteMethodCall,我们到后端找Base_onRemoteMethodCall

//-------------------------------------------------------------------------------------
void Base::onRemoteMethodCall(Network::Channel* pChannel, MemoryStream& s)
{
    SCOPED_PROFILE(SCRIPTCALL_PROFILE);

    if(isDestroyed())                                                                                
    {                                                                                                        
        ERROR_MSG(fmt::format("{}::onRemoteMethodCall: {} is destroyed!
",                                            
            scriptName(), id()));

        s.done();
        return;                                                                                            
    }

    ENTITY_METHOD_UID utype = 0;
    s >> utype;
    
    MethodDescription* pMethodDescription = pScriptModule_->findBaseMethodDescription(utype);
    if(pMethodDescription == NULL)
    {
        ERROR_MSG(fmt::format("{2}::onRemoteMethodCall: can't found method. utype={0}, methodName=unknown, callerID:{1}.
", 
            utype, id_, this->scriptName()));
        
        s.done();
        return;
    }

    // 如果是外部通道调用则判断来源性
    if (pChannel->isExternal())
    {
        ENTITY_ID srcEntityID = pChannel->proxyID();
        if (srcEntityID <= 0 || srcEntityID != this->id())
        {
            WARNING_MSG(fmt::format("{2}::onRemoteMethodCall({3}): srcEntityID:{0} != thisEntityID:{1}.
",
                srcEntityID, this->id(), this->scriptName(), pMethodDescription->getName()));

            s.done();
            return;
        }

        if(!pMethodDescription->isExposed())
        {
            ERROR_MSG(fmt::format("{2}::onRemoteMethodCall: {0} not is exposed, call is illegal! srcEntityID:{1}.
",
                pMethodDescription->getName(), srcEntityID, this->scriptName()));

            s.done();
            return;
        }
    }

    if(g_debugEntity)
    {
        DEBUG_MSG(fmt::format("{3}::onRemoteMethodCall: {0}, {3}::{1}(utype={2}).
", 
            id_, (pMethodDescription ? pMethodDescription->getName() : "unknown"), utype, this->scriptName()));
    }

    pMethodDescription->currCallerID(this->id());
    PyObject* pyFunc = PyObject_GetAttrString(this, const_cast<char*>
                        (pMethodDescription->getName()));

    if(pMethodDescription != NULL)
    {
        if(pMethodDescription->getArgSize() == 0)
        {
            pMethodDescription->call(pyFunc, NULL);
        }
        else
        {
            PyObject* pyargs = pMethodDescription->createFromStream(&s);
            if(pyargs)
            {
                pMethodDescription->call(pyFunc, pyargs);
                Py_XDECREF(pyargs);
            }
            else
            {
                SCRIPT_ERROR_CHECK();
            }
        }
    }
    
    Py_XDECREF(pyFunc);
}

到了这个类会调用具体的脚本对应的方法,来进行处理

到此为止,实体这个概念的全部内容讲解完成,我们接着上一章的内容讲解account相关方法

请求角色列表:

客户端

public override void __init__()
        {
            Event.fireOut("onLoginSuccessfully", new object[]{KBEngineApp.app.entity_uuid, id, this});
            baseCall("reqAvatarList", new object[0]);
        }

服务器

    def reqAvatarList(self):
        """
        exposed.
        客户端请求查询角色列表
        """
        DEBUG_MSG("Account[%i].reqAvatarList: size=%i." % (self.id, len(self.characters)))
        self.client.onReqAvatarList(self.characters)

服务器的处理很简单,直接把实体内部的characters这个属性返回回去了

可以看到,建立了mailbox通讯后,服务器的脚本逻辑是非常的简单。

我们来看下其他功能

创建角色:

客户端

        public void reqCreateAvatar(Byte roleType, string name)
        {
            Dbg.DEBUG_MSG("Account::reqCreateAvatar: roleType=" + roleType);
            baseCall("reqCreateAvatar", new object[]{roleType, name, "LSM_TEST_19870508"});
        }

服务器端

    def reqCreateAvatar(self, roleType, name):
        """
        exposed.
        客户端请求创建一个角色
        """
        avatarinfo = TAvatarInfos()
        avatarinfo.extend([0, "", 0, 0, TAvatarData().createFromDict({"param1" : 0, "param2" :b''})])
            
        """
        if name in all_avatar_names:
            retcode = 2
            self.client.onCreateAvatarResult(retcode, avatarinfo)
            return
        """
        
        if len(self.characters) >= 3:
            DEBUG_MSG("Account[%i].reqCreateAvatar:%s. character=%s.
" % (self.id, name, self.characters))
            self.client.onCreateAvatarResult(3, avatarinfo)
            return
        
        """ 根据前端类别给出出生点
        Reference: http://www.kbengine.org/docs/programming/clientsdkprogramming.html, client types
        UNKNOWN_CLIENT_COMPONENT_TYPE    = 0,
        CLIENT_TYPE_MOBILE                = 1,    // 手机类
        CLIENT_TYPE_WIN                    = 2,    // pc, 一般都是exe客户端
        CLIENT_TYPE_LINUX                = 3        // Linux Application program
        CLIENT_TYPE_MAC                    = 4        // Mac Application program
        CLIENT_TYPE_BROWSER                = 5,    // web应用, html5,flash
        CLIENT_TYPE_BOTS                = 6,    // bots
        CLIENT_TYPE_MINI                = 7,    // 微型客户端
        """
        spaceUType = GlobalConst.g_demoMaps.get(self.getClientDatas(), 1)
        
        # 如果是机器人登陆,随机扔进一个场景
        if self.getClientType() == 6:
            while True:
                spaceName = random.choice(list(GlobalConst.g_demoMaps.keys()))
                if len(spaceName) > 0:
                    spaceUType = GlobalConst.g_demoMaps[spaceName]
                    break

        spaceData = d_spaces.datas.get(spaceUType)
        
        props = {
            "name"                : name,
            "roleType"            : roleType,
            "level"                : 1,
            "spaceUType"        : spaceUType,
            "direction"            : (0, 0, d_avatar_inittab.datas[roleType]["spawnYaw"]),
            "position"            : spaceData.get("spawnPos", (0,0,0))
            }
            
        avatar = KBEngine.createBaseLocally('Avatar', props)
        if avatar:
            avatar.writeToDB(self._onAvatarSaved)
        
        DEBUG_MSG("Account[%i].reqCreateAvatar:%s. spaceUType=%i, spawnPos=%s.
" % (self.id, name, avatar.cellData["spaceUType"], spaceData.get("spawnPos", (0,0,0))))

    def _onAvatarSaved(self, success, avatar):
        """
        新建角色写入数据库回调
        """
        INFO_MSG('Account::_onAvatarSaved:(%i) create avatar state: %i, %s, %i' % (self.id, success, avatar.cellData["name"], avatar.databaseID))
        
        # 如果此时账号已经销毁, 角色已经无法被记录则我们清除这个角色
        if self.isDestroyed:
            if avatar:
                avatar.destroy(True)
                
            return
            
        avatarinfo = TAvatarInfos()
        avatarinfo.extend([0, "", 0, 0, TAvatarData().createFromDict({"param1" : 0, "param2" :b''})])

        if success:
            info = TAvatarInfos()
            info.extend([avatar.databaseID, avatar.cellData["name"], avatar.roleType, 1, TAvatarData().createFromDict({"param1" : 1, "param2" :b'1'})])
            self.characters[avatar.databaseID] = info
            avatarinfo[0] = avatar.databaseID
            avatarinfo[1] = avatar.cellData["name"]
            avatarinfo[2] = avatar.roleType
            avatarinfo[3] = 1
            self.writeToDB()

            avatar.destroy()
        
        if self.client:
            self.client.onCreateAvatarResult(0, avatarinfo)

 代码很简单,先创建角色,创建角色成功后再更新账号的角色列表。

这里之所以要销毁avatar,是因为avatar创建以后不一定立即使用。

进入游戏:

客户端:

        public void selectAvatarGame(UInt64 dbid)
        {
            Dbg.DEBUG_MSG("Account::selectAvatarGame: dbid=" + dbid);
            baseCall("selectAvatarGame", new object[]{dbid});
        }

服务器端:

    def selectAvatarGame(self, dbid):
        """
        exposed.
        客户端选择某个角色进行游戏
        """
        DEBUG_MSG("Account[%i].selectAvatarGame:%i. self.activeAvatar=%s" % (self.id, dbid, self.activeAvatar))
        # 注意:使用giveClientTo的entity必须是当前baseapp上的entity
        if self.activeAvatar is None:
            if dbid in self.characters:
                self.lastSelCharacter = dbid
                # 由于需要从数据库加载角色,因此是一个异步过程,加载成功或者失败会调用__onAvatarCreated接口
                # 当角色创建好之后,account会调用giveClientTo将客户端控制权(可理解为网络连接与某个实体的绑定)切换到Avatar身上,
                # 之后客户端各种输入输出都通过服务器上这个Avatar来代理,任何proxy实体获得控制权都会调用onEntitiesEnabled
                # Avatar继承了Teleport,Teleport.onEntitiesEnabled会将玩家创建在具体的场景中
                KBEngine.createBaseFromDBID("Avatar", dbid, self.__onAvatarCreated)
            else:
                ERROR_MSG("Account[%i]::selectAvatarGame: not found dbid(%i)" % (self.id, dbid))
        else:
            self.giveClientTo(self.activeAvatar)

    def __onAvatarCreated(self, baseRef, dbid, wasActive):
        """
        选择角色进入游戏时被调用
        """
        if wasActive:
            ERROR_MSG("Account::__onAvatarCreated:(%i): this character is in world now!" % (self.id))
            return
        if baseRef is None:
            ERROR_MSG("Account::__onAvatarCreated:(%i): the character you wanted to created is not exist!" % (self.id))
            return
            
        avatar = KBEngine.entities.get(baseRef.id)
        if avatar is None:
            ERROR_MSG("Account::__onAvatarCreated:(%i): when character was created, it died as well!" % (self.id))
            return
        
        if self.isDestroyed:
            ERROR_MSG("Account::__onAvatarCreated:(%i): i dead, will the destroy of Avatar!" % (self.id))
            avatar.destroy()
            return
            
        info = self.characters[dbid]
        avatar.cellData["modelID"] = d_avatar_inittab.datas[info[2]]["modelID"]
        avatar.cellData["modelScale"] = d_avatar_inittab.datas[info[2]]["modelScale"]
        avatar.cellData["moveSpeed"] = d_avatar_inittab.datas[info[2]]["moveSpeed"]
        avatar.accountEntity = self
        self.activeAvatar = avatar
        self.giveClientTo(avatar)

这里需要注意的是,baseRef.id指的是实体在内存中的id,base.dbid指的是数据库的id。

至此,其他方法都比较简单,就暂时不一一讲解。

思考两个问题:

1.怎么创建角色的时候进行重名判断

2.在控制台中详细调试了解对应的流程

我是青岛远硕信息科技发展有限公司的Peter,如果转载的话,请保留这段文字。

原文地址:https://www.cnblogs.com/lsm19870508/p/6924396.html