🚶 移动系统
移动系统
这一章节中,我们会介绍Entity实体的Cell部分的功能之一:移动系统!
在讲解之前,必须要提及引擎中控制器Controller的概念,因为移动系统包括下一章节的触发器系统,都是基于Controller的。那什么是Controller?
Controller介绍:
用于实现那些需要在后台花费很多个tick处理的任务或功能;
在Controller结束时会回调Python脚本的特定事件,如移动结束时回调onMoveOver等;
用于实现复杂的逻辑;
因为效率原因,用C/C++进行实现;
当Entity跨越Cell边界时Controller也跟着复制到新的Cell;
每个Entity上可以有无限多个Controller;
每个Controller实例都会返回一个ControllerID,用于删除它。(利用API-Entity.cancelController(id))
Ok,说完Controller,我们来说下这章节的系统-移动系统,它其实是一个Motion Controller移动控制器。
注意
任何实体,在任意时刻只能有一个移动控制器,重复调用任何移动函数,都将终止之前的移动控制器。主动终止时,可使用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移动组件中的一段)
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移动组件中的一段)
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的影响。
注意
它只能在一个real实体上被调用。
参数
controllerID
(int):要取消的控制器的ID;
一个专用的控制器类型字符串也可以作为参数,例如该章节的Movement可以作为cancelController的参数。
举个例子:
def stopMotion(self):
"""
停止移动
:return:
"""
if self.isMoving:
self.owner.cancelController("Movement")
self.isMoving = False
调用该API就可以停止某个移动控制器或者使用Movement作为参数,可以停止所有移动控制器。
回调函数:
Entity.onMove:
如果这个函数在脚本中有实现,相关的Entity.moveToPoint与Entity.moveToEntity还有Entity.navigate方法被调用并且成功后底层每帧移动都会回调此函数。
注意
该回调函数会每帧被调用,请谨慎函数里的代码,避免影响引擎效率。
Entity.onMoveOver:
如果这个函数在脚本中有实现,移动相关的方法如Entity.moveToPoint等被调用后,在到达指定目标点时会回调此函数。
一般作为移动成功的回调来使用。
Entity.onMoveFailure:
如果这个函数在脚本中有实现,移动相关的方法如Entity.moveToPoint等被调用并且失败时会回调此函数。
使用导航网格下的移动:
顾名思义就是使用导航网格navMesh,通过寻路的方式进行移动操作。
导航网格是什么?
简单说,就是Navmesh(3D情况下)或tile数据(2D情况下),这种数据可以被导航系统读取,并生成导航数据,给自动寻路成为可能。
KBE引擎可以有多个预先生成好的导航网格,不同的网格大小(会导致不同的导航路径),每个网格位于一个layer中,互相之间不影响。layer,是一个层的意思,用于区分网格所在的层。
引擎中增加一个网格数据:
通过工具生成Navmesh或Tile数据,这部分会在工具一章中介绍,这里不再赘述。
把Navmesh或Tile数据(比如新手村的一个地形数据)放在项目的资源目录下,如{项目资产库}/res/scenes/xinshoucun下,如图:
- 在Space空间对应的SpaceEntity(我们例子中使用Scene实体)的初始化时,加载地形的几何数据。
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。如下:
KBEngine.addSpaceGeometryMapping(self.spaceID, None, resPath, True, {0 : "srv_xinshoucun_1.navmesh", 1 : "srv_xinshoucun.navmesh"})
注意
几何数据的加载是一个异步的过程。
- 根据回调函数,检查加载是否正确。
其中包括:
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介绍:
Entity.canNavigate(self):
描述:通过这个方法判断当前实体是否可以使用导航功能。它只能在一个real实体上被调用。
通常,当实体所在Space使用KBEngine.addSpaceGeometryMapping加载过有效的导航用的碰撞数据(Navmesh或者2D的tile数据),并且实体在有效导航区域该功能可用。
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移动组件中的一段,这次丰富一下刚才例子中的代码,使其自动检测是否可以使用导航系统)
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的自定义数据:
KBEngine.setSpaceData(spaceID, key, value):
描述:设置指定key的space数据。
参数
spaceID
(int):空间ID。
key
(string):一个字符串关键字。
value
(string):要存放的数据。
KBEngine.getSpaceData(spaceID, key):
描述:获取指定key的space数据。
参数
spaceID
(int):空间ID。
key
(string):一个字符串关键字。