移动实体
如何随时间逐步移动、旋转和缩放实体,并以增量方式变化。
要在一段时间内移动、旋转或缩放场景中的实体,请使用 补间 组件。引擎会平滑执行所需的变换,在持续时间结束前的每一帧显示更新。另外, Transform 受影响实体的组件值会实时更新,以便在场景代码中需要进行邻近检查时使用。
Tween 组件具有以下函数:
setMove:在两个点之间移动setRotate:在两个方向之间旋转setScale:在两个尺寸之间缩放setMoveRotateScale:同时在这三个参数上进行过渡setMoveContinuous:沿相同方向持续移动setRotateContinuous:沿相同方向持续旋转setTextureMove:在两个位置之间偏移材质的纹理setTextureMoveContinuous:沿相同方向持续偏移材质的纹理
在两个点之间移动
要让实体在两个点之间移动,创建一个 补间 组件,并使用 setMove 函数。
const myEntity = engine.addEntity()
Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)
Tween.setMove(myEntity,
Vector3.create(1, 1, 1),
Vector3.create(8, 1, 8),
2000
)移动 Tween 需要以下信息:
实体:要移动的实体起始:用于起始位置的 Vector3结束:用于结束位置的 Vector3持续时间:在两个位置之间移动所需的毫秒数
此外,还提供以下可选参数:
easingFunction:要使用的缓动函数。参见 非线性 Tween
在两个方向之间旋转
要让实体在两个点之间旋转,创建一个 补间 组件,并使用 setRotate 函数。
旋转 Tween 需要以下信息:
起始:用于起始旋转的 Quaternion结束:用于结束旋转的 Quaternion持续时间:在两个位置之间移动所需的毫秒数
此外,还提供以下可选参数:
easingFunction:要使用的缓动函数。参见 非线性 Tween
带枢轴点旋转
当旋转实体时,旋转总是相对于实体的中心坐标。要使用另一组坐标作为枢轴点来旋转实体,请创建第二个(不可见)实体,将枢轴点作为其位置,并让它成为你要旋转的实体的父级。
当旋转父实体时,它的子实体也会都围绕父实体的位置旋转。注意, 位置 子实体的位置是相对于父实体的位置而言的。
注意,在这个示例中,系统正在旋转 pivotEntity 实体,它是 childEntity 实体,从玩家摄像机位置向前追踪一条射线。
在两个尺寸之间缩放
要在两个尺寸之间更改实体的缩放,请创建一个 补间 组件,并使用 setScale 函数。
缩放 Tween 需要以下信息:
起始:用于起始尺寸的 Vector3结束:用于结束尺寸的 Vector3持续时间:在两个位置之间移动所需的毫秒数
此外,还提供以下可选参数:
easingFunction:要使用的缓动函数。参见 非线性 Tween
从当前位置移动
当 Tween 处于活动状态时,引擎会实时更新 Transform 实体的组件值。这意味着你可以随时从实体的 Transform 组件中读取实体当前的位置,并将该值作为新 Tween 的 起始 。然后实体会从它当前所在的位置移动到目标位置,不管它是如何到达那里的。
旅行中重新设定目标
调用 setMove (或其他任何 tween 函数) 在已有运行中 tween 的实体上,会用新的 tween 替换正在运行的 tween。由于 起始 是从实体的实时位置读取的,这会在移动途中平滑地重新定向实体:它会从当前时刻所在的位置改变路线,不会有任何跳变。
下面的示例会将实体移动到玩家点击的任意平台。如果玩家在实体仍在移动时点击另一个平台,实体会平滑地改变方向前往新目标。
要在实体到达目的地时运行逻辑,请参见 Tween 完成时.
非线性 Tween
补间可以遵循不同的 缓动函数 ,这些会影响随时间变化的速率。 线性的 函数,意味着变化速度从开始到结束都是恒定的。可供选择的选项很多,它们会绘制出不同形状的曲线,取决于开头和/或结尾是否缓慢开始,以及缓慢到什么程度。有一个 easeinexpo 曲线开始时慢,结束时快,以指数方式加速;相反, easeoutexpo 曲线开始时快,结束时慢。
可选的 easingFunction 参数取值来自 EasingFunction 枚举,提供以下选项:
EF_EASEBACKEF_EASEBOUNCEEF_EASECIRCEF_EASECUBICEF_EASEELASTICEF_EASEEXPOEF_EASEINBACKEF_EASEINBOUNCEEF_EASEINCIRCEF_EASEINCUBICEF_EASEINELASTICEF_EASEINEXPOEF_EASEINQUADEF_EASEINQUARTEF_EASEINQUINTEF_EASEINSINEEF_EASEOUTBACKEF_EASEOUTBOUNCEEF_EASEOUTCIRCEF_EASEOUTCUBICEF_EASEOUTELASTICEF_EASEOUTEXPOEF_EASEOUTQUADEF_EASEOUTQUARTEF_EASEOUTQUINTEF_EASEOUTSINEEF_EASEQUADEF_EASEQUARTEF_EASEQUINTEF_EASESINEEF_LINEAR
恒定旋转
要让实体持续旋转,请使用 补间 组件,并使用 setRotateContinuous 函数。
持续旋转 Tween 需要以下信息:
实体:要旋转的实体方向:用于确定旋转轴的 Quaternion。只有该旋转的轴向重要,其角度大小会被忽略。例如,Quaternion.fromEulerDegrees(0, -1, 0)和Quaternion.fromEulerDegrees(0, -90, 0)效果相同,都会围绕 y 轴向负方向旋转。速度:实体每秒旋转多少度。负值会朝相反方向旋转。
此外,还提供以下可选参数:
持续时间:维持旋转多久的毫秒数。此时间过后,旋转会停止。
恒定移动
要让实体沿相同方向持续移动,请使用 补间 组件,并使用 setMoveContinuous 函数。
持续移动 Tween 需要以下信息:
实体:要移动的实体方向:用于移动的 Vector3速度:实体每秒移动多少米
此外,还提供以下可选参数:
持续时间:维持移动多久的毫秒数。此时间过后,移动会停止。
持续移动 Tween 需要以下信息:
跟随移动目标
setMoveContinuous 当目标不断变化时,这也是正确的工具,例如实体跟随玩家时。人们很容易想改为每帧创建一个新的 setMove tween,目标指向玩家的最新位置,但这会造成明显卡顿。
原因是你从 Transform 组件中读取的位置,是引擎最后一次回传给场景的位置,因此它会比实体真实所在的位置落后几帧。当你把这个值作为新 tween 的 起始 时,引擎会立即应用它,而实体会跳回几帧前所在的位置。一次这样的修正几乎察觉不到,但如果每秒重复很多次就会很明显。
setMoveContinuous 则可以完全避免这个问题,因为你提供的是方向和速度,而不是起始点。引擎会持续使用实体的真实位置,因此通过替换 tween 来改变方向时,实体不会跳跃。
Tween 序列
要让实体按顺序播放一系列 tween,请使用 TweenSequence 组件。该组件需要两个字段:
sequence:一个包含多个 tween 定义的数组,它们将按顺序执行。数组可以为空,在这种情况下它只播放当前 tween。loop(可选):如果未提供,序列只播放一次。如果该字段存在,则其值必须为TweenLoop枚举中的一个值。可接受的值有:TL_RESTART:当序列结束时,它会重新开始。如果最后一个状态与第一个状态不匹配,实体会立刻从一个跳到另一个。TL_YOYO:当序列结束时,它会反向执行,所有 tween 逆向播放,直到再次回到起点。然后它会再次开始。
来回移动
要让平台在两个位置之间持续来回移动,请将 sequence 数组留空,并设置 loop 为 TweenLoop.TL_YOYO
实体会在起点和终点之间来回移动,两个方向上的持续时间和缓动函数都相同。
沿路径跟随
要让实体沿包含多个点的更复杂路径移动,请在 sequence 中的 TweenSequence 组件。
注意,在 TweenSequence 中定义 tween 时,你需要使用更详细的 Tween.Mode.Move,或 Tween.Mode.Rotate,或 Tween.Mode.Scale 格式来定义 tween。在这种更详细的格式中,你需要指定:
持续时间:在两个位置之间移动所需的毫秒数easingFunction:要使用的缓动函数。参见 非线性 Tween。在这种格式中,该值是必需的。mode:tween 的模式,可以是Tween.Mode.Move,Tween.Mode.Rotate,或Tween.Mode.Scale.
而在 mode 字段中,你需要指定:
起始:tween 的起始值结束:tween 的结束值
Tween 完成时
使用 tweenSystem.tweenCompleted 来检测某个 tween 何时结束。这对于在 tween 结束时执行操作很有用,例如打开电梯门。
同时 Tween
要让实体在初始状态和结束状态之间移动、旋转和缩放,请创建一个 补间 组件,并使用 setMoveRotateScale 函数。该函数也可用于这三个参数的任意组合。
移动 Tween 需要以下信息:
实体:要移动的实体params:一个包含以下参数的对象位置: (可选) 一个带有起始和结束值的对象,既作为Vector3.旋转: (可选) 一个带有起始和结束值的对象,既作为Quaternion.缩放: (可选) 一个带有起始和结束值的对象,既作为Vector3。至少需要提供位置,旋转或缩放中的一个。持续时间: (必需) 在两组值之间过渡需要多少毫秒easingFunction: (可选) 要使用的缓动函数。参见 非线性 Tween
一个实体只能有一个 补间 组件,而每个 tween 组件一次只能执行一种变换。通过 setMoveRotateScale tween 类型,你可以让实体横向移动并同时旋转,但这两种运动会遵循同一时间线。如果你需要彼此独立的过渡,可以作为变通方案使用父子实体。例如,你可以让一个不可见的父实体横向移动,同时让一个可见的子实体旋转。
在下面的代码片段中,一个父实体在旋转,而一个子实体在缩放。
暂停 tween
要暂停 tween,请将 playing 属性改为 false。要恢复它,再把它改回 true。
要结束一个不需要继续的 tween,请删除实体上的 补间 组件。如果该实体还在使用 TweenSequence 组件,也一起删除它。
基于系统的 Tween
与其使用 Tween 组件并让引擎处理变换,你也许更愿意通过 系统 在场景中逐帧、增量地完成这个过渡。每次函数运行时都让实体移动一小段。
一方面,这让你在每一帧重新计算运动时拥有更强的控制力。另一方面,代码更复杂,而且性能较差的机器上的玩家可能会觉得 tween 有卡顿感,能察觉到每次增量。
通过系统移动
移动实体最简单的方法是逐步修改 位置 中存储的值 Transform 组件。
在这个示例中,我们让实体在游戏循环的每个 tick 中移动 0.05 米。
Vector3.Forward() 会返回一个朝前、长度为 1 米的向量。在本例中,我们再使用 Vector3.scale()将这个向量缩小到其长度的 1/20。如果我们的场景每秒有 30 帧,那么该实体的移动速度就是每秒 1.5 米。

通过系统旋转
让实体旋转的最简单方法,是逐步增量地更改 Transform 组件中的值,并将其作为系统函数的一部分来运行。
注意,为了将当前旋转与每次增量组合起来,我们使用 Quaternion.multiply。在四元数数学中,你是通过相乘而不是相加来组合两个旋转的。一个四元数乘以另一个四元数所得到的旋转,将等同于先执行一个旋转再执行另一个旋转后的最终旋转。
在这个示例中,我们会在游戏循环的每个 tick 中,让实体向上旋转 1 度。

通过系统绕枢轴点旋转
当旋转实体时,旋转总是相对于实体的中心坐标。要使用另一组坐标作为枢轴点来旋转实体,请创建第二个(不可见)实体,将枢轴点作为其位置,并让它成为你要旋转的实体的父级。
当旋转父实体时,它的子实体也会都围绕父实体的位置旋转。注意, 位置 子实体的位置是相对于父实体的位置而言的。
注意,在这个示例中,系统正在旋转 pivotEntity 实体,它是 childEntity 实体,从玩家摄像机位置向前追踪一条射线。

调整移动以延迟时间
假设访问你场景的玩家难以跟上帧率的节奏。这可能会导致移动看起来跳动,因为并非所有帧的时间间隔都相同,但每一帧都让实体移动相同的距离。
你可以使用 dt 参数来调整移动的缩放比例,以补偿这种不均匀的时间间隔。
上面的示例会让移动速度与前面的移动示例大致相同,即使帧率下降也是如此。当以每秒 30 帧运行时, dt 的值是 1/30。
你也可以通过将旋转量乘以 dt.
通过系统在两个点之间移动
如果你想让实体在两个点之间平滑移动,请使用 lerp (线性插值)算法。这个算法在游戏开发中非常有名,因为它非常实用。
该 lerp() 函数接受三个参数:
表示起始位置的向量
表示目标位置的向量
amount,一个从 0 到 1 的值,表示要进行多少比例的位移。
线性插值算法会在两条向量之间的路径中找到一个与所提供比例相匹配的中间点。
例如,如果起始向量是 (0, 0, 0) 而目标向量是 (10, 0, 10):
使用 0 的比例会返回 (0, 0, 0)
使用 0.3 的比例会返回 (3, 0, 3)
使用 1 的比例会返回 (10, 0, 10)
要在 lerp() 你的场景中实现这一点,我们建议创建一个 自定义组件 来存储所需信息。你还需要定义一个系统,用于在每一帧中实现渐进式移动。

通过系统在两个角度之间旋转
如果要在两个角度之间平滑旋转,请使用 slerp (球面 线性插值)算法。这个算法与一个 lerp非常相似,但它处理的是四元数旋转。
该 slerp() 函数接受三个参数:
要在你的场景中实现这一点,我们建议将传入 Slerp() 函数的数据存储在一个 自定义组件中。你还需要定义一个系统,用于在每一帧中实现渐进式旋转。

一种更简单但效率较低的方法是利用 Quaternion.rotateTowards 函数,并且避免使用任何自定义组件。
在上面的示例中 Quaternion.rotateTowards 接受三个参数:初始旋转、所需的最终旋转,以及每帧允许的最大增量。在这种情况下,由于最大增量为 dt * 10 度,旋转将会在大约 9 秒的时间内完成。
请注意,该系统还会检查旋转是否完成,如果完成就会将系统从引擎中移除。否则,即使旋转已经完成,系统也会在每一帧持续进行计算。
通过系统在两种尺寸之间改变缩放
如果你想让实体平滑地改变大小而不改变其比例,请使用 lerp (线性插值)算法的 Scalar 对象。
否则,如果你想以不同的比例改变各个轴,请使用 Vector3 来表示起始缩放和目标缩放,然后使用 lerp 的函数 Vector3.
该 lerp() 的函数 Scalar 对象接受三个参数:
起始缩放的数值
目标缩放的数值
amount,一个从 0 到 1 的值,表示要进行多少比例的缩放。
要在你的场景中实现这个 lerp,我们建议创建一个自定义组件来存储所需信息。你还需要定义一个系统,用于在每一帧中实现渐进式缩放。

通过系统在两个点之间以不规则速度移动
在使用 lerp 方法时,你可以让移动速度变为非线性。在前面的示例中,我们在每一帧中将 lerp 数值增加固定 مقدار,但我们也可以使用数学函数让数值呈指数增长,或采用其他方式来赋予不同的移动节奏。
你也可以使用会产生周期性结果的函数,比如正弦函数,来描述一种来回往复的移动。
通常,这些非线性过渡能为场景注入大量生机。沿曲线加速或逐渐减速的移动,能够很好地说明一个物体或角色的性质。你甚至可以利用会产生弹跳效果的数学函数。
上面的示例与我们之前展示的线性 lerp 示例非常类似,但 fraction 字段在每个 tick 中都会映射到一个非线性值。这个非线性值用于计算 lerp 函数,从而产生沿指数曲线运动的效果。
你也可以像上面那样,通过将线性过渡映射到曲线来以同样方式处理旋转或缩放的过渡。

通过系统沿路径移动
你可以让一个实体在一组向量数组上循环,在每个向量之间执行 lerp 移动,从而沿更复杂的路径行进。
上面的示例定义了一条由四个 3D 向量组成的 3D 路径。 PathTransportData 自定义组件保存的数据与上面示例中的自定义组件所使用的数据相同,但增加了一个 lerp path path 数组,其中包含路径中的所有点,以及一个 pathTargetIndex 字段,用于跟踪当前正在使用路径的哪个分段。
这个系统与 lerp 示例中的系统非常相似,但当一个 lerp 动作完成时,它会设置 目标 和 origin 字段为新值。如果到达路径末尾,我们就返回到路径中的第一个值。

纹理补间
要让纹理平滑滑动,请使用 补间 组件,并使用 setTextureMove 函数。
纹理补间接受以下信息:
实体:要移动其纹理的实体起始:起始位置的 Vector2结束:结束位置的 Vector2持续时间:在两个位置之间移动所需的毫秒数
此外,还提供以下可选参数:
movementType:(可选),定义移动是在 offset 还是 tiling 字段上。默认使用 offset。easingFunction:要使用的缓动函数。参见 非线性 Tween.
恒定纹理移动
要让纹理持续滑动,请使用 补间 组件,并使用 setTextureMoveContinuous 函数。
连续纹理补间接受以下信息:
实体:要移动其纹理的实体方向:用于移动的 Vector2速度:实体每秒移动多少单位
此外,还提供以下可选参数:
movementType:定义移动是在 offset 还是 tiling 字段上。默认使用 offset。持续时间:维持移动多久的毫秒数。此时间过后,移动会停止。
在以下内容中了解更多有关纹理补间的信息: Texture Tweens 部分。
最后更新于