🏗️ EntityComponent
什么是EntityComponent?
在新的引擎版本中,我们引入了EntityComponent-组件的支持。组件是attach到entity上的一个单元,可以在不同类型entity上进行重用,大大加强了引擎开发能力。
如:控制位置的组件Motion,该组件只对entity的位移做控制,如此一来Avatar、Monster等都可以attach该组件,而不用反复编写位移的代码。
概述:
在传统的结构设计中一般会使用“派生”来描述对象之间的关系。子类通过派生父类,来获得父类的功能。在设计游戏对象时,会根据游戏本身的需要而为游戏对象添加各种功能支持,比如渲染,碰撞,刚体,粒子系统等等。这些通用功能为了能够为各种派生类提供服务,都必须实现到基类中。这样就导致了游戏对象基类变得非常庞大臃肿,即难使用,又难维护。
“基于组件”的对象模型就是把所有需要提供给游戏对象的基础功能都独立成单独的“组件模块”(Component),一个具体的游戏对象可以将它需要的功能模块组合到一起使用。所有“功能”不再是父类中的接口,而变成子对象实例,为游戏对象提供服务。这样既保证了功能代码的可重用性,又增加了整个对象体系的模块化和灵活度。
在我们引擎中,Entity实体是EntityComponent组件的拥有者,也类似于容器,承载着各种各样的组件对象。虽然组件之间是完全并列的关系,但是他们之间还是会有一定的层级关系的,在设计一个对象的具体功能时,组件一般会被分为三个层次:
组件设计的层次:
1.引擎的基础组件:
这些组件会由引擎内部提供,完成基本的功能,可以被用来组合实现一些高级的复合功能。
提示
KBE引擎当前版本尚未推出基础组件,在后续的版本中会陆续添加。
2.模块功能脚本的组件:
这部分是开发者自行根据业务需求进行设计,实现一些相对独立的通用模块功能的组件。这类组件的设计,是脚本可重用的关键,需要仔细分析游戏对象中哪些功能可以被独立出来成为一个可重用的功能模块组件,并且在实现上应该尽量降低与其他组件的耦合性。
比如:一个角色游戏对象,需要设计一个Motion移动组件,在引擎端控制该对象的移动相关部分。这个功能相对独立,与其将他实现到角色Entity中,不如独立成一个组件,这样所有有类似行为的实体都可以通过包含这个组件来实现移动功能,比如Monster怪物、Npc都可以用上。
模块功能组件之间还可能有依赖关系,也就是一个功能模块组件可能依赖与另一个功能模块组件,从而在这个组件层次上形成更多的子层次。通过不同的“组装”,使实体的功能丰富多彩。
3.高层的胶水代码脚本或组件:
这些脚本用来真正将引擎基础组件和模块功能组件组合到一起实现最终游戏对象逻辑。用“胶水代码”来形容这些脚本非常的贴切,就是把所有这些子功能“粘”在一起。比如,一个Monster怪物实体,其拥有Motion移动、Combat战斗等组件,将这些组件组合起来实现一个AI战斗逻辑:跟随角色,并当进入技能释放范围时进行战斗的具体游戏逻辑。因为这一层次代表的都是最高层的游戏行为控制对象,是具体的游戏逻辑的“胶水”代码,不会再为更上层提服务,所以本身的可重用性并不高。但是这些对象之间按照类型区分,往往会有一些功能上的重合,所以反而可以继续使用派生关系来实现功能的重用。比如不同的怪物都有这些机制,只不过一些特性不一样(即参数配置不一样),那可以进行继承,并实现自己特殊的功能。
提示
从某种意义上来讲,这些“胶水代码”不算真正的“组件”,但是这些代码写在Entity实体中又太过零散、无法管理。我们暂且称之为业务组件,这样开发时相对独立、清晰。
小结:
一个功能到底应该用组件实现还是用派生实现并没有非常明确的界限,应该根据需要灵活运用。如果要实现的是demo级别的小工程,并不需要考虑很多,直接在Entity实体上实现功能就可以了。但是如果要有效地组织复杂的工程,提高代码的重用性,充分理解和合理的利用“基于组件”的对象模型设计思想还是很重要的。
那KBE引擎中的组件是怎么样的呢?
下面我们会围绕这几个问题来解释什么是EntityComponent?
Entity和EntityComponent之间是什么样的关系?
什么时候需要EntityComponent?
EntityComponent包含哪些东西?
如何实现一个EntityComponent?
如何给一个Entity增加一个EntityComponent?
如何获取一个Entity上的EntityComponent?
KBE引擎中的EntityComponent组件

Entity和EntityComponent的关系:
一个Entity可以Attach多个Component。如Avatar实体可以有Combat战斗组件、Motion移动组件等等。
Component必须Attach到Entity上才可以工作,无法独立运行或运作。
Component虽然是基于Entity的,但在设计时,一般的,无需指定某个固定类型的Entity。比如,Motion移动组件,可以给Avatar实体,使角色有移动能力,同时,又可以给Monster实体,让怪物也有移动能力。
Component之间,一般的,是相对独立或隔离的,但可以通过引擎内event的相关api或getComponent等方法互相进行访问。
提示
如Motion移动组件、Combat战斗组件,两者相对独立。但是AI组件会对Motion和Combat进行访问,根据自己逻辑的判断使Monster进行移动或进行战斗。
什么时候需要EntityComponent?
需要重用的功能模块。
某个类型的Entity拥有的功能比较复杂,需要进行独立封装时。
EntityComponent包含哪些东西,与Entity有啥不同?
和Entity差不多,一个EntityComponent包含多个属性、多个方法申明,同时也需要一个def配置文件与之对应。请参考Entity介绍一文。我们这里只说一些不同的点:
1. def配置文件的位置不同
我们知道Entity的def配置文件位于{项目资产库}/scripts/entity_defs
的一级目录下,而Component是在该目录下的components
子文件夹下。如下图:

该图中,声明了AI、Bag、Combat、Motion、SkillBox、Teleport等组件。具体可以参见mmo demo相关的介绍,这里就不再赘述。
2. def配置稍有不同
没有了Volatile的标签。因为对于Component来说,位置、方向的控制,都在Entity上,所以就不需要了。
没有Interfaces标签。Component组件不支持Interface的继承,所以没有了该标签。
3. 实现脚本的存放位置不同
我们知道Entity的实现脚本文件位于{项目资产库}/scripts/base
的一级目录下(如果是cell部分的实现代码,则在{项目资产库}/scripts/cell
下),而Component是在该目录下的components
子文件夹下。如下图:

4. api的差异
多了owner、ownerID等属性。owner是指明该组件的拥有者的实体对象;ownerID顾名思义就是拥有者的实体ID。
方法上也比Entity要少一些,在使用时需要self.owner.xxx调用owner实体的对应方法。
但是,几乎所有回调函数(如:onTimer、onDestroy等),引擎已经处理过并转发给了组件,所以回调函数是可以直接在组件上处理的。
并且,新增了onAttached和onDetached的回调函数,用来表示该组件已被附加或被分离的事件。
提示
详情可以参考API手册组件EntityComponent。
如何实现一个EntityComponent?
很简单,和Entity类似,比如我们创建一个Motion组件,如下:
1. 创建一个def配置文件
在{项目资产库}/scripts/entity_defs/components
文件夹下创建一个Motion.def
文件,文件内容如下:
<!--负责移动相关的组件-->
<root>
<Properties>
<!--移动速度-->
<moveSpeed>
<Type> UINT8 </Type> <!-- 使用时要缩放10倍, 最高25.5米每秒 -->
<Flags> ALL_CLIENTS </Flags>
<Default> 50 </Default>
</moveSpeed>
<!--是否正在移动-->
<isMoving>
<Type> BOOL </Type>
<Flags> CELL_PRIVATE </Flags>
<Default> 0 </Default>
</isMoving>
</Properties>
<BaseMethods>
</BaseMethods>
<CellMethods>
</CellMethods>
<ClientMethods>
</ClientMethods>
</root>
这里简单的声明了moveSpeed
和isMoving
的属性,没有做任何Method方法声明。
提示
moveSpeed
:移动速度,使用ALL_CLIENTS的Flags,使客户端和Cell都可见,并由Cell同步给客户端。
isMoving
:标记一下是否在移动中。
2. 创建一个实现组件的py脚本
还是拿Motion组件
的例子,因为Motion
在base
上没有逻辑,所以我们在{项目资产库}/scripts/cell/components
文件夹下创建Motion.py
,代码内容如下:
# -*- coding: utf-8 -*-
import KBEngine
from KBEDebug import *
import Math
class Motion(KBEngine.EntityComponent):
"""
负责运动的组件
"""
def __init__(self):
KBEngine.EntityComponent.__init__(self)
def gotoPosition(self, position, dist=0.0):
"""
移动到某个位置
:param position:Vector3, 目标位置点
:param dist: float, 达到多少距离则判定为到达
:return:
"""
if self.isMoving:
self.stopMotion()
if self.position.distTo(position) <= 0.05: # 阈值0.05,如果小于,则不进行移动
return
self.isMoving = True
speed = self.moveSpeed * 0.1
if self.owner.canNavigate():
DEBUG_MSG("Motion(%s[%i])::gotoPosition: canNavigate=True" % (self.owner.getScriptName(), self.ownerID))
self.owner.navigate(Math.Vector3(position), speed, dist, speed, 512.0, True, 0, None)
else:
if dist > 0.0:
dest_pos = Math.Vector3(position) - self.position
dest_pos.normalise()
dest_pos *= dist
dest_pos = position - dest_pos
else:
dest_pos = Math.Vector3(position)
WARNING_MSG("Motion(%s[%i])::gotoPosition: canNavigate=False" % (self.owner.getScriptName(), self.ownerID))
self.owner.moveToPoint(dest_pos, speed, 0, None, True, False)
def stopMotion(self):
"""
停止移动
:return:
"""
if self.isMoving:
self.owner.cancelController("Movement")
self.isMoving = False
# --------------------------------------------------------------------------------------------
# System Callbacks
# --------------------------------------------------------------------------------------------
def onAttached(self, owner):
"""
组件被附加到Entity时激发
:param owner: 组件拥有者
:return:
"""
INFO_MSG("Motion(%s[%i])::onAttached fired:" % (self.owner.getScriptName(), self.ownerID))
def onDetached(self, owner):
"""
组件从Entity上移除时激发
:param owner:组件拥有者
:return:
"""
INFO_MSG("Motion(%s[%i])::onDetached fired:" % (self.owner.getScriptName(), self.ownerID))
def onMove(self, controllerId, userArg):
"""
当owner回调onMove时调用本组件的方法
:param controllerId:
:param userArg:
:return:
"""
# DEBUG_MSG("Motion::onMove: %i controllerId =%i, userarg=%s" % (self.owner.id, controllerId, userarg))
self.isMoving = True
def onMoveFailure(self, controllerId, userarg):
"""
当owner回调onMoveFailure时调用本组件的方法
使用引擎的任何移动相关接口, 在entity一次移动失败时均会调用此接口
"""
ERROR_MSG(
"Motion(%s[%i])::onMoveFailure: controllerId =%i, userarg=%s" % (
self.owner.getScriptName(), self.ownerID, controllerId, userarg))
self.isMoving = False
def onMoveOver(self, controllerId, userarg):
"""
当owner回调onMoveOver时调用本组件的方法
使用引擎的任何移动相关接口, 在entity移动结束时均会调用此接口
"""
self.isMoving = False
基本和Entity实体的实现脚本类似,这里就不进行解释了,可以到《脚本篇-组件》一文进行查看。
如何给一个Entity增加一个EntityComponent?
我们知道了如何创建一个Component组件,那组件如何附加到对应的Entity实体上呢?
也很简单,在Entity的def配置文件上加一条Component的配置即可!拿Avatar.def举例:
<root>
...
...
<Components>
<!--移动相关的组件-->
<motion>
<Type> Motion </Type>
<!--默认的,组件的Persistent是true的,所以必须显式的指明为false-->
<Persistent> false </Persistent>
</motion>
...
...
</Components>
...
</root>
标签块Components是该实体的组件声明区域,我们在下面新增了motion标签,作为组件的名字,Type是组件的类型。
这样一来,Avatar实体就拥有了Motion组件,可以通过调用self.motion得到Motion组件对象啦。
注意
我们看到,可以利用def配置文件给一个Entity配置一个组件。但是不允许动态的创建和卸载组件!这容易导致一些极端问题!
由于组件是attach在实体的,所以必须先要有实体,而实体是存在分布式的特性,意味着你不知道某一个实体的本体是在哪个app上(仅仅是通过EntityCall进行通讯)。
一旦动态创建或销毁其附属的组件,是无法被同步的,这导致基于组件的通讯就会出现错误!
如何获取一个Entity上的EntityComponent?
如果知道一个Entity上组件的名字,则可以直接调用self.组件名获取,如上面例子中的self.motion。
通过组件的类型获取。利用API-Entity.getComponent(‘componentName’)得到第一个匹配的组件或Entity.getComponent(‘componentName’, true)得到匹配的组件list。
见代码:
# 假设有一个组件类型=Motion的组件。
# 获取一个Motion组件
motion = self.getComponent("Motion")
#获取所有Motion组件,返回的结果是一个list
motions = self.getComponent("Motion", true)