🪵 什么是Entity
什么是Entity?
在之前的章节中一直提到Entity,很多开发者会产生疑问,那什么是Entity?我们可以简单的理解为:Entity是引擎开发中的基础对象,中文称之为实体。一个角色可以是一个Entity,一个账号对象可以是一个Entity,一个场景对象可以是一个Entity,根据你的业务需求来设计即可。
下面我们会围绕这几个问题来解释什么是一个Entity?
- 什么时候需要Entity?
- Entity包含哪些东西?
- 如何注册一个新的实体类型?
- 如何实现一个Entity?

什么时候需要Entity?
满足以下任意一条的,就可以用Entity。
- 需要位置或方向的同步,需要由Entity进行承载(默认的,引擎会对位置和方向进行同步)。
- 需要被客户端看到的,或者客户端要与之交互的,也需要由Entity进行承载。
- 如果需要数据持久化存储时,需要用Entity的属性,并通过自动存储或者主动调用api进行保存。
- 有远程访问的需求时,需要基于Entity上的方法申明。比如:客户端想调用服务器上的一个方法,则必须要申明一个Entity。
- 需要引擎来管理和监控的对象,可以用Entity来实现。
- 当灾难发生后需要服务端自动进行灾难恢复的对象,必须用Entity来实现。
提示
比如:一个角色Avatar肯定是一个Entity,因为Avatar需要位置、方向的同步,且要被客户端看到,并且可以交互(控制移动、跳跃、传送等),同时身上有一些需要持久化的属性,比如角色名字、角色等级等。而角色身上的物品,就不需要Entity(一般的,角色拥有的物品会用一个背包的数据结构存储到Avatar属性上)。但是,掉落在地上的物品,却需要Entity,因为满足条件1和条件2。而且一旦被拾起后,就销毁该Entity实体,并把对应数据存进Avatar的背包属性里了。
再比如:一个传送门,虽然一般不会做位移和方向变化,但是需要被客户端看到,所以需要Entity(一般,是否进入了传送门的有效范围的检测,会利用引擎的触发器系统,其也是基于Entity的)。
Entity包含哪些东西?
和编程体系中类Class差不多,一个Entity包含多个属性、多个方法申明。但是又引擎特殊的地方,比如需要一个def配置文件与之对应。
提示
除了Entity自己的部分,在使用Entity前还需要进行一个注册过程,请参见本文下的小节:如何注册一个新的实体类型。
我们先来看下def配置文件:
1. def配置文件
定义一个Entity时,必须有一个同名的def配置文件,对该Entity公开的属性或方法进行定义。只有def中定义的部分,才能被其他服务器或者客户端调用到,类似于申明了公开属性和公开方法。
比如:一个Avatar实体,其def文件应该取名为Avatar.def,引擎会自动查找并做好关联。
1.1 配置文件的位置
在{项目资产库}/scripts/entity_defs
文件夹下,新增一个entity_name.def的文件即可(将entity_name替换成实体的名字,如Avatar.def、Account.def)。
提示
该文件夹下有一个types.xml
文件,是在自定义类型时使用的,这里不做赘述,详情参见自定义数据类型。
entity_defs/interfaces
文件夹是放置接口def的。详情参见接口def。
1.2 如何编写def文件
def文件其实是一个类似XML的文件,了解XML的同学肯定会熟悉def的编写方式。我们先来看一个简单的avatar实体的def。
<root>
<!--属性定义-->
<Properties>
<!--角色的名字-->
<name>
<!--unicode作为名字类型-->
<Type> UNICODE </Type>
<!-- 自定义的协议ID -->
<Utype> 1000 </Utype>
<!--长度设置为20-->
<DatabaseLength> 20 </DatabaseLength>
<!--cell以及所有client都有该属性,并由cell向所有客户端同步他-->
<Flags> ALL_CLIENTS </Flags>
<Default> </Default>
<!--需要持久化保存-->
<Persistent> true </Persistent>
</name>
</Properties>
<!--base上的方法申明-->
<BaseMethods>
</BaseMethods>
<!--cell上的方法申明-->
<CellMethods>
</CellMethods>
<!--客户端上的方法申明-->
<ClientMethods>
</ClientMethods>
</root>
我们定义了一个名为name
的属性,类型为UNICODE
(见引擎基础类型)。是不是一目了然就能看懂?
其中,root是根节点,类似于xml的根节点。Properties内是属性定义块,BaseMethods是该实体需要在base上申明的方法,这样,不同服务器上都能知道该实体在base上有哪些方法可供调用。类似的,CellMethods是该实体在cell上申明的方法、ClientMethods是客户端上申明的方法,一般在回调给客户端时使用。
1.3 Properties属性定义块
在Properties
块下的第一层标签的名字,就是属性名,如上方例子中的name属性。其中一个属性标签下有Type
、DatabaseLength
、Flags
、Default
、Persistent
这几种子标签。
Type
: 必填,表示该属性的类型。可以使用引擎的基本类型或自定义类型,我们在此使用UNICODE,即是一个字符串。请参考引擎基础类型和自定义数据类型的章节。
Utype
: 可选,表示该属性的自定义协议ID。如果客户端不使用kbe配套的SDK来开发,客户端就需要开发跟kbe对接的协议,此时该协议ID就可便于识别。(C++协议层使用UINT16来描述一个协议ID的)如果不定义该ID,则引擎会使用自身规则自动生成协议ID。
注意
下面的内容中的方法定义块中,也可以可选的定义UType,作用是一样的!
协议ID(即UType值)必须在所有def文件中保持唯一!
DatabaseLength
: 可选,数据库中存储的长度。如果不写该标签,则根据类型自动判断。
注意
如果Type=UNICODE时,必须指定DatabaseLength的值!
Flags
: 必填,属性作用域,表明该属性在哪些区域(或理解为哪些部分、服务器上哪些app)可见以及不同的属性源头。属性的修改必须在源头上进行,并且属性的同步是从源头同步到其他部分。其中包含如下几种值:
提示
BASE
:在base可见,不需要同步。BASE_AND_CLIENT
:在base和client可见,从base同步到client。CELL_PRIVATE
:在cell可见,不需要同步。CELL_PUBLIC
:在cell可见,从实体real部分同步到其他cell。real的概念参见ghost和real的概念(可以理解为在CELL_PRIVATE的基础上,增加了cell之间的同步)CELL_PUBLIC_AND_OWN
:在cell和client可见,从cell上的实体real部分同步到自己的client以及其他cell。(可以理解为在CELL_PUBLIC的基础上,增加了向client的同步)ALL_CLIENTS
:在cell和client可见,从cell上的实体real部分同步到所有client(包括自己的client)以及其他cell。(可以理解为在CELL_PUBLIC_AND_OWN的基础上,增加了其他所有client的同步)OWN_CLIENT
:在cell和client可见,从cell同步到client。(可以理解为在CELL_PUBLIC_AND_OWN的基础上,减少了对其他cell的同步)OTHER_CLIENTS
:在cell和client可见,从cell上的实体real部分同步到所有client(排除自己的client)以及其他cell。(可以理解为在ALL_CLIENTS的基础上,减少了对自己的client的同步)
注意
在我们的例子中,使用了ALL_CLIENTS是因为该属性在cell上且只让cell上进行修改,并且会自动同步到所有客户端(包括拥有该实体的客户端)。
这个属性作用域的可见性比较好理解,但是源头到目标的传递不是很好理解,见下图:

Default
: 默认值的设置,必须和该属性的类型一致。
Persistent
: bool,是否持久化。需要持久化则为true,有些时候一些属性不需要持久化的,比如moveSpeed,都是临时性的,可以设置为false。
提示
我们例子中角色的名字name是需要持久化的。
1.4 xxMethods方法定义块
Methods分BaseMethods
(base上的方法)、CellMethods
(cell上的方法)、ClientMethods
(client客户端上的方法)。和属性申明类似,标签名为方法名,方法的参数类型在Arg
标签下设置。如:给我们例子中的Avatar增加一个base上的方法createCell
:
1.4.1 增加一个方法
...
<BaseMethods>
<!--创建cell部分的实体。
这是服务器之间通讯的方法,在def中申明之后才可以在不同服务器之间通过定义的方法进行通讯
-->
<createCell>
<!--场景的CELL ENTITYCALL-->
<Arg> ENTITYCALL </Arg>
</createCell>
</BaseMethods>
...
这里我们申明一个叫做createCell的方法,用于让Avatar实体创建其cell部分。一旦该方法申明,在不同服务器上都可以调用我们Avatar实体base部分的createCell方法。代码类似如下:
...
# 有一个Avatar类型的实体avatar
avatar.base.createCell(sceneCell)
# 其中base表示该entity的baseEntityCall,sceneCell是一个CellEntityCall,表示要在哪个cell上创建该avatar的cell部分。
...
提示
刚才提到的“不同服务器”可以是在不同base之间,或者base和cell之间。只要在def中申明的都可以被调用到,否则引擎会报错。
1.4.2 如果需要多个参数怎么办?
很简单!有多个参数时,申明多个Arg标签即可(按照申明的顺序决定了参数的顺序)!如下方代码,需要广播某个avatar的群聊信息:
<root>
...
...
<!--客户端上的方法申明-->
<ClientMethods>
<!--广播某玩家的群聊事件-->
<onChat>
<!--玩家的实体id-->
<Arg> ENTITY_ID </Arg>
<!--说话的内容-->
<Arg> UNICODE </Arg>
</onChat>
</ClientMethods>
...
</root>
第一个参数,玩家实体ID,指明是哪个玩家发出的聊天信息;第二个参数,聊天的内容。
1.4.3 Exposed标签,把方法暴露给客户端
方法中可以增加Exposed标签使该方法暴露给客户端,使得客户端可以主动调用服务器上的方法。如:给Account实体增加一个暴露给客户端来调用的enterGame方法:
...
<BaseMethods>
<!--暴露给客户端的进入游戏的方法-->
<enterGame>
<!--暴露给客户端的方法,需要声明该属性-->
<Exposed/>
</enterGame>
</BaseMethods>
...
这里我们申明一个叫做enterGame
的方法,用于客户端向服务器请求进入游戏世界。这里我们使用了Exposed标签,该标签是指该方法可以暴露给客户端,让客户端进行调用。同时KBE引擎的sdk生成器会自动生成与之对应的客户端代码(在后面客户端实现章节中会介绍)。这样一来,客户端就可以直接调用服务器上的方法进行通讯了。
1.4.4 ClientMethods中新增一个方法,使服务器可以主动调用客户端上的方法
刚才的例子中,可以让客户端调用enterGame
请求进入游戏世界,那万一该请求出错或不允许时服务器如何通知客户端呢?
...
<ClientMethods>
<!--进入游戏失败的回调-->
<onEnterGameFailed>
<!--错误代码-->
<Arg> INT8 </Arg>
</onEnterGameFailed>
<!--进入游戏成功的回调-->
<onEnterGameSuccess>
<!--无参-->
</onEnterGameSuccess>
</ClientMethods>
...
类似的,ClientMethods
下申明客户端上的远程方法,使得服务器可以主动向客户端发出请求或回调。这里,我们申明onEnterGameSuccess
:进入游戏的请求成功时回调给客户端;onEnterGameFailed
:失败时回调,并给予一个错误代码的参数,类型为INT8。
1.5 接口def(interface def)
所谓接口def有点类似基类的概念,可以通过在标签<Interfaces></Interfaces>
中写入的方式继承接口def即可。interfaces def文件必须放置在entity_defs/interfaces
文件夹下。如有一个GameObject.def
是interface
,Avatar.def
需要继承它,那么写成:
<root>
<Interfaces>
<Interface> GameObject </Interface>
</Interfaces>
<!--属性定义-->
<Properties>
...
...
</Properties>
<!--base上的方法申明-->
<BaseMethods>
</BaseMethods>
<!--cell上的方法申明-->
<CellMethods>
</CellMethods>
<!--客户端上的方法申明-->
<ClientMethods>
</ClientMethods>
</root>
这样就完成了interface的申明,那么我们怎么去理解这个interface呢?我们举个例子,在之前的代码中我们知道Avatar实体是有一个name
属性的,而name
属性是比较常用的,我们可以把它放进GameObject.def
中,使所有继承GameObject的实体都有name
属性。那我们来操作一下,先新建一个GameObject.def
的文件,并在GameObject.def
中增加该属性:
<root>
<!--属性定义-->
<Properties>
<!--角色的名字-->
<name>
<!--unicode作为名字类型-->
<Type> UNICODE </Type>
<!--长度设置为20-->
<DatabaseLength> 20 </DatabaseLength>
<!--cell以及所有client都有该属性,并由cell向所有客户端同步他-->
<Flags> ALL_CLIENTS </Flags>
<Default> </Default>
<!--需要持久化保存-->
<Persistent> true </Persistent>
</name>
</Properties>
...
...
</root>
然后,在删除原来Avatar.def中的name
属性,并把GameObject设置进Interfaces标签内。结果如下:
<root>
<Interfaces>
<Interface> GameObject </Interface>
</Interfaces>
<!--属性定义-->
<Properties>
<!-- 删除掉的name块 -->
</Properties>
<!--base上的方法申明-->
<BaseMethods>
</BaseMethods>
<!--cell上的方法申明-->
<CellMethods>
</CellMethods>
<!--客户端上的方法申明-->
<ClientMethods>
</ClientMethods>
</root>
1.6 Volatile:易变属性同步控制
可以把Volatile理解为对位置、方向进行同步控制的设置。它包含如下属性:
<root>
...
<Volatile>
<position/> <!-- 位置 -->
<!-- 描述方向的三要素 -->
<yaw/>
<pitch/>
<roll/>
<!-- 优化VolatileInfo,关于VolatileInfo可以参考API或Manual文档,
优化后服务器在确定实体在地面时(navigate)将不同步实体的Y坐标,
客户端需要判断实体isOnGround,如果实体在地面则需要做贴地计算。
在高层建筑内寻路可能需要关闭优化,让服务器同步Y,这样才能精确计算上下层位置。
(不填默认为true)
-->
<optimized>true</optimized>
</Volatile>
...
</root>
2. 属性
属性的概念,和编程中的属性是类似的,只不过他拥有一些引擎上的特性,比如自动向数据库进行数据存储,自动从数据库取出并对应的属性对象上(必须指定成可持续化后)等等。
如何Set和Get:
# 假设我们在Avatar实体上有一个属性name,其作用域是在base上可见的。
class Avatar(KBEngine.Proxy):
"""
Avatar实体的base部分
"""
def __init__(self):
KBEngine.Proxy.__init__(self)
# 如果该属性设置为可持久化,则可以直接在init时读取到
DEBUG_MSG(self.name)
def modifyName(self, name):
# 直接设置,就可以修改属性值,并在保存数据库时自动保存进去
self.name = name
引擎中属性的特性(与传统意义上的属性区别):
- 与数据库之间,自动的匹配并存储,读取时也会自动匹配属性。参见疑问:为什么没有数据库底层操作一文。
- 迁移时保证一致性和有效性
- 自动恢复
3. 方法
暂无
4. ghost和real的概念
一个Entity实体,是有real和ghost之分。real是Entity真实的部分(可以理解为权威的Entity),ghost是它的幽灵,拥有部分数据的拷贝。
一个ghost是一种优化机制下的产物,是从邻近Cell的对应real Entity的一种拷贝。
在抽象space空间的边缘地带(所谓边缘是指贴着该space有另外一个或多个space相邻,它们共同组成一个大世界的情况下)或使用teleport传送另外一个space空间时,会把Entity自己做一份ghost出来,方便在跨越space时提前做好衔接,使过渡更加自然。
一般在一个大世界的地图中会使用到,该大世界由多个cell或多个space去组成。

ghost的作用:
解决跨越Cell边界的Entity的交互问题
包括:
- 对其的方法调用,会转发给real
- 属性的同步
ghost特性:
ghost上的属性是只读的,要更改属性值只能通过方法调用来更新其对应的Real Entity。
一个属性可以是real only的, 例如: 将永远不会存在于ghost上的属性。如果一个属性对于客户端是可见的,那么该属性必须是可以ghost的,例如:当前的武器、等级、名称
如何注册一个新的实体类型?
在引擎中,真正开始使用一个实体前,必须要进行注册。那如何注册呢?
很简单,在{项目资产库}/scripts/entities.xml
文件中,写入该Entity相关字段,如:
假如我们有一个Account
和Avatar
实体。
<root>
<Account hasClient="true"></Account>
<!-- <Account hasCell="true" hasBase="true" hasClient="true"></Account> -->
<Avatar/>
</root>
一个实体通常可能会包含cell
、base
、client
三个部分,默认引擎判断实体是否包含某个部分时会检查是否包含某个部分的属性或者方法并且同时检查相关目录(cell、base、client)
在一些情况下还需要明确设定标记来指明是否包含某个部分。例如:在Unity3D插件环境下游戏编程,引擎无法判定该实体是否有客户端部分,但实体需要同步到客户端,如:传送门,那么可以强制指定标记来标明实体包含了某部分。此时可以通过hasClient
、hasBase
、hasCell
的属性来强制设置。
如何实现一个Entity?
见《Get Started中创建一个Entity实体》一文