Skip to content

移动系统

在讲解之前,必须要提及引擎中控制器Controller的概念,因为移动系统包括下一章节的触发器系统,都是基于Controller的。那什么是Controller?

Controller介绍:

  1. 用于实现那些需要在后台花费很多个tick处理的任务或功能;

  2. 在Controller结束时会回调Python脚本的特定事件,如移动结束时回调onMoveOver等;

  3. 用于实现复杂的逻辑;

  4. 因为效率原因,用C/C++进行实现;

  5. 当Entity跨越Cell边界时Controller也跟着复制到新的Cell;

  6. 每个Entity上可以有无限多个Controller;

  7. 每个Controller实例都会返回一个ControllerID,用于删除它。(利用API-Entity.cancelController(id))

Ok,说完Controller,我们来说下这章节的系统-移动系统,它其实是一个Motion Controller移动控制器。

WARNING

任何实体,在任意时刻只能有一个移动控制器,重复调用任何移动函数,都将终止之前的移动控制器。主动终止时,可使用API-Entity.cancelController(id)或Entity.cancelController(“Movement”)。

移动系统分为:非导航网格下的移动和导航网格下的移动两种。

非导航网格下的移动:

所谓非导航网格下的移动,顾名思义就是不使用导航,直接进行移动操作。

API介绍:

1. Entity.moveToPoint(self, destination, velocity, distance, userData, faceMovement, moveVertically):

描述:直线移动Entity到给定的坐标点,成功或失败会调用回调函数。

参数

destination (Vector3):要移动到的目标位置点;

velocity (float):移动速度,单位m/s;

distance (float):距离目标小于该值停止移动,如果该值为0则移动到目标位置;

userData (object):用户自定义数据,用于传给回调函数的数据;

faceMovement (bool):如果实体面向移动方向则为True。如果是其它机制则为False;一般我们都会使用True,除非是一些没有朝向的实体,如可爆炸的木桶等,可以使用False。

moveVertically (bool):设为True指移动为直线移动,设为False指贴着地面移动。如果是False,引擎会自动使移动紧贴地面,防止出现“悬空”效果。

我们来看一段代码,方便理解:(摘取Motion移动组件中的一段)

py
def gotoPosition(self, position, dist=0.0):
    """
    移动到某个位置
    :param position:Vector3, 目标位置点
    :param dist: float, 达到多少距离则判定为到达
    :return:
    """
    if self.position.distTo(position) <= 0.05:  # 阈值0.05,如果小于,则不进行移动
        return

    self.isMoving = True
    speed = self.moveSpeed * 0.1

    WARNING_MSG("Motion(%s[%i])::gotoPosition: position" % (self.owner.getScriptName(), self.ownerID, dest_pos))

    self.owner.moveToPoint(position, speed, dist, None, True, False)

如果当前位置与目标位置距离太近,则不进行移动。直接调用moveToPoint让移动控制器以speed速度向目标位置positioin进行移动,并在距离<=dist时停止。

2. Entity.moveToEntity(self, destEntityID, velocity, distance, userData, faceMovement, moveVertically):

描述:直线移动实体到另一个Entity位置,成功或失败会调用回调函数。

参数

destEntityID (int):要移动到的目标Entity实体的id,引擎会查找该实体,并进行移动;

velocity (float):移动速度,单位m/s;

distance (float):距离目标小于该值停止移动,如果该值为0则移动到目标位置;

userData (object):用户自定义数据,用于传给回调函数的数据;

faceMovement (bool):如果实体面向移动方向则为True。如果是其它机制则为False;一般我们都会使用True,除非是一些没有朝向的实体,如可爆炸的木桶等,可以使用False。

moveVertically (bool):设为True指移动为直线移动,设为False指贴着地面移动。如果是False,引擎会自动使移动紧贴地面,防止出现“悬空”效果。

我们来看一段代码,方便理解:(摘取Motion移动组件中的一段)

py
def gotoEntity(self, targetId, dist=0.0):
    """
    移动到entity位置
    :param targetId: 目标entityid
    :param dist:  float, 达到多少距离则判定为到达
    :return:
    """

    entity = KBEngine.entities.get(targetId)
    if entity is None:
        WARNING_MSG("Motion(%s[%i])::gotoEntity: not found targetID=%i" % (
            self.owner.getScriptName(), self.ownerID, targetId))
        return

    if entity.position.distTo(self.position) <= dist:
        return

    self.isMoving = True
    speed = self.moveSpeed * 0.1
    self.owner.moveToEntity(targetId, speed, dist, None, True, False)

先根据entityID获取到entity,检查其是否存在。如果存在则检查两者距离是否已经在dist之内。直接调用moveToEntity让移动控制器以speed速度向ID=targetId的实体进行移动,并在距离<=dist时停止。

3. Entity.cancelController(self, controllerID):

描述:停止一个控制器对Entity的影响。

WARNING

它只能在一个real实体上被调用。

参数

controllerID (int):要取消的控制器的ID;

一个专用的控制器类型字符串也可以作为参数,例如该章节的Movement可以作为cancelController的参数。 举个例子:

py
def stopMotion(self):
    """
    停止移动
    :return:
    """
    if self.isMoving:
        self.owner.cancelController("Movement")
        self.isMoving = False

调用该API就可以停止某个移动控制器或者使用Movement作为参数,可以停止所有移动控制器。

回调函数:

Entity.onMove:

如果这个函数在脚本中有实现,相关的Entity.moveToPoint与Entity.moveToEntity还有Entity.navigate方法被调用并且成功后底层每帧移动都会回调此函数。

WARNING

该回调函数会每帧被调用,请谨慎函数里的代码,避免影响引擎效率。

Entity.onMoveOver:

如果这个函数在脚本中有实现,移动相关的方法如Entity.moveToPoint等被调用后,在到达指定目标点时会回调此函数。

一般作为移动成功的回调来使用。

Entity.onMoveFailure:

如果这个函数在脚本中有实现,移动相关的方法如Entity.moveToPoint等被调用并且失败时会回调此函数。

使用导航网格下的移动:

顾名思义就是使用导航网格navMesh,通过寻路的方式进行移动操作。

导航网格是什么?

简单说,就是Navmesh(3D情况下)或tile数据(2D情况下),这种数据可以被导航系统读取,并生成导航数据,给自动寻路成为可能。

KBE引擎可以有多个预先生成好的导航网格,不同的网格大小(会导致不同的导航路径),每个网格位于一个layer中,互相之间不影响。layer,是一个层的意思,用于区分网格所在的层。

引擎中增加一个网格数据:

  1. 通过工具生成Navmesh或Tile数据,这部分会在工具一章中介绍,这里不再赘述。

  2. 把Navmesh或Tile数据(比如新手村的一个地形数据)放在项目的资源目录下,如{项目资产库}/res/scenes/xinshoucun下,如图:

  1. 在Space空间对应的SpaceEntity(我们例子中使用Scene实体)的初始化时,加载地形的几何数据。
py
class Scene(KBEngine.Entity):
    """
    cell上游戏场景实体实现
    """
    def __init__(self):
        KBEngine.Entity.__init__(self)

        # 一个space代表的是一个抽象的空间,这里向这个抽象的空间添加了几何资源数据,如果数据是3D场景的
        # 该space中使用navigate寻路使用的是3D的API,如果是2D的几何数据navigate使用的是astar寻路
        res_path = 'scenes/xinshoucun'
        KBEngine.addSpaceGeometryMapping(self.spaceID, None, res_path)

        DEBUG_MSG('created scene[%d] entityID = %i, res = %s.' % (self.sceneUType, self.id, res_path))

只需调用API:addSpaceGeometryMapping即可给某个Space增加几何数据。其中res_path是相对于项目的资源文件夹(即{项目资产库}/res)的。

API: KBEngine.addSpaceGeometryMapping(spaceID, mapping, path, shouldLoadOnServer, params)

描述:关联一个给定空间的几何映射,函数调用之后服务端和客户端都会加载相应的几何体数据。

在服务端上,从给定目录里加载所有的几何数据到指定的空间。这些数据可能被分成很多区块,不同区块是异步加载的。

服务端仅加载场景的几何数据提供给导航和碰撞功能使用,客户端除了几何数据外还会加载纹理等数据。 3D场景当前默认使用的是recastnavigation插件所导出的数据,2D场景当前默认使用的是MapEditor编辑器导出的数据。

参数

spaceID (int):空间ID,指定在哪个空间增加几何数据;

mapping :网格碰撞数据的映射值。

path (string):包含几何数据的目录路径。

shouldLoadOnServer (bool):可选参数,指定是否在服务端上加载几何。默认为True。

params (dict):可选参数,指定不同layer所使用的navmesh。如下:

py
KBEngine.addSpaceGeometryMapping(self.spaceID, None, resPath, True, {0 : "srv_xinshoucun_1.navmesh", 1 : "srv_xinshoucun.navmesh"})

WARNING

几何数据的加载是一个异步的过程。

  1. 根据回调函数,检查加载是否正确。

其中包括:

4.1:KBEngine.onSpaceGeometryLoaded(spaceID, mapping)

描述:空间所需求的网格碰撞等数据加载完毕。

参数

spaceID (int):空间ID;

mapping (float):网格碰撞数据的映射值。

4.2:KBEngine.onAllSpaceGeometryLoaded(spaceID, isBootstrap, mapping)

描述:空间所需求的网格碰撞等数据全部加载完毕。

参数

spaceID (int):空间ID。

isBootstrap (bool):如果一个空间被分割由多个cell共同负载,那么isBootstrap描述的是是否为加载请求的发起cell。

mapping (float):网格碰撞数据的映射值。

其他API介绍:

  1. Entity.canNavigate(self):

描述:通过这个方法判断当前实体是否可以使用导航功能。它只能在一个real实体上被调用。

通常,当实体所在Space使用KBEngine.addSpaceGeometryMapping加载过有效的导航用的碰撞数据(Navmesh或者2D的tile数据),并且实体在有效导航区域该功能可用。

  1. Entity.navigate(self, destination, velocity, distance, maxMoveDistance, maxSearchDistance, faceMovement, layer, userData):

描述:使用导航系统来使这个Entity向一个目标点移动,成功或失败会调用回调函数。

参数

destination (Vector3):Entity移向的目标点;

velocity (float):移动速度,单位m/s;

distance (float):距离目标小于该值停止移动,如果该值为0则移动到目标位置;

maxMoveDistance (float):最大的移动距离;

maxSearchDistance (float):从导航数据中最大搜索距离;

faceMovement (bool):如果实体面向移动方向则为True。如果是其它机制则为False;一般我们都会使用True,除非是一些没有朝向的实体,如可爆炸的木桶等,可以使用False;

layer (int8):使用某个层的navmesh来寻路;

userData (object):用户自定义数据,用于传给回调函数的数据;

我们来看一段代码,方便理解:(摘取Motion移动组件中的一段,这次丰富一下刚才例子中的代码,使其自动检测是否可以使用导航系统)

py
def gotoPosition(self, position, dist=0.0):
    """
    移动到某个位置
    :param position:Vector3, 目标位置点
    :param dist: float, 达到多少距离则判定为到达
    :return:
    """
    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:
        WARNING_MSG("Motion(%s[%i])::gotoPosition: position=%s canNavigate=False" % (self.owner.getScriptName(), self.ownerID, position))
        self.owner.moveToPoint(position, speed, dist, None, True, False)

在这里,与之前的例子不同,我们增加了canNavigate的判断,如果可以使用导航系统,则我们使用导航系统进行导航移动。这里我们使用了512的maxSearchDistance,使其尽大可能的进行寻路搜索。

设置与获取某个Space的自定义数据:

  1. KBEngine.setSpaceData(spaceID, key, value):

描述:设置指定key的space数据。

参数

spaceID (int):空间ID。

key (string):一个字符串关键字。

value (string):要存放的数据。

  1. KBEngine.getSpaceData(spaceID, key):

描述:获取指定key的space数据。

参数

spaceID (int):空间ID。

key (string):一个字符串关键字。