KBEngine简单RPG-Demo源码解析

2023-11-04

一:环境搭建


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)

九:游戏配置
服务端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函数。

  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()中被正式处理
  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进程.
  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和端口信息)
  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)
转自:http://bbs.kbengine.org/forum.php?mod=viewthread&tid=166&extra=page%3D1
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

KBEngine简单RPG-Demo源码解析 的相关文章

  • webpack5之loader配置(四)

    总述 要明白为什么要引入loader 因为webpack只能理解js和json这样的文件 这是webpack开箱可用的自带能力 loader可以让 webpack去解析其他类型的文件 并且将这些文件转化为有效的模块以供我们应用程序使用 如何
  • 国产CAE的涅槃-岩土行业高性能离散元软件MatDEM

    作者 刘春博士 一 导读 2019年9月11日 ANSYS公司公开宣称 收购LSTC公司 一举获得其旗下拥有LS DYNA 结构 流体 电磁的多物理场求解器 LS PrePost 前后处理器 LS OPT LS TASC 参数优化 拓扑优化
  • 微信小程序——关于图片

    属性 类型 默认值 必填 说明 src string 否 图片资源地址 mode string scaleToFill 否 图片裁剪 缩放的模式 webp boolean false 否 默认不解析 webP 格式 只支持网络资源 lazy
  • 我在训练maskrcnn模型识别羊的时候发现,用了1500张图片,模型的损失是逐渐降低的,预测框标注的很好,但是掩膜图层识别效果非常差,resnet使用的是101,详细说出为什么这种原因,并列出详细的...

    在训练 Mask R CNN 模型识别羊时 用了 1500 张图片 模型的损失是逐渐降低的 说明模型在训练过程中较好地学习了预测框标注的特征 然而 掩膜图层识别效果非常差 这可能是由以下几种原因造成的 数据不足 如果训练数据中羊的掩膜图层标
  • QT环境搭建:解决Linux系统下QT(qDebug和console)无输出

    在Linux下调试时突然发现无论怎样qDebuge无法输出了 寻找了半天 发现是环境变量出了问题 我的环境是统信的UOS 解决办法就是修改QT LOGGING RULES变量 1 查看是否是这个变量的问题 export p grep QT
  • linux各种版本下载地址

    http blog chinaunix net uid 26760055 id 3138203 html 首先提供两个镜像站 http mirrors sohu com http mirrors 163 com Red Hat Enterp
  • linux ubuntu18.04安装(更新)cmake版本 (坑是真滴多,我都给你趟过去了)

    笔者就是想学一手网络编程 通过本地clion编写代码 在云服务器上跑起来 这就涉及到了cmake版本问题 本地编译器cmake版本为3 22 3 云服务器的版本过于低了 用这个语句装的版本为3 10 好多人都不建议用这个装 但是这个不装的话
  • 简单的整理一下VIM环境配置和插件安装

    http www zhaiqianfeng com 2017 02 install vim plugins html 先占个坑 周末有时间再写
  • Java Stream流

    目录 1 Stream 流的获取方法 2 Stream 流的常见中间方法 3 Stream 流的常见终结方法 4 Stream 流的收集方法 获取 Stream 流 创建一条流水线 并把数据放到流水线上准备进行操作 中间方法 流水线上的操作
  • nginx篇10-限速三剑客之limit_req

    本文主要是对nginx官方limit req相关模块的限速原理的解释和一些个人理解 主要参考的文章为Rate Limiting with NGINX and NGINX Plus和nginx的ngx http limit req modul
  • C++编码规范(1):代码注释

    当你阅读别人的代码时如果没有注释那会是件比较痛苦的事 一说到注释我们马上想到是通过 或 这样来添加一些描述信息 这只是狭义的注释 广义的注释我们可以理解为 任何有助于理解代码的信息都可以看成注释 我们可以把写代码和写文章类比下 自然语言会有
  • Qt - 获取屏幕分辨率

    欢迎转载 请注明出处 https blog csdn net qq 39453936 spm 1010 2135 3001 5343 原文链接 https blog csdn net qq 39453936 article details
  • MySQL必知必会——第十七章组合查询

    组合查询 本章讲述如何利用UNION操作符将多条SELECT语句组合成一个结果集 组合查询 多数SQL查询都只包含从一个或多个表中返回数据的单条SELECT语句 MySQL也允许执行多个查询 并将结果作为单个查询结果返回 这些组合查询通常称
  • chatgpt提问句式整理

    你不知道 GPT知道 1 元问题 我想了解xxxx 我应该向你问哪些问题 2 请给我列出xxx领域 行业相关的 最常用的50个概念 并做简单解释 如果有英文缩写 请给出完整的英文解释 3 请详细介绍一下elon musk的主要生平事迹 请详
  • 设计模式:再谈单例模式

    单例模式可以说是每个人问设计模式都会脱口而出的几个设计模式之一 为什么之前写过一次了 现在我又写一遍 肯定不是重新写一遍怎么是设计模式 这次写的单例模式主要围绕的关键词有 延迟加载 高并发 线程安全 为什么说用枚举的单例模式优雅且怎么做到的
  • 服务器出现漏洞如何处理

    1 从基本做起 及时安装系统补丁 不管是什么操作系统 都是在更新中不断完善 都存在着漏洞 这些漏洞就是电脑被入侵的最 好通道 所以及时打补丁更新系统 防止被攻击利用 是服务器最重要的安全保证之一 2 安装杀毒软件 现在网络上的病毒非常猖獗
  • ConcurrentHashMap中有十个提升性能的细节,你都知道吗?

    历史文章推荐 HashMap面试 看这一篇就够了 七种方式教你在SpringBoot初始化时搞点事情 Java序列化的这三个坑千万要小心 Java中七个潜在的内存泄露风险 你知道几个 JDK 16新特性一览 啥 用了并行流还更慢了 Inno
  • layui上传文件php上传接口异常,layui.upload上传图片报错“请求上传接口出现异常”...

    layui upload上传图片报错 请求上传接口出现异常 且接口报404问题 在调试layui upload上传图片时候报错 请求上传接口出现异常 且接口返回是404 返回接口丢失了 复盘处理流程 花了一些事件 做了如下的一些问题筛选处理

随机推荐

  • 企业信息化快速开发平台 JeeSite

    JeeSite是基于多个优秀的开源项目 高度整合封装而成的高效 高性能 强安全性的 开源 Java EE快速开发平台 JeeSite本身是以Spring Framework为核心容器 Spring MVC为模型视图控制器 MyBatis为数
  • 特征选择过滤器 - mutual_info_regression(连续目标变量的互信息)

    文章目录 函数 参数说明 函数 sklearn feature selection mutual info regression X y discrete features auto n neighbors 3 copy True rand
  • AD17 PCB板框无法设置

    could not find board outline using primitives centerline due to the following error at least 2 connected tracks arcs or
  • Python实现定时执行脚本(1)

    前言 本文是该专栏的第11篇 后面会持续分享python的各种干货知识 值得关注 很多时候 我们需要对工作上的python脚本附加一个定时执行的功能 让脚本自动在某个时间段开始执行 当接到这样的需求时 你脑海中想到的会不会是Time模块 的
  • 关于GD32调试JLINK连接不上的问题

    最近有一个项目要用到GD32E230 然后经过了绘画原理图 PCB布线 焊接元件 再到最后一步的调试 发现用JLINK的SWD模式无论怎么也连不上 在和同事纠结了两天之后 发现了原来是PCB里面IC的封装画错了 以下把GD32和STM32官
  • 第九节:JS中的循环语句

    while语句 一般在循环体中来改变判断条件的值 如果不进行值得改变 循环条件一直满足 会造成死循环 语法 while 条件 要执行的代码块 例子 while i lt 10 text 数字是 i i do while 循环是 while
  • 功率MOSFET的正向导通等效电路

    转自 功率MOSFET的正向导通等效电路 电路设计论坛 电子技术论坛 广受欢迎的专业电子论坛 MOSFET 一 基础 Infinity lsc的博客 CSDN博客 MOSFET 二 米勒效应 Infinity lsc的博客 CSDN博客 米
  • 物联网之Linux网络编程三

    本篇主讲内容 1 广播和组播 2 UNIX域套接字 3 网络总结 广播 前面介绍的数据包发送方式只有一个接受方 称为单播 如果同时发给局域网中的所有主机 称为广播 只有用户数据报 使用UDP协议 套接字才能广播 广播地址 以192 168
  • 如何查看Oracle数据库的端口列表Portlist?

    如何查看Oracle数据库的端口列表Portlist 要在SQL PLUS工具中查看Oracle数据库的端口列表 可以执行以下步骤 在SQL PLUS中使用系统管理员帐户登录到Oracle数据库 运行以下命令 SELECT DISTINCT
  • 最大熵阈值分割算法原理及实现

    写在前面 前面介绍了OTSU算法 对于阈值分割法 不得不介绍另外一种较为突出的算法 最大熵阈值分割法 KSW熵算法 最大熵阈值分割法和OTSU算法类似 假设将图像分为背景和前景两个部分 熵代表信息量 图像信息量越大 熵就越大 最大熵算法就是
  • android FlatBuffers剖析

    概述 FlatBuffers是google最新针对游戏开发退出的高性能的跨平台序列化工具 目前已经支持C C Go Java JavaScript PHP and Python C和Ruby正在支持中 相对于json和Protocol Bu
  • Pytorch 分布式训练(DP/DDP)

    参考 需要看 0 实操教程 GPU多卡并行训练总结 以pytorch为例 1 PyTorch 源码解读之 DP DDP 模型并行和分布式训练解析 知乎 2 pytorch中分布式训练DP DDP原理 知乎 3 pytorch中多卡训练 yt
  • 共轭梯度法详细推导分析

    共轭梯度法是一种经典的优化算法 算法求解速度较快 虽然比梯度下降法复杂 但是比二阶方法简单 一 引入 1 优化模型建立 假定待优化的问题如下所示 min x f x 1 2 x T A x b T x min x f x frac 1 2
  • vs 查看 C++ #define 宏定义展开代码

    方法一 define 通常是代码的展开 如果是 复杂 的 define 如何直观的查看展开效果 在 vs 中 选中需要查看的 cpp 文件 右键属性 预处理器 预处理到文件 选择是 确定 右键 cpp 文件 编译 debug 目录中会有一个
  • 打印机驱动如何连接计算机,打印机与电脑第一次连接驱动怎么操作

    打印机作为我们办公的设备之一 在使用过程中需要与电脑完成连接 才能进行使用 很多小伙伴不知道打印机与电脑第一次连接驱动怎么操作 于是就在小编后台给小编留言 那么今天小编就来叫教教大家打印机与电脑第一次连接驱动的方法 下面就让我们一起来看看吧
  • Tomcat性能调优方案

    Tomcat性能调优方案 一 操作系统调优 对于操作系统优化来说 是尽可能的增大可使用的内存容量 提高CPU的频率 保证文件系统的读写速率等 经过压力测试验证 在并发连接很多的情况下 CPU的处理能力越强 系统运行速度越快 适用场景 任何项
  • Python3-excel文档操作(二):利用openpyxl库处理excel表格:在excel表格中插入图片

    1 简介 excel表中可以插入图片 使用openpyxl库可以实现这个功能 2 代码 coding utf 8 import os import sys import time import openpyxl from openpyxl
  • 在React中使用Swiper做触摸内容滑动

    在React中使用Swiper做触摸内容滑动 缘由 在实现一个触摸内容滑动的功能的时候 想reactJS该怎么处理呢 然后被提点用Swiper可以 一想确实可以 用了网上的一些用在react里面的Swiper 但是并不怎么靠谱和好用 最后看
  • 【改进灰狼优化算法】改进收敛因子和比例权重的灰狼优化算法【期刊论文完美复现】(Matlab代码实现)

    个人主页 研学社的博客 欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 文献来源 4 Matlab代码实现 1 概述 文献来源
  • KBEngine简单RPG-Demo源码解析

    一 环境搭建 1 确保已经下载过KBEngine服务端引擎 如果没有下载请先下载 下载服务端源码 KBEngine https github com kbengine kbengine releases latest 编译 KBEngine