上一篇開始,我們就提到了一個概念,並且進行了初步的運用,這個概念就是實體。
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文檔裏是這麼解釋的
腳本層與實體遠程交互的常規手段(其他參考:allClients、otherClients、clientEntity)。
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!\n", 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}.\n", 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}.\n", 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}.\n", pMethodDescription->getName(), srcEntityID, this->scriptName())); s.done(); return; } } if(g_debugEntity) { DEBUG_MSG(fmt::format("{3}::onRemoteMethodCall: {0}, {3}::{1}(utype={2}).\n", 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.\n" % (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.\n" % (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,如果轉載的話,請保留這段文字。