一:環境搭建
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
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicGcq5CeTJVSfdTM4IjMxATMvwFNwUTMwIzLcdWbp9CXzRWYvxGc19CX0VmbuEmbph2Yz9mLjlGdhR3cvw1LcpDc0RHaiojIsJye.jpg)
* 或者使用 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)
九:遊戲配置
服務端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函數。
- public void createAccount()
- {
- KBEngine.Event.fireIn("createAccount", new object[]{stringAccount, stringPasswd});
- }
複制代碼
2. 事件在KBE插件中kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, KBEngineApp.process()中被正式處理
- public virtual void process()
- {
- // 處理網絡
- _networkInterface.process();
- // 處理外層抛入的事件
- Event.processInEvents();
- // 向服務端發送心跳以及同步角色資訊到服務端
- sendTick();
- }
複制代碼 3. 建立賬号函數被調用, createAccount_loginapp函數表示請求向服務端loginapp程序要求建立一個賬号,而此時可能還沒有連接配接伺服器,需要先連接配接,如果已經連接配接上了則向loginapp發送一個包“bundle.send”。
可以看到向Bundle中寫入了相關需要的資料,而Bundle會将資料序列化成二進制流,服務端會采用相同的協定将其歸原并将調用服務端協定所綁定的方法(後面會講到服務端具體方法)。
- public void createAccount(string username, string password)
- {
- KBEngineApp.app.username = username;
- KBEngineApp.app.password = password;
- KBEngineApp.app.createAccount_loginapp(true);
- }
- public void createAccount_loginapp(bool noconnect)
- {
- if(noconnect)
- {
- reset();
- _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_createAccount_callback, null);
- }
- else
- {
- Bundle bundle = new Bundle();
- bundle.newMessage(Message.messages["Loginapp_reqCreateAccount"]);
- bundle.writeString(username);
- bundle.writeString(password);
- bundle.writeBlob(new byte[0]);
- bundle.send(_networkInterface);
- }
- }
複制代碼 建立傳回結果:
UI.cs -> onCreateAccountResult
服務端部分:
1. 通過上面可以得知用戶端向服務端發送了一條建立賬号的協定, 協定名稱為“Loginapp_reqCreateAccount”(注意,所有的協定名稱都能在服務端找到對應的方法, Loginapp_代表了協定的作用域僅為Loginapp, 方法名稱為reqCreateAccount)
- void Loginapp::reqCreateAccount(Network::Channel* pChannel, MemoryStream& s)
- {
- std::string accountName, password, datas;
- s >> accountName >> password;
- s.readBlob(datas);
- if(!_createAccount(pChannel, accountName, password, datas, ACCOUNT_TYPE(g_serverConfig.getLoginApp().account_type)))
- return;
- }
複制代碼 服務端解析出了賬号名與密碼,在_createAccount函數中會将這條請求最終送到dbmgr,dbmgr檢查之後決定是否建立資料庫賬号,并最終将結果傳回到loginapp,然後由loginapp将結果中轉至用戶端。
十一:登入賬号
1: kbengine_unity3d_demo\Assets\Scripts\u3d_scripts\UI.cs, 向KBE層觸發了登陸事件
- public void login()
- {
- info("connect to server...(連接配接到服務端...)");
- KBEngine.Event.fireIn("login", new object[]{stringAccount, stringPasswd});
- }
複制代碼 2: kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, 插件觸發登陸函數,并最終向loginapp發送了一個登陸包“Loginapp_login”
- public void login(string username, string password)
- {
- KBEngineApp.app.username = username;
- KBEngineApp.app.password = password;
- KBEngineApp.app.login_loginapp(true);
- }
- public void login_loginapp(bool noconnect)
- {
- if(noconnect)
- {
- reset();
- _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_loginapp_callback, null);
- }
- else
- {
- Dbg.DEBUG_MSG("KBEngine::login_loginapp(): send login! username=" + username);
- Bundle bundle = new Bundle();
- bundle.newMessage(Message.messages["Loginapp_login"]);
- bundle.writeInt8((sbyte)_args.clientType); // clientType
- bundle.writeBlob(new byte[0]);
- bundle.writeString(username);
- bundle.writeString(password);
- bundle.send(_networkInterface);
- }
- }
複制代碼
服務端部分:
1:服務端loginapp.cpp中“void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)”被觸發, 這個函數進行了一系列的檢查,
确定合法後向dbmgr發送一個登陸請求包“(*pBundle).newMessage(DbmgrInterface:
nAccountLogin);”, dbmgr也會進行一系列的檢查并将登陸結果傳回到loginapp。
- void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)
- {
- ...
- ...
- if(loginName.size() > ACCOUNT_NAME_MAX_LENGTH)
- {
- INFO_MSG(fmt::format("Loginapp::login: loginName is too long, size={}, limit={}.\n",
- loginName.size(), ACCOUNT_NAME_MAX_LENGTH));
- _loginFailed(pChannel, loginName, SERVER_ERR_NAME, datas, true);
- s.done();
- return;
- }
- if(password.size() > ACCOUNT_PASSWD_MAX_LENGTH)
- {
- INFO_MSG(fmt::format("Loginapp::login: password is too long, size={}, limit={}.\n",
- password.size(), ACCOUNT_PASSWD_MAX_LENGTH));
- ...
- ...
- ...
- // 向dbmgr查詢使用者合法性
- Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
- (*pBundle).newMessage(DbmgrInterface::onAccountLogin);
- (*pBundle) << loginName << password;
- (*pBundle).appendBlob(datas);
- dbmgrinfos->pChannel->send(pBundle);
- }
複制代碼
1.1: loginapp得到dbmgr的登入合法結果後向baseappmgr發送了配置設定網關(baseapp)請求(registerPendingAccountToBaseapp), 通常是負載較低的一個baseapp程序.
- void Loginapp::onLoginAccountQueryResultFromDbmgr(Network::Channel* pChannel, MemoryStream& s)
- {
- ...
- ...
- ...
- // 如果大于0則說明目前賬号仍然存活于某個baseapp上
- if(componentID > 0)
- {
- Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
- (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseappAddr);
- (*pBundle) << componentID << loginName << accountName << password << entityID << dbid << flags << deadline << infos->ctype;
- baseappmgrinfos->pChannel->send(pBundle);
- return;
- }
- else
- {
- // 注冊到baseapp并且擷取baseapp的位址
- Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
- (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseapp);
- (*pBundle) << loginName;
- (*pBundle) << accountName;
- (*pBundle) << password;
- (*pBundle) << dbid;
- (*pBundle) << flags;
- (*pBundle) << deadline;
- (*pBundle) << infos->ctype;
- baseappmgrinfos->pChannel->send(pBundle);
- }
- }
複制代碼 1.2:baseappmgr最終傳回所配置設定的baseapp的ip位址等資訊,loginapp将其轉發給用戶端(登入成功協定onLoginSuccessfully,包含baseapp的ip和端口資訊)
- void Loginapp::onLoginAccountQueryBaseappAddrFromBaseappmgr(Network::Channel* pChannel, std::string& loginName,
- std::string& accountName, std::string& addr, uint16 port)
- {
- ...
- ...
- ...
- Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
- (*pBundle).newMessage(ClientInterface::onLoginSuccessfully);
- uint16 fport = ntohs(port);
- (*pBundle) << accountName;
- (*pBundle) << addr;
- (*pBundle) << fport;
- (*pBundle).appendBlob(infos->datas);
- pClientChannel->send(pBundle);
- SAFE_RELEASE(infos);
- }
複制代碼 2: 用戶端插件得到傳回結果後調用KBEngineApp.cs->login_baseapp()函數開始正式登入到baseapp。
3:baseapp收到登入請求
- void Baseapp::loginGateway(Network::Channel* pChannel,
- std::string& accountName,
- 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”:
- def onBaseAppReady(isBootstrap):
- """
- KBEngine method.
- baseapp已經準備好了
- @param isBootstrap: 是否為第一個啟動的baseapp
- @type isBootstrap: BOOL
- """
- INFO_MSG('onBaseAppReady: isBootstrap=%s' % isBootstrap)
- # 安裝螢幕
- Watcher.setup()
- if isBootstrap:
- # 建立spacemanager
- KBEngine.createBaseLocally( "Spaces", {} )
複制代碼
2. 世界管理器建立出所有的場景
在spaces.py中, spaces通過initAlloc函數根據配置中scripts/data/d_spaces.py建立出space實體,space實體描述的是一個抽象空間,一個空間可以被邏輯定義為大地圖、場景、房間、宇宙等等。
- def initAlloc(self):
- # 注冊一個定時器,在這個定時器中我們每個周期都建立出一些Space,直到建立完所有
- self._spaceAllocs = {}
- self.addTimer(3, 1, SCDefine.TIMER_TYPE_CREATE_SPACES)
- self._tmpDatas = list(d_spaces.datas.keys())
- for utype in self._tmpDatas:
- spaceData = d_spaces.datas.get(utype)
- if spaceData["entityType"] == "SpaceDuplicate":
- self._spaceAllocs[utype] = SpaceAllocDuplicate(utype)
- else:
- self._spaceAllocs[utype] = SpaceAlloc(utype)
複制代碼
SpaceAlloc: 普通地圖,可以了解為大地圖,但整個世界中隻能有一個這樣類型的地圖。
SpaceAllocDuplicate:副本地圖,可以複制出很多個
上面函數注冊了一個定時器, 這裡是定時器的回調, 每一秒回調一次。
self._spaceAllocs[spaceUType].init(), 這裡真正開始建立這些space實體, 裡面調用的createBaseAnywhere函數來建立實體, 如果啟動了多個baseapp這個函數根據負載情況将實體選擇到合适的程序中建立。
- def createSpaceOnTimer(self, tid, tno):
- """
- 建立space
- """
- if len(self._tmpDatas) > 0:
- spaceUType = self._tmpDatas.pop(0)
- self._spaceAllocs[spaceUType].init()
- if len(self._tmpDatas) <= 0:
- del self._tmpDatas
- self.delTimer(tid)
複制代碼
Space實體建立出來之後,此時還沒有真正建立出空間, 這個實體僅僅是将要與某個真正空間關聯的實體, 可以通過它來操控那個空間。
但空間隻能在cellapp上存在, 是以我們需要調用API讓實體在cell上建立出一個空間,并在cell上建立出一個實體與空間關聯, 這個實體就像一個空間的句柄。
- class Space(KBEngine.Base, GameObject):
- def __init__(self):
- self.createInNewSpace(None)
複制代碼 此功能由createInNewSpace完成, __init__可以了解為Space的構造函數。
3. 為這個抽象的空間增加幾何資料
有指定幾何資料的空間可以被看做是一個特定的場景, 這些幾何資料與用戶端對應的場景表現相關聯, 例如:導航網格(navmesh), 服務端通過這些資料讓NPC進行正确的移動,碰撞等。
上面Space建立cell部分之後, cell上的Space._init__也會被調用, 其中addSpaceGeometryMapping API接口完成幾何資料加載工作
(注意:為了加載大量資料不讓程序卡頓,這個資料加載是多線程的,它會通過一些回調來告訴開發者加載狀态,具體參考API手冊)。
- class Space(KBEngine.Entity, GameObject):
- def __init__(self):
- KBEngine.addSpaceGeometryMapping(self.spaceID, None, resPath)
十四:在世界中投放NPC/Monster
Space的cell建立完畢之後, 引擎會調用base上的Space實體, 告知已經獲得了cell(onGetCell),那麼我們确認cell部分建立好了之後就可以開始投放NPC出生點了 。
(注意:這裡并不是直接将NPC/Monster建立出來,而是先在對應的位置建立了一個出生點, 出生點的好處是可以根據一定規則, 當NPC/Monster在某區域減少的時候
可以在合适的時候将其建立出來,例如:一群怪被玩家清理掉了,半小時後怪刷出。 )
onGetCell添加了一個刷出生點的定時器, 我們不能一次性建立出所有的出生點,因為數量可能很多, 使用定時器分批建立 。
- scripts/base/space.py:
- def onGetCell(self):
- """
- KBEngine method.
- entity的cell部分實體被建立成功
- """
- self.addTimer(0.1, 0.1, SCDefine.TIMER_TYPE_SPACE_SPAWN_TICK)
複制代碼
出生點的資料(實體類型、坐标、朝向等)是通過配置檔案給出的,script/data/d_spaces_spawns.py與script/data /spawnpoints/xinshoucun_spawnpoints.xml 關于這2個配置的由來可以參考配置章節
- kbengine_demos_assets\scripts/base/space.py:
- def spawnOnTimer(self, tid, tno):
- """
- 出生怪物
- """
- if len(self.tmpCreateEntityDatas) <= 0:
- self.delTimer(tid)
- return
- datas = self.tmpCreateEntityDatas.pop(0)
- if datas is None:
- ERROR_MSG("Space::onTimer: spawn %i is error!" % datas[0])
- KBEngine.createBaseAnywhere("SpawnPoint",
- {"spawnEntityNO" : datas[0], \
- "position" : datas[1], \
- "direction" : datas[2], \
- "modelScale" : datas[3], \
- "createToCell" : self.cell})
複制代碼
SpawnPoint實體被建立出來之後,其構造函數中會調用API接口建立實體的cell部分
- kbengine_demos_assets\scripts/base/spawnpoint.py:
- class SpawnPoint(KBEngine.Base, GameObject):
- def __init__(self):
- self.createCellEntity(self.createToCell)
複制代碼
SpawnPoint的cell部分會在目前位置根據自身被建立時所給予的參數資訊來建立出真正的NPC/Monster
- kbengine_demos_assets\scripts/base/spawnpoint.py:
- def spawnTimer(self, tid, tno):
- datas = d_entities.datas.get(self.spawnEntityNO)
- if datas is None:
- ERROR_MSG("SpawnPoint::spawn:%i not found." % self.spawnEntityNO)
- return
- params = {
- "spawnID" : self.id,
- "spawnPos" : tuple(self.position),
- "uid" : datas["id"],
- "utype" : datas["etype"],
- "modelID" : datas["modelID"],
- "modelScale" : self.modelScale,
- "dialogID" : datas["dialogID"],
- "name" : datas["name"],
- "descr" : datas.get("descr", ''),
- }
- e = KBEngine.createEntity(datas["entityType"], self.spaceID, tuple(self.position), tuple(self.direction), params)
十五:Monster的AI(移動、攻擊、思考)
Monster繼承了一系列的接口, 每種接口對應于不同的功能。
(注意:這裡使用的繼承而沒有用元件的原因是目前的設計def定義的遠端方法隻能與entity是同一個層的,可以了解為entity.xxx一級的屬性,如果是元件形式則entity.component.xxx方法是無法被遠端調用到的。
一定要使用元件形式也可以, 繼承這些接口之後,在接口子產品中實作元件, 如果有需要遠端調用的接口則通過接口層向元件中轉發)
- class Monster(KBEngine.Entity, // 每個實體都必須從引擎基本實體類型繼承出來,這樣引擎才可以維護,并擁有一些API特性
- NPCObject,
- Flags, // 一個管理标記資訊的子產品,标記如: 正在交易中、正在xx。
- State, // 狀态子產品, 主狀态例如:死亡、活着。子狀态例如:閑置狀态、戰鬥狀态
- Motion, // 關于移動的封裝
- Combat, // 關于戰鬥公式、戰鬥屬性等等的封裝
- Spell, // 技能釋放、buff/debuff維護等
- 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思考:
- def onWitnessed(self, isWitnessed):
- """
- KBEngine method.
- 此實體是否被觀察者(player)觀察到, 此接口主要是提供給伺服器做一些性能方面的優化工作,
- 在通常情況下,一些entity不被任何用戶端所觀察到的時候, 他們不需要做任何工作, 利用此接口
- 可以在适當的時候激活或者停止這個entity的任意行為。
- @param isWitnessed : 為false時, entity脫離了任何觀察者的觀察
- """
- INFO_MSG("%s::onWitnessed: %i isWitnessed=%i." % (self.getScriptName(), self.id, isWitnessed))
- if isWitnessed:
- self.enable()
十六:場景傳送
首先看看API接口的要求
- def teleport( self, nearbyMBRef, position, direction ):
- 功能說明:
- 瞬間移動一個Entity到一個指定的空間。這個函數允許指定實體移動後的位置與朝向。
- 如果需要在不同空間跳轉( 通常用于不同場景或者房間跳轉 ),可以傳一個CellMailbox給這個函數( 這個mailbox所對應的實體必須在目的空間中 )。
- 這個函數隻能在real的實體上被調用。
- 參數: nearbyMBRef 一個決定Entity跳往哪個Space的CellMailbox( 這個mailbox所對應的實體必須在目的Space中 ),它被認為是傳送目的地。這個可以設為None,在這種情形下它會在目前的cell完成瞬移。
- position Entity瞬移後的坐标,是一個有3個float(x, y, z)組成的序列。
- direction Entity瞬移後的朝向,是一個由3個float組成的序列(roll,pitch, yaw)。
複制代碼
demo中可以看見2個傳送門實體, 對應服務端的腳本為Gate.py
- class Gate(KBEngine.Entity, GameObject):
- def __init__(self):
- KBEngine.Entity.__init__(self)
- GameObject.__init__(self)
- self.addTimer(1, 0, SCDefine.TIMER_TYPE_HEARDBEAT) # 心跳timer, 每1秒一次
- # ----------------------------------------------------------------
- # callback
- # ----------------------------------------------------------------
- def onHeardTimer(self, tid, tno):
- """
- entity的心跳
- """
- self.addProximity(5.0, 0, 0)
- def onEnterTrap(self, entityEntering, range_xz, range_y, controllerID, userarg):
- """
- KBEngine method.
- 有entity進入trap
- """
- if entityEntering.isDestroyed or entityEntering.getScriptName() != "Avatar":
- return
- DEBUG_MSG("%s::onEnterTrap: %i entityEntering=(%s)%i, range_xz=%s, range_y=%s, controllerID=%i, userarg=%i" % \
- (self.getScriptName(),
- self.id, entityEntering.getScriptName(), entityEntering.id, \
- range_xz, range_y, controllerID, userarg))
- if self.uid == 40001003: # currspace - teleport
- spaceData = d_spaces.datas.get(entityEntering.spaceUType)
- entityEntering.teleport(None, spaceData["spawnPos"], tuple(self.direction))
- else: # teleport to xxspace
- if entityEntering.spaceUType == 3:
- gotoSpaceUType = 4
- else:
- gotoSpaceUType = 3
- spaceData = d_spaces.datas.get(gotoSpaceUType)
- entityEntering.teleportSpace(gotoSpaceUType, spaceData["spawnPos"], tuple(self.direction), {})
- def onLeaveTrap(self, entityLeaving, range_xz, range_y, controllerID, userarg):
- """
- KBEngine method.
- 有entity離開trap
- """
- if entityLeaving.isDestroyed or entityLeaving.getScriptName() != "Avatar":
- return
- INFO_MSG("%s::onLeaveTrap: %i entityLeaving=(%s)%i." % (self.getScriptName(), self.id, \
- 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),
- <b><b><b><b>scripts/base/Space.py</b></b></b></b>
- def teleportSpace(self, entityMailbox, position, direction, context):
- """
- defined method.
- 請求進入某個space中
- """
- entityMailbox.cell.onTeleportSpaceCB(self.cell, self.spaceUTypeB, position, direction)
複制代碼
玩家獲得space的cell之後就可以調用API正式跳轉到指定空間中
- def onTeleportSpaceCB(self, spaceCellMailbox, spaceUType, position, direction):
- """
- defined.
- baseapp傳回teleportSpace的回調
- """
- self.teleport(spaceCellMailbox, position, direction)
轉自:http://bbs.kbengine.org/forum.php?mod=viewthread&tid=166&extra=page%3D1