一:环境搭建
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函数。
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); } /* 创建账号,通过loginapp */ 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); } /* 登录到服务端(loginapp), 登录成功后还必须登录到网关(baseapp)登录流程才算完毕 */ 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)