KBEngine簡單RPG-Demo源碼解析

一:環境搭建

1. 確保已經下載過KBEngine服務端引擎,若是沒有下載請先下載    
      下載服務端源碼(KBEngine):        
      https://github.com/kbengine/kbengine/releases/latest   

     編譯(KBEngine):      
      http://www.kbengine.org/docs/build.html   

      安裝(KBEngine):        
      http://www.kbengine.org/docs/installation.html

2. 下載unity3d demo源碼(kbengine_unity3d_demo)
     https://github.com/kbengine/kbengine_unity3d_demo/releases/latest

3. 下載kbengine客戶端插件與服務端Demo資產庫:    
      * 使用git命令行,進入到kbengine_unity3d_demo目錄執行:        
                 git submodule update --init --remote                        
                 

      * 或者使用 TortoiseGit(選擇菜單): TortoiseGit -> Submodule Update:
                

      * 也能夠手動下載kbengine客戶端插件與服務端Demo資產庫            
                客戶端插件下載:                
                       https://github.com/kbengine/kben ... /archive/master.zip                
                       下載後請將其解壓縮,插件源碼請放置在: Assets/plugins/kbengine/kbengine_unity3d_plugins            

                服務端資產庫下載:                
                       https://github.com/kbengine/kbengine_demos_assets/releases/latest                
                       下載後請將其解壓縮,並將目錄文件放置於服務端引擎根目錄"kbengine/"之下,以下圖:

4. 拷貝服務端資產庫"kbengine_demos_assets"到服務端引擎根目錄"kbengine/"之下,以下圖:
                


二:配置Demo(可選):
改變登陸IP地址與端口(注意:關於服務端端口部分參看http://www.kbengine.org/cn/docs/installation.html):

                 
                kbengine_unity3d_demo\Scripts\kbe_scripts\clientapp.cs -> ip    
                kbengine_unity3d_demo\Scripts\kbe_scripts\clientapp.cs -> port


三:啓動服務器:
確保「kbengine_unity3d_demo\kbengine_demos_assets」已經拷貝到KBEngine根目錄:    
      參考上方章節:開始使用啓動腳本啓動服務端:   

Windows:        
      kbengine\kbengine_demos_assets\start_server.bat    
Linux:        
      kbengine\kbengine_demos_assets\start_server.sh
      檢查啓動狀態:        
               若是啓動成功將會在日誌中找到"Components::process(): Found all the components!"。     
               任何其餘狀況請在日誌中搜索"ERROR"關鍵字,根據錯誤描述嘗試解決。        
               (更多參考: http://www.kbengine.org/docs/startup_shutdown.html)


四:啓動客戶端:
直接在Unity3D編輯器啓動或者編譯後啓動
(編譯客戶端:Unity Editor -> File -> Build Settings -> PC, MAC & Linux Standalone.)

五:生成導航網格(可選):
服務端使用Recastnavigation在3D世界尋路,recastnavigation生成的導航網格(Navmeshs)放置於:    
      kbengine\demo\res\spaces\*

在Unity3D中使用插件生成導航網格(Navmeshs):    
      https://github.com/kbengine/unity3d_nav_critterai


六:演示截圖:

七:服務端資產庫文件夾結構
http://kbengine.org/cn/docs/concepts/directorys.html
看assets, 注意:demo使用的不是默認的assets資產目錄,而是上面章節下載的kbengine_demos_assets,但文件夾結構與意義是一致的。

八:客戶端文件夾結構
kbengine_unity3d_demo
             -> Assets                                                         // Unity3d資產庫
                      -> Plugins
                              -> kbengine                                   // KBEngine插件層(包含了網絡消息處理、客戶端實體維護、與服務端對接層)
                      -> Scripts
                              -> kbe_scripts                                // 客戶端邏輯腳本層(https://github.com/kbengine/kben ... e_scripts/README.md
                                       -> Account.cs                       // 對應於服務端的帳號實體的客戶端部分實現
                                       -> Avatar.cs                          // 對應於服務端的角色實體的客戶端部分實現
                                       -> clientapp.cs                      // 按照服務端的概念cellapp、baseapp、etc,這裏咱們抽象出一個clientapp
                                       -> Combat.cs                       // 對應於服務端的def interfaces/Combat的客戶端部分實現
                                       -> GameObject.cs                 // 對應於服務端的def interfaces/GameObject的客戶端部分實現
                                       -> Gate.cs                             // 對應於服務端的Gate實體的客戶端部分實現
                                       -> Monster.cs                       // 對應於服務端的Monster實體的客戶端部分實現
                                       -> NPC.cs                             // 對應於服務端的NPC實體的客戶端部分實現
                                       -> Skill.cs                              // 一個簡單的不能再簡單的技能執行類,服務端cell/skill下面也有,而客戶端主要是進行一些檢查
                                       -> SkillBox.cs                        // 玩家的技能列表,對應於服務端的def interfaces/Skillbox的客戶端部分實現
                                       -> SkillObject.cs                    // 技能對象(施法者、目標、受術者等),服務端cell/skill下面也有
                              -> u3d_scripts                               // 客戶端UI等表現層
                                       -> UI.cs                                // 處理UI部分
                                       -> World.cs                          // 處理場景世界部分
                                       -> GameEntity.cs                 // 全部服務端同步過來的實體在表現層都必須繼承該類,完成統一的表現(頭頂名稱、血條等)與控制(實體狀態、移動)

------------------------------------------

基本設計結構:
                                                                  -遊戲-
                                   |                                                                        |
                  表現層u3d_scripts(UI && 世界)                      KBE層kbe_scripts(插件 && 邏輯)

1:  表現層與KBE層能夠配置爲不一樣線程也能配置爲同一個線程跑(單線程)
2:  表現層與KBE層使用事件交互, 向KBE層觸發的事件使用fireIn(...),KBE層向外部觸發的事件使用fireOut(...)。 那麼表現層想要監聽KBE觸發的Out事件,須要註冊監聽Event.registerOut, KBE須要監聽外部觸發進來的事件則反之。
3: 使用unity3D插件與服務端配套則服務端中的scripts/client文件夾能夠忽略(https://github.com/kbengine/kben ... e_scripts/README.md)html

九:遊戲配置
服務端demo全部的配置都存放於kbengine_demos_assets\scripts\data之下。
scripts\data\
               d_avatar_inittab.py    // 角色初始化表, 用於新創建的角色設置初始值, 由kbengine\kbe\tools\xlsx2py\rpgdemo\avatar_init.bat導出。
               d_dialogs.py               // NPC對話表, 其中'menu1'對於的是一個對話協議的ID,服務端根據不一樣的協議ID執行不一樣的對話功能, 由kbengine\kbe\tools\xlsx2py\rpgdemo\dialogs.bat導出。
               d_entities.py               // 實體類型表,描述某類型怪移動速度,攻擊力等,由kbengine\kbe\tools\xlsx2py\rpgdemo\NPC.bat導出。
               d_skills.py                   // 技能表,描述某類型技能斷定條件,輸出等,由kbengine\kbe\tools\xlsx2py\rpgdemo\skils.bat導出。
               d_spaces.py               // 場景副本表,描述space是大地圖仍是副本,以及地圖名稱等,由kbengine\kbe\tools\xlsx2py\rpgdemo\spaces.bat導出。
               d_spaces_spawns.py // NPC、Monster等出生點信息,目前是手填的,也能夠採用工具布點導出。

        spawnpoints\
                 xinshoucun_spawnpoints.xml   // 這個出生點信息主要用於warring這個demo,(NPC、Monster等出生點信息,採用Unity3d布點導出, 能夠在unity打開warring這個demo,
                                                                 // 在unity3d(菜單上)->ublish->Build Publish AssetBundles(打包全部須要動態加載資源),而後在Assets->StreamingAssets目錄下會獲得 "場景名稱_spawnpoints.xml"的出生點表)。


十:建立帳號

客戶端部分:
1: kbengine_unity3d_demo\Assets\Scripts\u3d_scripts\UI.cs
        1.1 點擊登陸按鈕致使createAccount()被調用, createAccount中向KBE層觸發了一個建立帳號事件,參數是帳號名與密碼。
          注意:KBEngine插件kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs中已經註冊了這個「createAccount」事件,對應於KBEngineApp.createAccount函數。git

  1.         public void createAccount()
  2.         {
  3.            KBEngine.Event.fireIn("createAccount", new object[]{stringAccount, stringPasswd});
  4.         }
複製代碼




2. 事件在KBE插件中kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, KBEngineApp.process()中被正式處理github

  1.                 /*
  2.                         插件的主循環處理函數
  3.                 */
  4.                 public virtual void process()
  5.                 {
  6.                         // 處理網絡
  7.                         _networkInterface.process();
  8.                         
  9.                         // 處理外層拋入的事件
  10.                         Event.processInEvents();
  11.                         
  12.                         // 向服務端發送心跳以及同步角色信息到服務端
  13.                         sendTick();
  14.                 }
複製代碼

3. 建立帳號函數被調用, createAccount_loginapp函數表示請求向服務端loginapp進程要求建立一個帳號,而此時可能尚未鏈接服務器,須要先鏈接,若是已經鏈接上了則向loginapp發送一個包「bundle.send」。
            能夠看到向Bundle中寫入了相關須要的數據,而Bundle會將數據序列化成二進制流,服務端會採用相同的協議將其歸原並將調用服務端協議所綁定的方法(後面會講到服務端具體方法)。數據庫

  1.                 public void createAccount(string username, string password)
  2.                 {
  3.                         KBEngineApp.app.username = username;
  4.                         KBEngineApp.app.password = password;
  5.                         KBEngineApp.app.createAccount_loginapp(true);
  6.                 }
  7.                 /*
  8.                         建立帳號,經過loginapp
  9.                 */
  10.                 public void createAccount_loginapp(bool noconnect)
  11.                 {
  12.                         if(noconnect)
  13.                         {
  14.                                 reset();
  15.                                 _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_createAccount_callback, null);
  16.                         }
  17.                         else
  18.                         {
  19.                                 Bundle bundle = new Bundle();
  20.                                 bundle.newMessage(Message.messages["Loginapp_reqCreateAccount"]);
  21.                                 bundle.writeString(username);
  22.                                 bundle.writeString(password);
  23.                                 bundle.writeBlob(new byte[0]);
  24.                                 bundle.send(_networkInterface);
  25.                         }
  26.                 }
複製代碼

建立返回結果:
UI.cs -> onCreateAccountResult

服務端部分:
1. 經過上面能夠得知客戶端向服務端發送了一條建立帳號的協議, 協議名稱爲「Loginapp_reqCreateAccount」(注意,全部的協議名稱都能在服務端找到對應的方法, Loginapp_表明了協議的做用域僅爲Loginapp, 方法名稱爲reqCreateAccount)服務器

  1. void Loginapp::reqCreateAccount(Network::Channel* pChannel, MemoryStream& s)
  2. {
  3.         std::string accountName, password, datas;
  4.         s >> accountName >> password;
  5.         s.readBlob(datas);
  6.         
  7.         if(!_createAccount(pChannel, accountName, password, datas, ACCOUNT_TYPE(g_serverConfig.getLoginApp().account_type)))
  8.                 return;
  9. }
複製代碼

服務端解析出了帳號名與密碼,在_createAccount函數中會將這條請求最終送到dbmgr,dbmgr檢查以後決定是否建立數據庫帳號,並最終將結果返回到loginapp,而後由loginapp將結果中轉至客戶端。

十一:登陸帳號
1: kbengine_unity3d_demo\Assets\Scripts\u3d_scripts\UI.cs, 向KBE層觸發了登錄事件網絡

  1.         public void login()
  2.         {
  3.                 info("connect to server...(鏈接到服務端...)");
  4.                 
  5.                 KBEngine.Event.fireIn("login", new object[]{stringAccount, stringPasswd});
  6.         }
複製代碼

2: kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, 插件觸發登錄函數,並最終向loginapp發送了一個登錄包「Loginapp_login」數據結構

  1.         public void login(string username, string password)
  2.         {
  3.             KBEngineApp.app.username = username;
  4.             KBEngineApp.app.password = password;
  5.             KBEngineApp.app.login_loginapp(true);
  6.         }
  7.        
  8.         /*
  9.             登陸到服務端(loginapp), 登陸成功後還必須登陸到網關(baseapp)登陸流程纔算完畢
  10.         */
  11.         public void login_loginapp(bool noconnect)
  12.         {
  13.             if(noconnect)
  14.             {
  15.                 reset();
  16.                 _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_loginapp_callback, null);
  17.             }
  18.             else
  19.             {
  20.                 Dbg.DEBUG_MSG("KBEngine::login_loginapp(): send login! username=" + username);
  21.                 Bundle bundle = new Bundle();
  22.                 bundle.newMessage(Message.messages["Loginapp_login"]);
  23.                 bundle.writeInt8((sbyte)_args.clientType); // clientType
  24.                 bundle.writeBlob(new byte[0]);
  25.                 bundle.writeString(username);
  26.                 bundle.writeString(password);
  27.                 bundle.send(_networkInterface);
  28.             }
  29.         }
複製代碼


服務端部分:
1:服務端loginapp.cpp中「void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)」被觸發, 這個函數進行了一系列的檢查,
肯定合法後向dbmgr發送一個登錄請求包「(*pBundle).newMessage(DbmgrInterface:nAccountLogin);」, dbmgr也會進行一系列的檢查並將登錄結果返回到loginapp。多線程

  1. void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)
  2. {
  3.         ...
  4. ...
  5.         if(loginName.size() > ACCOUNT_NAME_MAX_LENGTH)
  6.         {
  7.                 INFO_MSG(fmt::format("Loginapp::login: loginName is too long, size={}, limit={}.\n",
  8.                         loginName.size(), ACCOUNT_NAME_MAX_LENGTH));
  9.                 
  10.                 _loginFailed(pChannel, loginName, SERVER_ERR_NAME, datas, true);
  11.                 s.done();
  12.                 return;
  13.         }
  14.         if(password.size() > ACCOUNT_PASSWD_MAX_LENGTH)
  15.         {
  16.                 INFO_MSG(fmt::format("Loginapp::login: password is too long, size={}, limit={}.\n",
  17.                         password.size(), ACCOUNT_PASSWD_MAX_LENGTH));
  18.                 
  19.                 ...
  20. ...
  21. ...
  22.         // 向dbmgr查詢用戶合法性
  23.         Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
  24.         (*pBundle).newMessage(DbmgrInterface::onAccountLogin);
  25.         (*pBundle) << loginName << password;
  26.         (*pBundle).appendBlob(datas);
  27.         dbmgrinfos->pChannel->send(pBundle);
  28. }
複製代碼


1.1: loginapp獲得dbmgr的登陸合法結果後向baseappmgr發送了分配網關(baseapp)請求(registerPendingAccountToBaseapp), 一般是負載較低的一個baseapp進程.app

  1. void Loginapp::onLoginAccountQueryResultFromDbmgr(Network::Channel* pChannel, MemoryStream& s)
  2. {
  3.         ...
  4. ...
  5. ...
  6.         // 若是大於0則說明當前帳號仍然存活於某個baseapp上
  7.         if(componentID > 0)
  8.         {
  9.                 Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
  10.                 (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseappAddr);
  11.                 (*pBundle) << componentID << loginName << accountName << password << entityID << dbid << flags << deadline << infos->ctype;
  12.                 baseappmgrinfos->pChannel->send(pBundle);
  13.                 return;
  14.         }
  15.         else
  16.         {
  17.                 // 註冊到baseapp而且獲取baseapp的地址
  18.                 Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
  19.                 (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseapp);
  20.                 (*pBundle) << loginName;
  21.                 (*pBundle) << accountName;
  22.                 (*pBundle) << password;
  23.                 (*pBundle) << dbid;
  24.                 (*pBundle) << flags;
  25.                 (*pBundle) << deadline;
  26.                 (*pBundle) << infos->ctype;
  27.                 baseappmgrinfos->pChannel->send(pBundle);
  28.         }
  29. }
複製代碼

1.2:baseappmgr最終返回所分配的baseapp的ip地址等信息,loginapp將其轉發給客戶端(登陸成功協議onLoginSuccessfully,包含baseapp的ip和端口信息)dom

  1. void Loginapp::onLoginAccountQueryBaseappAddrFromBaseappmgr(Network::Channel* pChannel, std::string& loginName, 
  2.                                                                                                                         std::string& accountName, std::string& addr, uint16 port)
  3. {
  4.         ...
  5. ...
  6. ...
  7.         Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
  8.         (*pBundle).newMessage(ClientInterface::onLoginSuccessfully);
  9.         uint16 fport = ntohs(port);
  10.         (*pBundle) << accountName;
  11.         (*pBundle) << addr;
  12.         (*pBundle) << fport;
  13.         (*pBundle).appendBlob(infos->datas);
  14.         pClientChannel->send(pBundle);
  15.         SAFE_RELEASE(infos);
  16. }
複製代碼

2: 客戶端插件獲得返回結果後調用KBEngineApp.cs->login_baseapp()函數開始正式登陸到baseapp。

3:baseapp收到登陸請求

  1. void Baseapp::loginGateway(Network::Channel* pChannel, 
  2.                                                    std::string& accountName, 
  3.                                                    std::string& password)
複製代碼

進行了一系列的檢查,包括:帳號是否已經在線,是否能夠在這裏登陸等等。
當檢查合法後,向dbmgr發送了一個查詢帳號信息的請求「DbmgrInterface::queryAccount」,dbmgr將查詢到的帳號數據(包括屬性等)返回到baseapp, Baseapp:nQueryAccountCBFromDbmgr
當函數結果爲合法時,根據配置中定義的帳號實體腳本名稱「g_serverConfig.getDBMgr().dbAccountEntityScriptType」建立了Account實體, 同時還建立了一個clientMailbox,帳號實體中調用clientMailbox->方法()便可與客戶端通信了。
Account實體被建立後, 首先__init__被調用, 接着onEntitiesEnabled被調用, 此時實體正式可用了。

帳號登錄成功後, 客戶端Account.cs中會調用__init__() -> baseCall("reqAvatarList");來請求得到角色列表,
UI.cs中onReqAvatarList獲得結果。

十二:建立角色與選擇角色進入遊戲
1. 建立角色UI.cs -> void onSelAvatarUI()中
       account.reqCreateAvatar(1, stringAvatarName);
       UI.cs中onCreateAvatarResult獲得結果。

2.選擇角色進入遊戲
UI.cs -> onSelAvatarUI()中
account.selectAvatarGame(selAvatarDBID);
這裏使用角色的數據庫ID做爲標識,服務端上Account實體有角色列表屬性,角色列表的數據結構大概爲
AvatarList <Dict<AvatarDBID(UINT64), INFOS>>

十三:建立世界(大地圖與副本)
1. 建立世界管理器服務端啓動以後,baseapp與cellapp準備完畢、準備關閉等事件都會通知到kbengine_defs.xml配置中指定的個性化腳本。kbe默認個性化腳本爲kbengine.py,  baseapp進程準備好以後會調用kbengine.py的onBaseAppReady回調函數, demo在這個函數中斷定是否爲第一個啓動的baseapp(假如啓動了不少baseapps),
若是是第一個baseapp,腳本建立了一個世界管理實體「spaces」:

  1. def onBaseAppReady(isBootstrap):
  2.                               """
  3.                               KBEngine method.
  4.                               baseapp已經準備好了
  5.                               @param isBootstrap: 是否爲第一個啓動的baseapp
  6.                               @type isBootstrap: BOOL
  7.                               """
  8.                               INFO_MSG('onBaseAppReady: isBootstrap=%s' % isBootstrap)
  9.         
  10.                               # 安裝監視器
  11.                               Watcher.setup()
  12.         
  13.                               if isBootstrap:
  14.                                                             # 建立spacemanager
  15.                                                             KBEngine.createBaseLocally( "Spaces", {} )
複製代碼


2. 世界管理器建立出全部的場景
在spaces.py中, spaces經過initAlloc函數根據配置中scripts/data/d_spaces.py建立出space實體,space實體描述的是一個抽象空間,一個空間能夠被邏輯定義爲大地圖、場景、房間、宇宙等等。

  1.         def initAlloc(self):
  2.                                       # 註冊一個定時器,在這個定時器中咱們每一個週期都建立出一些Space,直到建立完全部
  3.                                       self._spaceAllocs = {}
  4.                                       self.addTimer(3, 1, SCDefine.TIMER_TYPE_CREATE_SPACES)
  5.                 
  6.               self._tmpDatas = list(d_spaces.datas.keys())
  7.                                       for utype in self._tmpDatas:
  8.                                                  spaceData = d_spaces.datas.get(utype)
  9.                                               if spaceData["entityType"] == "SpaceDuplicate":
  10.                                                          self._spaceAllocs[utype] = SpaceAllocDuplicate(utype)
  11.                                               else:
  12.                                                          self._spaceAllocs[utype] = SpaceAlloc(utype)
複製代碼


SpaceAlloc: 普通地圖,能夠理解爲大地圖,但整個世界中只能有一個這樣類型的地圖。
SpaceAllocDuplicate:副本地圖,能夠複製出不少個

上面函數註冊了一個定時器, 這裏是定時器的回調, 每一秒回調一次。
self._spaceAllocs[spaceUType].init(), 這裏真正開始建立這些space實體, 裏面調用的createBaseAnywhere函數來建立實體, 若是啓動了多個baseapp這個函數根據負載狀況將實體選擇到合適的進程中建立。

  1.     def createSpaceOnTimer(self, tid, tno):
  2.            """
  3.            建立space
  4.            """
  5.            if len(self._tmpDatas) > 0:
  6.                      spaceUType = self._tmpDatas.pop(0)
  7.                   self._spaceAllocs[spaceUType].init()
  8.            
  9.              if len(self._tmpDatas) <= 0:
  10.                      del self._tmpDatas
  11.                      self.delTimer(tid)
複製代碼


Space實體建立出來以後,此時尚未真正建立出空間, 這個實體僅僅是將要與某個真正空間關聯的實體, 能夠經過它來操控那個空間。
但空間只能在cellapp上存在, 所以咱們須要調用API讓實體在cell上建立出一個空間,並在cell上建立出一個實體與空間關聯, 這個實體就像一個空間的句柄。

  1. class Space(KBEngine.Base, GameObject):
  2.               def __init__(self):
  3.                             self.createInNewSpace(None)
複製代碼

此功能由createInNewSpace完成, __init__能夠理解爲Space的構造函數。


3. 爲這個抽象的空間增長几何數據
有指定幾何數據的空間能夠被看作是一個特定的場景, 這些幾何數據與客戶端對應的場景表現相關聯, 例如:導航網格(navmesh), 服務端經過這些數據讓NPC進行正確的移動,碰撞等。
上面Space建立cell部分以後, cell上的Space._init__也會被調用, 其中addSpaceGeometryMapping API接口完成幾何數據加載工做
(注意:爲了加載大量數據不讓進程卡頓,這個數據加載是多線程的,它會經過一些回調來告訴開發者加載狀態,具體參考API手冊)。

  1. class Space(KBEngine.Entity, GameObject):
  2.               def __init__(self):
  3.              KBEngine.addSpaceGeometryMapping(self.spaceID, None, resPath)
 
十四:在世界中投放NPC/Monster
Space的cell建立完畢以後, 引擎會調用base上的Space實體, 告知已經得到了cell(onGetCell),那麼咱們確認cell部分建立好了以後就能夠開始投放NPC出生點了。
(注意:這裏並非直接將NPC/Monster建立出來,而是先在對應的位置建立了一個出生點, 出生點的好處是能夠根據必定規則, 當NPC/Monster在某區域減小的時候
能夠在合適的時候將其建立出來,例如:一羣怪被玩家清理掉了,半小時後怪刷出。)

onGetCell添加了一個刷出生點的定時器, 咱們不能一次性建立出全部的出生點,由於數量可能不少, 使用定時器分批建立。
  1. scripts/base/space.py:
  2. def onGetCell(self):
  3.                       """
  4.                       KBEngine method.
  5.                       entity的cell部分實體被建立成功
  6.                       """
  7.                 
  8.                       self.addTimer(0.1, 0.1, SCDefine.TIMER_TYPE_SPACE_SPAWN_TICK)
  9.                 
  10.                
複製代碼

出生點的數據(實體類型、座標、朝向等)是經過配置文件給出的,script/data/d_spaces_spawns.py與script/data/spawnpoints/xinshoucun_spawnpoints.xml 關於這2個配置的由來能夠參考配置章節
  1. kbengine_demos_assets\scripts/base/space.py:
  2. def spawnOnTimer(self, tid, tno):
  3.           """
  4.           出生怪物
  5.           """
  6.           if len(self.tmpCreateEntityDatas) <= 0:
  7.                    self.delTimer(tid)
  8.                    return
  9.           datas = self.tmpCreateEntityDatas.pop(0)
  10.           if datas is None:
  11.                     ERROR_MSG("Space::onTimer: spawn %i is error!" % datas[0])
  12.                    KBEngine.createBaseAnywhere("SpawnPoint",
  13.                                     {"spawnEntityNO" : datas[0], \
  14.                                     "position" : datas[1], \
  15.                                     "direction" : datas[2], \
  16.                 "modelScale" : datas[3], \
  17.                                     "createToCell" : self.cell})
複製代碼

SpawnPoint實體被建立出來以後,其構造函數中會調用API接口建立實體的cell部分
  1. kbengine_demos_assets\scripts/base/spawnpoint.py:
  2. class SpawnPoint(KBEngine.Base, GameObject):
  3.           def __init__(self):
  4.                      self.createCellEntity(self.createToCell)
複製代碼

SpawnPoint的cell部分會在當前位置根據自身被建立時所給予的參數信息來建立出真正的NPC/Monster
  1. kbengine_demos_assets\scripts/base/spawnpoint.py:
  2.     def spawnTimer(self, tid, tno):
  3.         datas = d_entities.datas.get(self.spawnEntityNO)
  4.         
  5.         if datas is None:
  6.             ERROR_MSG("SpawnPoint::spawn:%i not found." % self.spawnEntityNO)
  7.             return
  8.             
  9.         params = {
  10.             "spawnID"    : self.id,
  11.             "spawnPos" : tuple(self.position),
  12.             "uid" : datas["id"],
  13.             "utype" : datas["etype"],
  14.             "modelID" : datas["modelID"],
  15.             "modelScale" : self.modelScale,
  16.             "dialogID" : datas["dialogID"],
  17.             "name" : datas["name"],
  18.             "descr" : datas.get("descr", ''),
  19.         }
  20.         
  21.         e = KBEngine.createEntity(datas["entityType"], self.spaceID, tuple(self.position), tuple(self.direction), params)
 
十五:Monster的AI(移動、攻擊、思考)
Monster繼承了一系列的接口, 每種接口對應於不一樣的功能。
(注意:這裏使用的繼承而沒有用組件的緣由是目前的設計def定義的遠程方法只能與entity是同一個層的,能夠理解爲entity.xxx一級的屬性,若是是組件形式則entity.component.xxx方法是沒法被遠程調用到的。
必定要使用組件形式也能夠, 繼承這些接口以後,在接口模塊中實現組件, 若是有須要遠程調用的接口則經過接口層向組件中轉發)
  1. class Monster(KBEngine.Entity,    // 每一個實體都必須從引擎基本實體類型繼承出來,這樣引擎才能夠維護,並擁有一些API特性
  2.                         NPCObject, 
  3.                         Flags,                                          // 一個管理標記信息的模塊,標記如: 正在交易中、正在xx。
  4.                         State,                                         // 狀態模塊, 主狀態例如:死亡、活着。子狀態例如:閒置狀態、戰鬥狀態
  5.                         Motion,                                       // 關於移動的封裝
  6.                         Combat,                                    // 關於戰鬥公式、戰鬥屬性等等的封裝
  7.                         Spell,                                          // 技能釋放、buff/debuff維護等
  8.                         AI):                                            // 智能思考模塊
複製代碼

移動實體:
    scripts/cell/Motion.py                randomWalk : 隨機走動, 一般用於怪物閒置狀態時的走動
              backSpawnPos: 返回出生點,若是怪物被引誘至較遠距離,則返回到出生時的點,避免被玩家帶到別處。
              gotoEntity: 移動到目標實體的位置。
              gotoPosition:移動到目標座標點     實體繼承與這個功能模塊以後,實體就能夠調用相關方法來移動了, 例如:monster.randomWalk()。 這些移動函數都是二次封裝的,裏面調用了引擎所提供的底層API函數來實現。
思考與攻擊:
這裏思考模塊作的比較簡單,只是添加了一個定時器以必定頻率執行一些流程, 這些流程根據狀態區分, 例如:怪物主狀態爲活着, 子狀態爲戰鬥時, 流程中(onThinkFight)會不斷檢查本身敵人列表的敵人,
根據敵人的狀況決定是否攻擊或者追擊。 當距離敵人較遠時使用「self.gotoPosition(entity.position, attackMaxDist - 0.2)」移動到離敵人較勁的可攻擊距離, 當可攻擊距離時對目標釋放一個技能「self.spellTarget(skillID, entity.id)」

須要注意的是,  服務端上怪物成千上萬, 而AI是比較耗的,若是隻有一個玩家在線, 顯然大量的怪物是不須要開啓AI思考來白白耗掉CPU的, 這裏有一個優化方法。
只有在玩家視野範圍內的怪物才激活AI思考:
  1.         def onWitnessed(self, isWitnessed):
  2.                 """
  3.                 KBEngine method.
  4.                 此實體是否被觀察者(player)觀察到, 此接口主要是提供給服務器作一些性能方面的優化工做,
  5.                 在一般狀況下,一些entity不被任何客戶端所觀察到的時候, 他們不須要作任何工做, 利用此接口
  6.                 能夠在適當的時候激活或者中止這個entity的任意行爲。
  7.                 @param isWitnessed        : 爲false時, entity脫離了任何觀察者的觀察
  8.                 """
  9.                 INFO_MSG("%s::onWitnessed: %i isWitnessed=%i." % (self.getScriptName(), self.id, isWitnessed))
  10.                 
  11.                 if isWitnessed:
  12.                         self.enable()
 
十六:場景傳送
首先看看API接口的要求
  1. def teleport( self, nearbyMBRef, position, direction ): 
  2. 功能說明:
  3. 瞬間移動一個Entity到一個指定的空間。這個函數容許指定實體移動後的位置與朝向。
  4. 若是須要在不一樣空間跳轉( 一般用於不一樣場景或者房間跳轉 ),能夠傳一個CellMailbox給這個函數( 這個mailbox所對應的實體必須在目的空間中 )。 
  5. 這個函數只能在real的實體上被調用。 
  6. 參數: nearbyMBRef 一個決定Entity跳往哪一個Space的CellMailbox( 這個mailbox所對應的實體必須在目的Space中 ),它被認爲是傳送目的地。這個能夠設爲None,在這種情形下它會在當前的cell完成瞬移。  
  7. position Entity瞬移後的座標,是一個有3個float(x, y, z)組成的序列。  
  8. direction Entity瞬移後的朝向,是一個由3個float組成的序列(roll,pitch, yaw)。  
複製代碼

demo中能夠看見2個傳送門實體, 對應服務端的腳本爲Gate.py
  1. class Gate(KBEngine.Entity, GameObject):
  2.         def __init__(self):
  3.                 KBEngine.Entity.__init__(self)
  4.                 GameObject.__init__(self) 
  5.                 
  6.                 self.addTimer(1, 0, SCDefine.TIMER_TYPE_HEARDBEAT)                                # 心跳timer, 每1秒一次
  7.         # ----------------------------------------------------------------
  8.         # callback
  9.         # ----------------------------------------------------------------
  10.         def onHeardTimer(self, tid, tno):
  11.                 """
  12.                 entity的心跳
  13.                 """
  14.                 self.addProximity(5.0, 0, 0)
  15.                 
  16.         def onEnterTrap(self, entityEntering, range_xz, range_y, controllerID, userarg):
  17.                 """
  18.                 KBEngine method.
  19.                 有entity進入trap
  20.                 """
  21.                 if entityEntering.isDestroyed or entityEntering.getScriptName() != "Avatar":
  22.                         return
  23.                         
  24.                 DEBUG_MSG("%s::onEnterTrap: %i entityEntering=(%s)%i, range_xz=%s, range_y=%s, controllerID=%i, userarg=%i" % \
  25.                                                (self.getScriptName(), 
  26. self.id, entityEntering.getScriptName(), entityEntering.id, \
  27.                                                 range_xz, range_y, controllerID, userarg))
  28.                 
  29.                 if self.uid == 40001003: # currspace - teleport
  30.                         spaceData = d_spaces.datas.get(entityEntering.spaceUType)
  31.                         entityEntering.teleport(None, spaceData["spawnPos"], tuple(self.direction))                
  32.                 else:                                         # teleport to xxspace
  33.                         if entityEntering.spaceUType == 3:
  34.                                 gotoSpaceUType = 4
  35.                         else:
  36.                                 gotoSpaceUType = 3
  37.                         
  38.                         spaceData = d_spaces.datas.get(gotoSpaceUType)
  39.                         entityEntering.teleportSpace(gotoSpaceUType, spaceData["spawnPos"], tuple(self.direction), {})
  40.         def onLeaveTrap(self, entityLeaving, range_xz, range_y, controllerID, userarg):
  41.                 """
  42.                 KBEngine method.
  43.                 有entity離開trap
  44.                 """
  45.                 if entityLeaving.isDestroyed or entityLeaving.getScriptName() != "Avatar":
  46.                         return
  47.                         
  48.                 INFO_MSG("%s::onLeaveTrap: %i entityLeaving=(%s)%i." % (self.getScriptName(), self.id, \
  49.                                 entityLeaving.getScriptName(), entityLeaving.id))
複製代碼

在onHeardTimer中添加了一個範圍觸發器,當某個實體進入當前實體必定範圍內觸發器觸發回調onEnterTrap, 當在範圍內的實體離開了範圍則觸發回調onLeaveTrap。
其中進入範圍回調中調用了場景傳送接口, 「entityEntering.teleportSpace(gotoSpaceUType, spaceData["spawnPos"], tuple(self.direction), {})」, 這個接口首先會從KBEngine.globalData中得到
世界管理器的baseMailbox, 而後調用他的base方法teleportSpace, scripts/base/Spaces.py中teleportSpace方法找到對應的space, 而後將本身的cellMailbox回調給cell上的玩家實體(Avatar),
  1.         <b><b><b><b>scripts/base/Space.py</b></b></b></b>
  2. def teleportSpace(self, entityMailbox, position, direction, context):
  3.                 """
  4.                 defined method.
  5.                 請求進入某個space中
  6.                 """
  7.                 entityMailbox.cell.onTeleportSpaceCB(self.cell, self.spaceUTypeB, position, direction)
複製代碼

玩家得到space的cell以後就能夠調用API正式跳轉到指定空間中
  1.         def onTeleportSpaceCB(self, spaceCellMailbox, spaceUType, position, direction):
  2.                 """
  3.                 defined.
  4.                 baseapp返回teleportSpace的回調
  5.                 """
  6.                 self.teleport(spaceCellMailbox, position, direction)
相關文章
相關標籤/搜索