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)
相关文章
相关标签/搜索