部分引用KBEngine官网的一些句段,感谢kbe。
下列符号解释 =:继承 ==:等同 +:与上具有子关系
CellAppMgr:管理多个CellApp
+CellApp:管理多个区域
+ Cell:一个区域
==Space:等同Cell
+此区域上的玩家entity:代表玩家
+Witness(对象):监视周围的玩家entity,将发生的事件消息同步给客户端
+AOI(兴趣范围):默认500M
+GhostEntitys(list):存取此区域边沿外界一定距离内的玩家entity的列表
+GhostEntity:从邻近的Cell的对应的entity的部分数据的拷贝的实体
+属性数据:只读,如果某个属性对于客户端是可见的,那么该属性必须是可以存在
Ghost的,例如:当前的武器、等级、名称
+范围:默认500M,可配置,大于等于玩家的AOI
+负载平衡:CellApp会告诉它们的Cell的边界应该在哪里
+新建的玩家Entity加入到正确的Cell上
+一个服务器组一个Mgr实例
DBMgr:管理Entity数据的数据库存储
+存数据:在BaseApp间轮流调度处理,BaseApp向CellApp要entity的cell部分的数据再定时转给DBMgr存储
Machine:监视服务器进程信息,每个服务器机器上有一个machine
+作用:启动/停止服务器进程,通知服务器群组各个进程的存活状态,监视机器的使用状态:cpu/内存/带宽
+machine不会tcp连接的
ObjectPool:对象池,一些对象频繁的被创建,例如:moneystream,bundle,tcppacket等等,这个对象池通过服务端峰值有效的预
估提前创建一些对象缓存起来,在用到的时候直接从对象池中提取
SmartPoolObject:智能池对象,创建一个智能池对象,与相应的对象池绑定,只要此对象析构时,那么会调用绑定的对象池进行
回收
Components:组件类,记录当前组件信息
ServerApp:App基类,每个app都需继承这个类,基本的C++框架模块 都会存在这个类,其中有继承通道超时操作,继承通道取消注册操作,
Resmgr:资源类,存有一切环境信息,例如bin或者资源目录,和一些已打开文件
ThreadPool:线程池类
NetWork:网络
+Bundle:发送的包裹
+Packet:接收的包裹
+NetworkInterface:理解为网络上下文对象。
+EndPoint:对端,可以理解成与对端的会话信息及与对端网络的解决方案。硬封装 创建socket,绑定,监听,recv,send函数
+ListenerRecviver:保存有对端对象,特性,网络上下文对象(NetworkInterface)
+EventDispatcher:事件分配,封装着一个poller和一个Task(Task可以add,所以这个类又是一个可以作为多任务分配的任务集),接口基本都是用poller,注册事件。异步IO的体现
+Channel:代表着一个连接,成员有EndPoint,TcpPacketReceiver,TcpPacketSender,Init时会将fd注册到poller,接收到的包裹经过过滤器存放到bufferPacket,然后bufferPacket会被NetworkInterface类调用processChannels时调用本类的processPackets进行处理
+onPacketReceived方法:这个方法里是用来判断及计数的,例如lastTickBytesRecevied 这个成员变量是用来对一帧之内recv多次直至为空产生的数据量进行计数,查看TcpPacketReceiver这个类的processRecv方法与PacketReceiver::handleInputNotification方法,计数时要进行与kbengine.def配置的参数进行判断及处理
+PacketReader: 包裹阅读器,具有解单包功能,和操作单包,是在Channel被调用processPackets时被调用本类的processMessages函数
+TcpPacketReceiver = PacketReceiver:封装了接收行为,是驱动者,操作接受行为时是被调用processRecv这个方法,方法里操作:创建packet,调用EndPoint的recv,再调用本类的processPacket,processPacket中调用channel的onPacketReceived,过滤包裹形成可用包;processRecv被PacketReceiver::handleInputNotification(intfd)这个方法循环调用
+TcpPacketSender = PacketReceiver:封装了发送行为,是驱动者,创建包裹,最终是调用EndPoint的发送函数
+TcpPacket = Packet:tcp包裹,一个Channel会存放多个Packet,
+PacketFilter:包裹过滤器,将Packet存放到Channel,被PacketReceiver调用processPacket函数时调用了本类的recv函数,是在传输层recv后,立即调用的,也就是没放到buffer,还没调用到Channel类的processPackets函数之前
+ WebSocketPacketFilter=PacketFilter:继承于PacketFilter,重写PacketFiler的recv和send,因为websocket会有web格式包头信息,所以要先过滤掉
EpollPoller =EventPoller:网络模型类,使用epoll封装的消息事件类
SelectPoller=EventPoller:网络模型类,使用select封装的消息事件类
PyFileDescriptior:python层的文件描述类,用于设置Python的相对应的网络消息回调方法,网络事件触发时,会调用相对应的回调
内存结构:
MoneyStream:内存流
函数的认识:
QueryPerformanceCounter():获取高性能计时器从开机到调用的tick数
QueryPerformanceFrequency():获取高性能计时器的赫兹 也就是每秒多少次tick
以上两个函数可以计算出高精度的时间 误差看机器 一般是微妙级
KBEngine.calcStampsPerSecond():获取每秒多少次cpu tick
Python C API:
PyImport_GetModuleDict:获取模块管理的(python层面)字典?
Int PyDict_SetItemString(PyObject* p, const char* key, PyObject* val):插入值val到字典以key为key,python底层会调用
PyUnicode_FromString()创建一个unicode的PyObject对象。
PyUnicode_FromString(const char* u):创建一个unicode的PyObject对象。
PyModule_GetDef
ServerApp:最底层,C++服务端模型都在这层
PythonApp = ServerApp:Python模型在这层
KBE
+ C++层
+ 服务端底层框架
+ 游戏服务端逻辑框架
+ python层
+ 配合C++服务端逻辑框架层
+ 游戏逻辑框架
Xml:kbengine的类,完全操作一个xml文件,封装了tinyxml库的接口。
XML:使用了tinyxml解析库
Tinyxml库:
+TiXmlNode = TiXmlBase:一个xml节点,一个xml文件一般begin节点是document,节点成员有parent,prev,next,LastChild,FirstChild;parant就是这一条链表的父亲节点,下图的描述只存在于除了是属性信息TxXmlAttribute的情况
,
+TiXmlDocument = TiXmlNode:文档节点,读取xml文件的最开端,文档节点的parse函数就是将xml里面的内容解析成一个个节点链接在文件节点之后,
+TiXmlComment = TiXmlNode:注释节点,标志:begin:<!-- end:-->
+TiXmlDelcaration = TiXmlNode:声明节点,标志:begin:<?xml end:>
+TiXmlText = TiXmlNode:文本节点,有两种情况会创建;标志1:begin:<![CDATA[ end:]]>;标志2:例子:<Type> AVATAR </Type>,当读到<Type>的>时,继续跳过些空格,如果遇到字符不是'<',那么会创建这个类来保存后面的文本,直到遇到'<'。
+TiXmlElement = TiXmlNode:元素节点,保存有多个属性节点,标志:begin:< + alpha end:>,例:<Account></Account>
+TiXmAttribute = TiXmlBase:属性信息,属于元素节点的成员,例:<Account hasClient="true"></Account>这里面的 hasClient="true",name和value都有保存
+TiXmlParsingData:存有当前文件游标位置
ScriptDefModule:描述一个脚本def模块
DataType:存有不同和有别名的数据类型。有别名的数据类型是通过读取assets\scripts\entity_defs\alias.xml文件进行保存的
PropertyDescription:属性类
EntityDef:静态类,加载xml文件保存方法名,根据xml文件名搜索相关模块的python脚本,然后再根据已经保存下来的方法名检查此python脚本有没有保存的方法名;entity_app、dbmgr和ClientApp会调用EntityDef初始化。
PyMemoryStream:调用pickler静态方法将自己的方法注册到Pickler相关的python
Pickler:静态类,存有pickler方法系列注册到python的模块,
EntityApp = ServerApp:
isInitialize:调用了installPyScript,installPyModules,installEntityDef
installPyScript:将python空间目录路径设置成组件通用和当前组件的脚本路径,getscript().install()添加、初始化脚本基础模块kbengine,
BaseApp和CellApp在installPyModules有调用EntityApp::installPyModules
BaseApp = ServerApp:在installPyModules时将一些关于entity方法注册到相对应的python模块,例如:
在installAllScriptModules时根据entities.xml里的每个字段在python目录空间(也就是设置的脚本目录)里导入跟这个字段同名的模块,例如<Account hasClient="true"></Account>,那么就把Account.py导入进来。
Base = ScrptObject:代表着一个实体entity
\kbe\res:是系统资源目录server/kbengine_defs.xml
\assets\res\server:是用户资源目录server/kbengine.xml
\assets\scripts:是用户脚本目录entities.xml
FixedMessages:姑且叫做固定消息,程序启动时会加载kbe\res\server\messages_fixed.xml,然后保存在自己的_infomap,每次程序开始时在给MessageHandlers添加app里已经定义好的消息时会判断是否是固定消息的,如果是那么会赋值消息ID,而且消息里的消息ID是不能重复的; 继承MessageHandle类,然后加入MessageHandles类的其中一个成员。
MessageHandlers:消息操作集类,
MessageHandler:消息操作类,所有的C++层的网络消息操作句柄都继承于这个类
对C++层的网络消息定义不懂,可以仔细看看interface_defs和loginapp_interface_macros文件,因为这两个文件配合利用宏定义而巧妙的产生各个消息类的声明及定义,消息操作集类的声明及定义
划黄线地方是操作消息类型,这里是添加这个类型对象到消息集合,定义及声明的地方在如下
调用消息句柄是在PacketReader::processMessages里调用pMsgHandlers进行查找,操作,如图
\kbe\res\server\kbengine_defs.xml:服务系统默认配置这个最好不好更改,如果要更改配置,更改服务用户配置文件;如果你需要改变引擎设置,请在({assets}/res/server/kbengine.xml)中覆盖kbe/res/server/kbengine_defs.xml的对应参数来修改,这样的好处是不会破坏引擎的默认设置,在你更新引擎时也不会产生冲突,以及在多个逻辑项目时不会影响到其他的项目设置。
\asserts\res\server\kbengine.xml:服务用户配置
任何一个App都会读取/assets/scripts/common;/assets/scripts/data;/assets/scripts/user_type;kbe/res/scripts/common这四个目录,如果是BaseApp/CellApp/DBMgr那么会读取这个
任何一个App都会读取设置python路径:
/assets/scripts/server_common;/assets/scripts/common;assets/scripts/data;/assets/scripts/user_type;/kbe/res/script;/kbe/res/scripts/common;/kbe/res/scripts/common/lib-dynload;/kbe/res/scripts/common/DLLs; /kbe/res/scripts/common/Lib 这些目录
如果是BaseApp/CellApp/DBMgr那么还会加上assets/scripts/server_common;
DB:
DBUtil:有个静态成员变量pThreadPoolMaps_,这个变量是根据xml表配的数据库接口名称为key添加的线程池(默认为default),线程池类是DBThreadPool
DBThreadPool = ThreadPool:保存着DBThread;kbengine_defs.xml中配置的databaseInterface配置多少个就创建多少个。
DBThread = TPThread:TPThread是封装 ThreadPool的其中的一个线程,也就是说ThreadPool有多个TPThread(有多少这个对象就有多少条线程),TPThread的workfunc执行时在处理Task时会调用自身的onProcessTaskStart(Task),processTask(Task),onProcessTaskEnd(Task),所以就会调用到DBThread重写的虚函数;而DBThread在onProcessTaskStart (Task) 时是有开始mysql的事务,onProcessTaskEnd (Task)是有提交事务。而且DBThread在启动轮询处理开始时onStart()会创建DBInterface留做此线程用,onEnd()会删除掉DBInterface
DBInterfaceMysql = DBInterface:是与mysql直接操作的接口类,保存有ip、端口等详细信息;kbengine_defs.xml中配置的databaseInterface配置多少个就创建多少个,配置里一般都有个default
DBUtil:数据库控制单元,创建、初始化、删除DBInterface,
EntityTables:存有多个表(EntityTableMysql);同步到数据库时会为每个EntityTableMysql会增加任务到线程池内
EntityTableMysql:mysql表实体,存有表的属性数据:字段类型,字段名,表名;
Component:App会去主动搜寻需要告知本组件信息的App,如果有别的组件广播到本组件来,那么也会保存下来;Commponent类的process会发送udp消息组广播给machine,如果发送成功并且达到是machine的条件,然后直到没回消息,那么证明machine存在,设置下状态,再次process时会搜索本组件需要连接的,所有需要连接的组件都询问下machine,如果有的话,那么都添加,然后更改状态,下次process时就是连接这些组件(connectComponent);state是0时是寻找machine,state是1时是寻找需要连接的组件,state是2时是连接在state是1时添加的组件;连接需要连接的组件完成后,发送注册组件消息给连接完成的组件,对端组件就会
entity_defs下面的文件的Properties里面的每个字段的Flags值会影响到ScriptDefModule的,也会影响到读取并创建PropertyDescription存在ScriptDefModule的哪个PropertyDescription
需要persistent的,ScriptDefMoudle的persistentPropertyDescr_保存下这个属性
entitiy_defs下面的文件的Implement字段有值的话,那么读取assets\scripts\entity_defs\interface相关字段值的文件
entity_defs下面的每个文件是一个ScriptDefModule(模块),也就是说每个模块都会添加相应的文件里面的属性、方法和相应的Implement的Interface下的文件。调用这个loadAllScriptModules方法进行加载所有python类
FixedDictType = DataType:举例这么一个用户自定义类型;这个类型需要工具,会在alias.xml读取此类型出来,会读取implementedBy这个字段,例如:<implementedBy>AVATAR_DATA . inst</implementedBy>,会根据 " ."分割字符串,分割出AVATAR_DATA、inst,然后读取AVATAR_DATA模块(也就是py文件),然后从模块中返回inst对象,py文件中例:inst = AVATAR_DATA_PICKLER(),会从模块中返回一些方法保存在此类型类中,此类中成员moduleName是这implementedBy字段的值,即是AVATAR_DATA . inst
SyncEntityStreamTemplateHandler:是属于baseapp的类;主要是同步持久化数据,会将dbAccountEntityScriptType相对应的ScriptDefModule模块的属性默认数据转换成python对象(在PropertyDescription::addPersistentToStream这里面)
Component组件如果不是guconsole(也就是说是console)的话,那么相连的组件的uid必须是一样的
BundleBroadcast:广播发送包裹,是udp协议栈,构造时会绑定监听端口,epListen是用来监听的socket,epBroastcast是用来广播的socket
CellApp、BaseApp组件与DBMgr连接并注册到DBMgr后,DBMgr会将需注册的组件的信息(也就是前面的CellApp、BaseApp)告诉其他已经和DBMgr连接的CellApp、BaseApp组件,被告知的与CellApp或BaseApp同类型的组件会注册最开始的CellApp或BaseApp的组件并连接
也就是说,所有的BaseApp和CellApp是有互联的
Components::groupOrderid或者 globalOrderid:App组内的排序ID,例如给CellApp组内的排序ID,BaseApp组内的排序ID,LoginApp组内的排序ID,这些ID的分配是在App连接DBMgr时,由DBMgr调用ServerApp::onRegisterNewApp(),onRegisterNewApp再调用addComponent方法,addComponent分配的ID,再由DBMgr的SyncAppDatasHandler类调用process时同步这些ID给相关的App,会发送onDbmgrInitCompleted消息给相关的APP,相关的App收到消息后,会设置id,(BaseApp会多做一步,设置在App所在python环境中),会调用python层的onInit,还会在创建InitProgressHandler类,InitProgressHandler是定时器类,会调用process做些初始化的工作,并且会发送消息给相关的AppMgr,AppMgr进行相关的工作,InitProgressHandler的process会一直循环执行到App的初始化完成度大于1,可以看C++层调用python层的onReadyForLogin方法
AppMgr的initProgress:(完成度)是指App类型的所有App初始化完成除以所有App的值,完成初始化的App 50 / 总共App 100 = 0.5
App也有自己的完成度
InitProgressHandler:是在BaseApp上创建并调用的话,如果是第一个BaseApp,会向DBMgr获取所有模块的一些实体
在做上面的工作的同时,DBMgr充当着GlobalDataServer的角色,所以会将GlobalDataServer相关的数据发送给新的App
组件的uid是根据环境变量的uid设置的
CellApp同上图
BaseApp和
BaseApp和BaseAppMgr连接后,BaseApp有个onUpdateLoad方法在同步BaseApp信息给BaseAppMgr
FixedDictType:这个类型有个成员moduleName,保存alias.xml中是FIXED_DICT
FixedDict类型修改值是没有调用entity重载的set,所以没有同步属性
alias.xml:这个文件不仅仅是设置别名,如果别名的类型是复杂类型,那么还会再延伸出属性,具有与Account.def等文件的Property字段的同样功能
遇到是Array类型,那么增加表,表名取父表+父字段名字+当前字段名
Base启动时会询问
只要有python类继承base或cell,都会有__py_onTimer添加到模块中
每个ServerDefModule都有保存着所代表的python对象类型(PyTypeObject*scriptType_),例如Account,那就是Account类型的对象,CreateObject函数就是创建所代表类型的python对象,这个对象的成员是根据Account.xml的属性来创建的,然后在EntityApp<>中的onCreateEntity()里用new去重新构造python对象,例如new(pyEntity)E(eid,sm),这里E是Base或Cell,因为保存着所代表的python对象类型都是是继承Base或Cell,不明白为什么都是继承Base或Cell,请看对ServerDefModule的解释 nitializeEntity
kbe\src\lib\entitydef\entity_macro.h:定义entity与python C++扩展的宏
kbe\src\server\baseapp\base_interface_macros.h:定义base与python通信的消息参数宏(通信指的其实就是调用函数)
继承Base的python类的cellData是在Base构造时创建的cellData,详情请看Base构造
在Base目录下的Space.py的__init__初始化时调用Base的CreateInNewSpace
CellApp的Entity是Entity类
BaseApp的Entity是Base类
Entity类和Base类在初始化时会调用CreateNamespace,然后在调用子类python类的__init__
基本上python模块里的KBEngine模块是存在EntityApp的getScript().getModule()
Kbe初始化好后,BaseApp调用了kbengine.py里的onBaseAppReady,如果是第一个的话,那么调用了kbengine.createBaseLoaclly("Spaces", {}),与kbengine.createBaseLoaclly绑定的C++函数Baseapp::__py_createBase调用了Baseapp::getSingleton().createEntity,创建了一个继承与Base的python类Spaces,Spaces是整个kbe创建地图的起点,看spaces的__init__方法时你就懂了,注意Spaces在初始化时会也用C++提供的定时器注册定时器。
在CellApp创建python类Space的Entity时,Space的__init__会调用addSpaceGeometryMapping创建地图
玩家在网关登录(也就是BaseApp),BaseApp会调用idClient.alloc()分配一个EntityID给玩家
Base的initializeScript就是调用的子python类的__init__
当BaseApp要创建CellApp,也就是调用createCellEntity,就会在CellApp创建Cell,调用onCreateCellEntityFromBaseapp,然后创建完会发送onEntityGetCell消息给BaseApp,然后BaseApp会调用python类的onGetCell;而在最开始创建Space调用CreateInNewSpace会发送消息给CellAppMgr,CellAppMgr再发送给CellApp创建NewSpace并创建Entity,然后会给BaseApp发送onEntityGetCell消息;
总结(猜测):只要是BaseApp通知CellApp的消息中会导致创建Entity都会给BaseApp发送onEntityGetCell消息,在Base::onGetCell() 给Base创建EntityMailBox给成员ceilMailBox,也就是说所有的Base或Proxy最终都会创建mailBox只不过类型是多选项的,
Base:: onEntityGetCell ():在这个方法里,会设置base的spaceID,在initClientCellPropertys会将spaceID发送给客户端
CellApp的Entity会保存有baseMailBox、clientMailBox,baseMailBox会记住在BaseApp的ID
BaseApp的Base或Proxy会保存有cellMailBox、clientMailBox,cellMailBox会记住在CellApp上所在的spaceID和CellApp的ID, Baseapp::createCellEntity(EntityMailboxAbstract*createToCellMailbox, Base* base):将base创建到这个createToCellMailbox相对应 CellApp上的Enitity
KBEngine.createBaseAnywhere("SpawnPoint",{"spawnEntityNO" : datas[0], \
"position" : datas[1], \
"direction" : datas[2], \
"modelScale" : datas[3], \
"createToCell" : self.cell}
-> BaseApp:: createBaseAnywhere–>BaseAppMgr –> BaseApp:: onCreateBaseAnywhere–>BaseApp::CreateEntity –> SpacePoint::__init__ ->self.createCellEntity(self.createToCell)(这里的createToCell与最开始的createToCell是一样的)-> Base::createCellEntity() -> Baseapp::getSingleton().createCellEntity (EntityMailboxAbstract* createToCellMailbox,Base* base)(这里的createToCellMailbox是最开始的createToCell, base是前面的SpawnPoint类) -> Cellapp::onCreateCellEntityFromBaseapp ->告诉BaseApp:: onEntityGetCell和创建Entity
Client如何远程调用服务端entity的方法(其实就是client发送消息给服务端,服务端根据方法ID调用实体的相关方法而已):Client发送给Base消息onRemoteMethodCall,BaseApp根据发来的EntityID(ProxyID/BaseID),因为是调用base的方法,发来的entityID必须是客户端的proxyID一致,也就是不能调用别的proxy,只能调用客户端的代理base/proxy,调用Base的onRemoteMethodCall,根据消息的utype查找出相应的BaseMethon,调用相关的python方法
Server如何远程调用客户端的entity方法(从服务端的角度):server是通过mailbox与client交互的,我也不知道为什么非要弄个mailbox,mailbox是存在entity(Base/Proxy)上的,server怎么保证通过mailbox与client交互呢?是通过python层级对client操作,都要用sefl.client这个行为操作,例如:self.client.onReqAvatarList(self.characters), client指的就是mailbox,client调用onReqAvatarList时,调用了onScriptGetAttribute()(这是在写mailbox类时重写的方法),在这个方法里面,查找了这个方法名(onReqAvatarList),将查找出来的方法进行创建一个能操作的python类createRemoteMethod(pMethodDescription),再将创建的类对象返回给python,python执行到onReqAvatarList()的()时调用了前面创建的类的方法tp_call(),然后Base*pEntity=Baseapp::getSingleton().findEntity(mailbox->id()); methodDescription->checkArgs(args)进行检查,检查参数是否一致,就这个self.characters,检查会true的话,就创建一个mail:mailbox->newMail((*pBundle));,然后再执行methodDescription->addToStream(mstream,args);这行代码是将methodDescription里的方法ID和传进去的args进行序列化到mstream,然后再(*pBundle).append(mstream->data(), (int)mstream->wpos());,进行send给client
数据库的数据保存:特殊处理,数据库保存方向和位置,只要模块有cell属性,sm->hasCell(),那么都会有position和direction
BaseApp在创建Avatar时Avatar类仅仅只是辅助创建下,write db data后就destroy
其实不管是BaseApp下的Base/Proxy的CellMailBox还是对应在CellApp下的Entity的 ID都是一样,也就是说只要BaseApp才生产entityID,而且不管是Entity还是在Base/Proxy还是MailBox ID必须是一致的,同个entity类型
CellApp::Witness类:监视拥有者玩家内的视野范围的玩家(AOI),负责同步视野范围其他玩家的客户端数据及位置信息。有三种状态,一(将要进入视野范围内):其他玩家进入拥有者玩家的视野范围,那么将其他玩家在拥有者玩家的Witness里的引用的状态更改成普通状态,并同步其他玩家的客户端信息与位置信息给拥有者玩家,通知拥有者玩家有其他玩家进入视野范围;二(将要离开视1野范围内):其他玩家离开拥有者玩家的视野范围,那么通知拥有者玩家有其他玩家要离开,并删除其他玩家在拥有者玩家的Witness里的引用;三(普通状态,还在视野范围内),同步还在视野范围的其他玩家的volatile信息给拥有者玩家。一直在update
EntityCoordinateNode = CoordinateNode:
AOITrigger = RangeTrigger:
一个玩家都有一个witness成员,和一个EntityCoordinateNode;witness类又包含着AOITrigger类,EntityCoordinateNode又会影响到AOITrigger,
AOITrigger
也就是EntityCoordinateNode的数值一改变
CoordinateSystem:update(node)函数功能,更新node在x链,y链,z链上的位置,使其符合一定顺序位置
每次服务端收到客户端的同步位置,都会调用node的update()
AllClients:会调用onScriptGetAttribute方法,然后会newClientsRemoteEntityMethod(pMethodDescription, otherClients_, id_)
ClientsRemoteEntityMethod:这个类主要是做远程远程方法给entityAOI内的玩家,例如self.otherClients.onJump(),otherClients是AllClients类型,python层调用onJump(),那么就C++层otherClients就调用了onScriptGetAttribute();具体操作类似Server如何远程调用客户端的entity方法;只不过这个是AOI内玩家都远程调用一遍
Kbe的tools:
Xls的表头符号$代表着这个字段是有映射值,需要替换
!代表着是key
.代表着是不为空
数据库保存ARRAY类型属性的方法就是新建一张子表,表名结构:父表名_属性字段名_被ARRAY定义类型的值,例如:
Alias.xml:
<AVATAR_INFO_LIST> FIXED_DICT
<implementedBy>AVATAR_INFO.avatar_info_list_inst</implementedBy>
<Properties>
<values>
<Type> ARRAY <of> AVATAR_INFO </of>
</Type>
</values>
</Properties>
</AVATAR_INFO_LIST>
Account.def
<root>
<Properties>
<characters>
<Type> AVATAR_INFOS_LIST </Type>
<Flags> BASE </Flags>
<Default> </Default>
<Persistent> true </Persistent>
</characters>
<Properties>
例如这个属性【characters】保存到数据库,那么首先已经有Account表了,这张表咋们就不说了,【characters】的类型是AVATAR_INFOS_LIST,这个类型固定字典的保存有values这个值,但是这个值是ARRAY类型,所以此时会创建一张子表以对应这个ARRAY定义的值,表名是父表名Account_属性名characters_被ARRAY定义类型的值values,【Account_characters_values】;将ARRAY类型看作一个张表的数据,ARRAY是数组,一张表的多个数据也是数组,所以kbe属性里凡是遇到ARRAY类型的,都会另创建一张表;由于【Account_characters_values】是子表,所以在这个表内会创建一个字段parentID,这个parentID对应父表的ID。
BaseApp持久化数据到DBMgr:一个entity(Base/Proxy),调用writeToDB()函数,如果entity(Base/Proxy)有cell那么发条消息给CellApp,获取下Cell上的数据,再返回到BaseApp给entity(Base/Proxy),然后entity(Base/Proxy)再调用onCellWriteToDBCompleted()函数,函数里获取entity(Base/Proxy)需要持久化的数据发送给DBMgr;如果此Entity没有dbid,那么DBMgr认为是insert类型,反之,update类型;如果是insert类型 且 Entity有个属性是ARRAY类型,那么这个会在Content的optable循环插入需要insert或update的数据。
每个entity的数据不会因为被改变而实时改变数据库,但是有个定时存档,最好还是改变数据了,手动存档
Entity数据写入数据库是将所有需要持久化的数据,也就是只要entity脏了,那么不管它的属性有没有脏
只要entity的某个需要持久化的属性改变了,那么该entity都会置脏
定时存档:Archiver::archive(Base base), 调用base.writeToDB(NULL, NULL, NULL)
定时备份:Backuper::backup()
其实这两个的区别就是backup是将entity置脏,而writetodb是将数据写入db,如果没有entity没有置脏,那么不会写入db,可以去追溯entity的两个成员变量:shouldAutoArchive_、shouldAutoBackup_
Cellapp:非玩家entity都是不会同步entity所在的地图信息
帐号登录,在返回DB信息后,创建Base时会添加个clientMailBox,然后会同步些客户端数据给客户端,这时还没有cell
在创建Base之后,创建cell后会返回onEntityGetCell,base会根据如果有clientmailbox,那么会调用initClientCellPropertys这个方法,同步base本身的celldata属性给客户端
CellApp上面的cell也有clientMailBox,但是cell上的clientmailbox是MAILBOX_TYPE_CLIENT_VIA_BASE这个类型的,也就是要通过base转发到client
只有hasClient(是否有clientChannal)才会有witness观察者对象
hasClient在kbengine引擎有多次出现,有不同出现地方不同的含义:1.在ServerDefModule里的hasClient是指这个模块有没有需要同步给客户端的数据,有这个类型ENTITY_CLIENT_DATA_FLAGS;2.是否有clientChannal,出现了hasClient,可以顺着往前推,会有看到判断是否有clientChannal
在cell,有两种AOI trigger,一种是TrapTrigger,这种保存着一个控制器pProximityController_,一种是AOITrigger,这种保存着一个withness,
两个Trigger都继承RangeTrigger,每个trigger 都会有个节点RangeTriggerNode插入在坐标系统里,这个RangeTriggerNode节点是用来比较附近的非自己的EntityCoordinateNode,以此来判断自己是否在其他节点的AOI范围内。也就是自己的EnittyCoordinateNode是否在其他RangeTriggerNode的AOI范围内,感兴趣的话,会调用RangerTriggerNode的Trigger来调用
RangeTrigger:有两个RangeTriggerNode(positiveBoundary, nagativeBoundary),都会将RangeTriggerNode添加到原点的entityNode,这两个node代表着
EntityCoordinateNode:是实体的node,只watcherNode
gameUpdateHertz:是一秒内多少tick
玩家entity会通过是否是client添加witness对象(有witness对象,那么就添加AOI对象,有AOI对象就会在coordinate系统添加范围节点),而npc/monster entity没有,只能通过脚本层,调用entity::addProximity来添加范围触发器,而范围触发器ProximityController在构造时会添加TrapTrigger,并且调用TrapTrigger::install,而TrapTrigger是继承RangeTrigger,RangeTrigger::install()的功能是安装两个范围节点到coordinateSystem,添加范围触发器ProximityController到pControllers->add(p),也就是说entity里有个witness对象,也有pControllers用来存放多个触发器;
coordiante的update里调用了onNodePass系列,这个node是RangeTriggerNode类型,那么是调用了RangeTriggerNode::onNodePass系列,RangeTriggerNode::onNodePass系列又调用了RangeTrigger::onNodePass系列,条件允许调用了this->onEnter(pNode),而这个RangeTrigger的子类型是TrapTrigger,则调用了TrapTrigger::onEnter,子类型是AOITrigger,则调用了AOITrigger::onEnter;TrapTrigger::onEnter调用了ProximityController::onEnter,ProximityController::onEnter调用了Entity::onEnterTrap,AOITrigger::onEnter调用了Witness::onEnterAOI
GlobalDataClient:是继承于kbe自定义的map类型(姑且称为DIYmap),这个DIYmap类型初始化对象设置成重写好的一些python map的API:SCRIPT_INIT(Map, 0, &Map::mappingSequenceMethods, &Map::mappingMethods, 0, 0),mappingMethods
/** python map操作所需要的方法表 */
PyMappingMethodsMap::mappingMethods =
{
(lenfunc)Map::mp_length, // mp_length
(binaryfunc)Map::mp_subscript, // mp_subscript 下标取值
(objobjargproc)Map::mp_ass_subscript // mp_ass_subscript 下标 赋值
};
在mp_ass_subscript调用了一个虚函数onDataChanged;在GlobalDataClient类也重写了onDataChanged,所以在脚本层改变了GlobalDataClient类型的数据,就会调用onDataChanged,onDataChanged调用pickler::pickle(value, 0),此时value是一个entity(base/proxy),那么会回调entity(base/proxy)的__reduce_ex__,__reduce_ex__方法返回了一个tuple类型,参数(mailbox的unpickle方法, tuple类型参数(一些参数要传给unpickle的参数,这里面有个参数非常重要,ENTITY_MAILBOX_TYPE这个类型的参数, 因为要让别的app获得这个mailbox,所以要设置好这个mailbox的类型)),pickler方法结束后,发送onBroadcastGlobalDataChanged消息给DBMgr;
DBMgr接收到后,DBMgr将数据发送给相关全局数据类型的组件,并保存数据在DBMgr
并且每个App类型也可以有各自的AppData,可以参考BaseApp或者CellApp C++层的onInstallPyModules方法,例如只管理Base的全局BaseApp上,看下图,可以直接在python层,这样子调用KBEngine.baseAppData
还有一个是所有的继承于EntityApp的App都可以获取到数据,看下图,可以直接在python层,这样子调用KBEngine.globalData,
因为有保存mailbox并且cellApp和baseApp之间都是有互通的,所以可以通过保存在GlobalData的相关模块的mailbox进行远程调用方法
baseAppData和cellAppData同理
MAILBOX_TYPE_CELL = 0, // mailbox的类型是cell
MAILBOX_TYPE_BASE = 1, // mailbox的类型是base
MAILBOX_TYPE_CLIENT = 2,
MAILBOX_TYPE_CELL_VIA_BASE = 3, // mailbox的类型是 通过base的cell 也就是说调用cell这个mailbox上的方法,会先将消息和参数发给属于有这个base实体的baseapp上,再由目标baseapp将消息和参数发送给对应的cell
MAILBOX_TYPE_BASE_VIA_CELL = 4, // 同上理
MAILBOX_TYPE_CLIENT_VIA_CELL = 5,
MAILBOX_TYPE_CLIENT_VIA_BASE= 6,
GhostManager:是CellApp的一个类,作用1:在teleport的时候real的entity切换成ghost时,任何发给Entity消息都存放在这个类
BaseMessagesForwardHandler:主要是用于保存发送给关于Base的Cell的消息,一般要创建这个类时,表示CellApp上的Entity在进行Teleport操作
ghostCell 和 realCell:这两个存在Entity类里,主要针对Entity进行Teleport操作时的一些辅助标志;Entity在操作Teleport时,会通知相关的base调用onMigrationCellappStart方法增加BaseMessagesForwardHandler类;然后设置entity的realCell(ComponentID)为要migration的Cell的ComponentID,然后调用Entity的addToStream方法,发送Entity的数据(包括witness、controller、Timer等)到目标Cell,
在Cell上被观察者,一改变属性,那么会将自身属性同步给观察者,会在Entity类onDefDataChanged里对witnesses进行
createInNewSpace:创建了一个 调用这个接口的python类的类型的对象
entity的destroy:python层的destroy调用的是C++层的__py_pyDestroyEntity,这个方法调用的了entity的C++层的onDestroyEntity函数,和entity的C++层的destroyEntity(),
这个C++层的onDestroyEntity是在__py_pyDestroyEntity传进来的参数,可以传两个参数(deleteFromDB,writeToDB),就第一个deleteFromDB意思就是要删除这个entity在db的数据,
判断了是否要与db数据关联与这个deleteFromDB的标志,如果都为True的话,那么通知dbmgr,removeEntity;
destroyEntity再调用的是APP的destroyEntity,然后在APP的destroyEntity里的entity列表会删除掉此entity,然后再调用entity的C++层的destroy,然后entity的destroy方法其中调用了entity的onDestroy,
并且调用了取消了所有定时器,然后entity的onDestroy里,如果callScript是为True的话,那么回调python层的onDestroy,然后执行了跟实体类型相关的销毁,有调用python层的onDestroy;
cellapp的entity销毁看entity.cpp entity_macro.h entity_app.h;baseapp的base/proxy销毁看base.cpp entity_macro.h entity_app.h;
看demo里,是包装一个方法destroySelf,没有直接调用python层的destroy,而是在里面进行判断了是否有cell,先调用C++提供的python的destroyCellEntity,当cell被销毁后会调用base的python层的onLoseCell,然后再调用destroySelf,这次才直接调用C++提供给python层的destroy。Demo这样写是为了更安全的destroy
demo的cellapp的entity的离开场景设计:在avatar的python层的teleport的onDestroy方法中调用base的space的logoutSpace方法,
logoutSpace方法又调用了cell上的entity的onLeave
报错Components::findComponents: find {}({})...,原因是在Components::findComponents方法里请求machine找某组件时,没回应,循环多次而引起的报错
报错BundleBroadcast::receive: failed! It can be caused by the firewall, the broadcastaddr, etc.""Maybe broadcastaddr is not a LAN ADDR, or the Machine process is not running. 原因是组播后,调用BundleBroadcast::receive时select超时多次而引起的报错
ScriptTimers类:存在于Entity上成员变量,主要是保存无数个Entity在py层创建的定时器;addTimer成员函数最终还是getApp()->addTimer
EntityScriptTimerHandler类继承于TimerHandler:是在Entity addTimer操作时,new出来时将entity的this存进去(详细请查看entity_macro.h的pyAddTimer,python层的timer可以循环触发,有repeat参数),随着addTimer时存放在g_pApp->timers().add里面,这样子app在processTimer时就可以调用这个对象的handler里再调用entity的onTimer方法,这里定时器的时间是按照自定义帧为单位的
addTimer:第一个参数是第一次执行距离现在的时间,第二个参数是执行的间隔时间(除第一次外),如果是0那么代表只执行一次
在这里代码的意思是如果参数repeatOffset也就是第二个参数,计算出来没达到一帧,那么就算是一帧,因为服务端是按照一秒几帧进行循环的,不能比一帧还小的单位,假如一秒是执行10帧,第二个参数是0.05秒(50毫秒),那么0.05 * 10等于0.5,0.5没达到一帧,那么算成一帧
g_kbetime:是按照自定义帧数来计算的,默认是一秒10帧的话,那么在1秒内会update10次,每update一次,g_kbetime就会增加一次,想要获取当前是g_kbetime总共经历update多少秒,可以g_kbetime除以自定义每秒多少帧,按照默认,g_kbetime / 10,得出经历了多少秒,kbe提供了一个方法gameTimeInSeconds。这个是用来给逻辑timer进行判断
与timer相关的:
isOnGroud:这个就是服务器发过来的位置坐标可能不是在地面上的。这时直接去渲染的话人物就会离地,但是通过这个标志位可以避勉这种错误的渲染,自己在客户端处理一下不让渲染出来的人物飞起来。
onLogOnAttempt:这个是dbmgr有在线信息,所以在BaseApp上调用的一个方法
登录流程:client连接loginApp,然后loginApp向dbmgr查询accountinfos,并且查询是否已有在线记录,dbmgr回loginApp查询结果信息,loginApp根据结果将信息发送给baseAppMgr,baseAppMgr就能发送相关信息给BaseApp,如果有在线信息,那么表示已存在,否则向dbmgr查询account的entity信息
initClientBasePropertys:(method)Proxy初始化client的信息,并发送onUpdatePropertys
entity_def的interfaces是的继承不能是多个相同的,例如npcobject继承gameobject,但是monster继承了npcobject,却又继承了gameobject
PyObject_CallFunction:是调用一个函数对象
PyObject_CallObject:是调用一个python类的__call__
Python的__getattr__与__getattribute__的区别:当访问某个实例属性时, getattribute会被无条件调用,如未实现自己的getattr方法,会抛出AttributeError提示找不到这个属性,如果自定义了自己getattr方法的话,方法会在这种找不到属性的情况下被调用,
日志管理:kbe的日志管理,在进行写入文件是用了第三方库log4,各个服未进行连接之前是在写入各自服的日志,与Log服连接之后,服将日志数据发送到log服,由log服管理
--cid:(必须设置)
类型为uint64,全名component id,每个进程都有一个唯一ID,唯一ID在合适的时候用于区分他们之间的身份。
--gus:(可选设置)
类型为uint16,全名genUUID64 sections,这个值会被引擎KBEngine.genUUID64函数用到,设置为不同的值genUUID64将在
不同的区间产生唯一uuid。
这个值如果能在多个服务组进程之间保持唯一性,那么在合服的时候能够带来一定的便利性。
例如:游戏服A和游戏服B中的物品在数据库中存储的ID都使用genUUID64生成,那么在合服的时候能够直接向一张表中合并数据。
(注意:如果gus超过65535或者小于等于0,genUUID64将只能保证当前进程唯一)
定义文件的格式
<root>
//该实体的父类def
//这个标签只在Entity.def中有效,如果本身就是一个接口def则该标签被忽略
<Parent> Avatar </Parent>
//易变属性同步控制
<Volatile>
//这样设置则总是同步到客户端
<position/>
//没有显式的设置则总是同步到客户端
<!-- <yaw/> -->
//设置为0则不同步到客户端
<pitch> 0 </pitch>
//距离10米及以内同步到客户端
<roll> 10 </roll>
</Volatile>
//注册接口def,类似于C#中的接口
//这个标签只在Entity.def中有效,如果本身就是一个接口def则该标签被忽略
<Implements>
//所有的接口def必须写在entity_defs/interfaces中
<Interface> GameObject </Interface>
</Implements>
<Properties>
//属性名称
<accountName>
//属性类型
<Type> UNICODE </Type>
// (可选)
//属性的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
//开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
//自身规则所生成的协议ID,这个ID必须所有def文件中唯一
<Utype> 1000 </Utype>
//属性的作用域 (参考下方:属性作用域章节)
<Flags> BASE </Flags>
// (可选)
//是否存储到数据库
<Persistent> true </Persistent>
// (可选)
//存储到数据库中的最大长度
<DatabaseLength> 100 </DatabaseLength>
// (可选,不清楚最好不要设置)
//默认值
<Default> kbengine </Default>
// (可选)
//数据库索引,支持UNIQUE与INDEX
<Index> UNIQUE </Index>
</accountName>
...
...
</Properties>
<ClientMethods>
//客户端暴露的远程方法名称
<onReqAvatarList>
//远程方法的参数
<Arg> AVATAR_INFOS_LIST </Arg>
// (可选)
//方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
//开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
//自身规则所生成的协议ID,这个ID必须所有def文件中唯一
<Utype> 1001 </Utype>
</onReqAvatarList>
...
...
</ClientMethods>
<BaseMethods>
// Baseapp暴露的远程方法名称
<reqAvatarList>
// (可选)
//定义了此标记则允许客户端调用,否则仅服务端内部暴露
<Exposed/>
// (可选)
//方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
//开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
//自身规则所生成的协议ID,这个ID必须所有def文件中唯一
<Utype> 1002 </Utype>
</reqAvatarList>
...
...
</BaseMethods>
<CellMethods>
// Cellapp暴露的远程方法名称
<hello>
// (可选)
//定义了此标记则允许客户端调用,否则仅服务端内部暴露
<Exposed/>
// (可选)
//方法的自定义协议ID,如果客户端不使用kbe配套的SDK来开发,客户端需要开发跟kbe对接的协议,
//开发者可以定义属性的ID便于识别,c++协议层使用一个uint16来描述,如果不定义ID则引擎会使用
//自身规则所生成的协议ID,这个ID必须所有def文件中唯一
<Utype> 1003 </Utype>
</hello>
</CellMethods>
</root>
属性作用域
如果一个属性的作用域分为多个部分,那么在实体的对应部分都存在该属性。
存在于实体多个部分的属性只能从属性的源头进行修改,其他部分会得到同步。
请参考如下表:(S与SC或者C都代表属性包含这个部分,不同的是S代表属性的源头,C代表数据由源头同步,SC代表实体的real部分才是源头,ghosts部分也是被同步过去的)
[类型] [ClientEntity] [BaseEntity] [CellEntity]
BASE - S -
BASE_AND_CLIENT C S -
CELL_PRIVATE - - S
CELL_PUBLIC - - SC
CELL_PUBLIC_AND_OWN C - SC
ALL_CLIENTS C(All Clients) - SC
OWN_CLIENT C - S
OTHER_CLIENTS C(Other Clients) - SC
Python层的KBEngine.time()是调用的App的time(),例如BaseApp,而BaseApp的time()是放回g_kbetime
KBEngine逻辑也可以组合。
继承的只是interface, demo没有很严格的实现为interface。
你可以将要暴露的网络接口定义在interface中,你可以实现组件挂在interface脚本下面,这样继承了interface实际上他包含了一个组件就是组合了
在BaseApp上的python entity都是继承Base类的,在CellApp上的python entity都是继承Entity类,如果BaseApp的entity的属性标志是设置成BaseAndClient,才会每次更改属性会实时同步属性给客户端,可以看Base类的onDefDataChange函数,CellApp上就看Entity类的onDefDataChange函数
kbengine.xml里的externalAddress和baseapp的externalAddress、loginapp的externalAddress是强制指定外部ip,kbengine是这么解释的:
强制指定外部IP地址或者域名,在某些网络环境下,可能会使用端口映射的方式来访问局域网内部的KBE服务器,那么KBE在当前
的机器上获得的外部地址是局域网地址,此时某些功能将会不正常。例如:账号激活邮件中给出的回调地址,登录baseapp。
注意:服务端并不会检查这个地址的可用性,因为无法检查。
如果这个被指定后,引擎只会发会这个地址
FixedDictType:这个类型在addStreamEx时会检查字典里的key是不是跟设计的也就是alias.xml里设计的key一样,如果不是一样,会报这类错误:"FixedDictType::addToStreamEx: {} not found key[{}]. keyNames[{}]\n
图说明了在python层只要改变了position那么会调用pySetPosition,而pySetPosition是会调用onDefDataChanged,而在这之前会调用Network::FixedMessages::MSGInfo* msgInfo = Network::FixedMessages::getSingleton().isFixed("Property::position");,因为要给这个属性定义ID,所以会发现messages_fixed.xml还可以给属性进行固定ID,不过这个只是给特别的属性进行固定ID而已,而且这个固定会固定所有的python类的指定属性的ID,就例如这个position属性,而且这里巧妙的调用了onDefDataChanged
Network::FixedMessages::MSGInfo* msgInfo =
Network::FixedMessages::getSingleton().isFixed("Property::spaceID")
C++层会调用python层的onEntitiesEnabled时是这个实体已经成为client的代理
FixedDictType如果有ImplementedBy了的话,那么C++层的createFromStream即调用了python层的createFromDict方法,这个方法是数据库数据转换成内存数据调用的;C++层的addToStream即调用了python层的asDict方法,这个方法是内存数据要换成数据库数据或者网络流数据时调用的,也就是说客户端会得到的数据都是asDict之后的,注意是有ImplementedBy了后
FixedDictType是不传输里面的key的,详情可以看FixedDictType::addToStreamEx这个方法,只传里面的key相对应的值
在alias.xml里定义的类型别名,根本类型如果是FixedType,那么kbengine C++底层是实现了一个python类型(FixedType),然后在实例化时调用了,也就是从数据到对象,调用了createFromDict(obj),然后再调用isSameType,判断是否是同类型。
不能在createFromDict里调用deepcopy,因为deepcopy会把源对象的所有信息,包括源对象保存的类型信息拷贝到目标对象,因为createFromDict时的obj对象还是FixedType。
Alias.xml里面的类型是不能初始化的
FixedDictType:createFromDict时 是最底层级最先createFromDict,然后才一级级往上createFromDict,而addstream时是最高层级先asDict再往下一层级asDict,这里的层级是指包含的属性,可以参考datatype.cpp的FixedDictType的createFromStream和addToStream及FixedDict.cpp的initialize
KBEngine的BaseApp的base和proxy是有区别的,proxy继承于base,但是proxy类有与客户端的mailbox绑定的功能,也就是某个proxy object与客户端建立绑定,proxy就可以利用self.client直接与客户端通信
如果要接平台接口的话,kbengine有提供Interfaces的一条进程/App在进行管理,bigworld是没有的,玩家进行账号创建或者登录时,DBMgr都会发送给Interfaces消息,
登陆时发送的登陆消息,详情请看dbmgr/interfaces_handler.cpp的279行 createAccount方法:
凡是将Entity化的都会使用这个宏 ENTITY_HEADER,例如Base
ENTITY_HEADER这个宏声明及定义了很多entity的相关信息,有entityID之类的信息
凡是将python子类化都会使用这个宏 BASE_SCRIPT_HREADER,例如Base
KBEngine有提供了断线重连,BaseApp提供了方法给客户端断线重连,需要懂的知识点
与client交互的Proxy类型,当这个实体与客户端建立实体联系后,Proxy会产生一个key,存在在Proxy的成员rnnUID,这个rnnUUID是方便断线重连时进行校验,重连后会再产生key
方法如下
会校验key(就是rnnUUID)
详细请看baseapp.cpp的reLoginBaseapp函数,在2700多行那
在启动服务器时就会就会读取alias.xml,然后一一创建类型,一一调用initialize,例如是dict类型的话,
然后读取数据结构类型里的属性类型,字段解析创建字段,最终调用addDataType加入到数据类型集
Witness类:
aoiEntities:观察到的entity
witnesses_:保存着所有观察到我的entity
witnesses_count_:观察到的entitycount
客户端连接服务端,都会请求importClientMessage 和importClientEntityDef
importClientMessage是连接loginapp和baseapp都会请求,importClientEntityDef只有在连接baseapp之后才会请求
kbe底层的Space是维护了一个coordinateSystem_,这是一个坐标系统,进入到这个space的entity都会加入到这个坐标系统里面,这个坐标系统会维护加入到这个坐标系统里面的entity,space->addEntityAndEnterWorld(pEntity);
当entity首次进入cell里,如果有客户端的话,那么会设置一个观察者 witness,会调用setWitness函数,而setWitness函数里会调用attach函数,attach函数会调用onAttach函数,onAttach函数会通知客户端onEntityEnterWorld,而当entity进行teleport的时候,那么传送到目的地时,entity不会调用setWitness,而是调用entity的createWitnessFromStream,然后这个方法会调用witness的createFromStream,而在reqTeleportToCellApp函数里,是在createFromStream后,再调用onEnterWorld,而函数里面会判断如果有witness时,会发送onEntityEnterSpace的方法,也就是首次登录进场景是发送onEntityEnterWorld的方法,会调用onGetWitness,teleport进入场景的是发送onEntityEnterSpace的方法,会调用Space类的addEntityAndEnterWorld函数,不会调用onGetWitness
总结:也就是当A玩家拥有witness时会收到服务端发送的onEntityEnterWotld,并且会收到服务端发送的onEntityEnterSpace,当B玩家登录或者传送时,在A玩家的视野里,A玩家都会收到服务端发送的onEntityEnterWorld,当A玩家服务端调用teleport时,A玩家客户端会收到服务端发送来的onEntityEnterSpace,当A玩家离线时也就是不拥有witness时收到服务端发送的onEntityLeaveWorld
客户端通过调用cellCall调用cell entity的接口,onRemoteCallCellMethodFromClient