> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/move-entities.md).

# 移动实体

如何随时间逐步移动、旋转和缩放实体，并以增量方式变化。

要在一段时间内移动、旋转或缩放场景中的实体，请使用 `补间` 组件。引擎会平滑执行所需的变换，在持续时间结束前的每一帧显示更新。另外， `Transform` 受影响实体的组件值会实时更新，以便在场景代码中需要进行邻近检查时使用。

{% hint style="info" %}
**💡 提示**：在 [Creator Hub 中的场景编辑器](/creator/content-creator-zh/chang-jing-bian-ji-qi/ru-men/about-editor.md)，你可以通过 **操作**以无代码方式触发声音播放，参见 [让任意项目变为智能项目](/creator/content-creator-zh/chang-jing-bian-ji-qi/jiao-hu-xing/make-any-item-smart.md).
{% endhint %}

Tween 组件具有以下函数：

* `setMove`：在两个点之间移动
* `setRotate`：在两个方向之间旋转
* `setScale`：在两个尺寸之间缩放
* `setMoveRotateScale`：同时在这三个参数上进行过渡
* `setMoveContinuous`：沿相同方向持续移动
* `setRotateContinuous`：沿相同方向持续旋转
* `setTextureMove`：在两个位置之间偏移材质的纹理
* `setTextureMoveContinuous`：沿相同方向持续偏移材质的纹理

## 在两个点之间移动

要让实体在两个点之间移动，创建一个 `补间` 组件，并使用 `setMove` 函数。

```ts
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](#non-linear-tweens)

## 在两个方向之间旋转

要让实体在两个点之间旋转，创建一个 `补间` 组件，并使用 `setRotate` 函数。

```ts
const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)

Tween.setRotate(myEntity, 
	Quaternion.fromEulerDegrees(0, 0, 0), 
	Quaternion.fromEulerDegrees(0, 170, 0), 
	700
)
```

旋转 Tween 需要以下信息：

* `起始`：用于起始旋转的 Quaternion
* `结束`：用于结束旋转的 Quaternion
* `持续时间`：在两个位置之间移动所需的毫秒数

此外，还提供以下可选参数：

* `easingFunction`：要使用的缓动函数。参见 [非线性 Tween](#non-linear-tweens)

### 带枢轴点旋转

当旋转实体时，旋转总是相对于实体的中心坐标。要使用另一组坐标作为枢轴点来旋转实体，请创建第二个（不可见）实体，将枢轴点作为其位置，并让它成为你要旋转的实体的父级。

当旋转父实体时，它的子实体也会都围绕父实体的位置旋转。注意， `位置` 子实体的位置是相对于父实体的位置而言的。

```ts
const pivotEntity = engine.addEntity()
Transform.create(pivotEntity, {
	position: Vector3.create(4, 1, 4),
})

const childEntity = engine.addEntity()
Transform.create(childEntity, {
	position: Vector3.create(1, 0, 0),
	parent: pivotEntity,
})
MeshRenderer.setBox(childEntity)

Tween.setRotate(pivotEntity, 
	Quaternion.fromEulerDegrees(0, 0, 0), 
	Quaternion.fromEulerDegrees(0, 170, 0), 
	700
)
```

注意，在这个示例中，系统正在旋转 `pivotEntity` 实体，它是 `childEntity` 实体，从玩家摄像机位置向前追踪一条射线。

## 在两个尺寸之间缩放

要在两个尺寸之间更改实体的缩放，请创建一个 `补间` 组件，并使用 `setScale` 函数。

```ts
const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)

Tween.setScale(myEntity, 
	Vector3.create(1, 1, 1), 
	Vector3.create(4, 4, 4), 
	2000
)	

```

缩放 Tween 需要以下信息：

* `起始`：用于起始尺寸的 Vector3
* `结束`：用于结束尺寸的 Vector3
* `持续时间`：在两个位置之间移动所需的毫秒数

此外，还提供以下可选参数：

* `easingFunction`：要使用的缓动函数。参见 [非线性 Tween](#non-linear-tweens)

## 从当前位置移动

当 Tween 处于活动状态时，引擎会实时更新 `Transform` 实体的组件值。这意味着你可以随时从实体的 `Transform` 组件中读取实体当前的位置，并将该值作为新 Tween 的 `起始` 。然后实体会从它当前所在的位置移动到目标位置，不管它是如何到达那里的。

```ts
pointerEventsSystem.onPointerDown(
	{
		entity: myEntity,
		opts: { button: InputAction.IA_POINTER, hoverText: 'Move' },
	},
	() => {
		Tween.setMove(myEntity, 
			Transform.get(myEntity).position, 
			Vector3.create(8, 1, 8), 
			2000,
			EasingFunction.EF_EASEOUTQUAD
		)
	}
)
```

{% hint style="warning" %}
**📔 注意**：始终显式传入 `起始` 值。如果 `起始` 参数未设置，则会被视为 *(0, 0, 0)*，因此实体会先瞬移到场景原点，然后再移动。同样，如果你传入一个硬编码的 `起始` 与实体当前的位置不匹配，实体会先明显瞬移到该位置，然后再移动。请在实体的 `Transform` 组件中读取当前位置，就像本节示例中那样，只要你希望移动从实体当前所在位置开始。
{% endhint %}

### 旅行中重新设定目标

调用 `setMove` (或其他任何 tween 函数) 在已有运行中 tween 的实体上，会用新的 tween 替换正在运行的 tween。由于 `起始` 是从实体的实时位置读取的，这会在移动途中平滑地重新定向实体：它会从当前时刻所在的位置改变路线，不会有任何跳变。

下面的示例会将实体移动到玩家点击的任意平台。如果玩家在实体仍在移动时点击另一个平台，实体会平滑地改变方向前往新目标。

```ts
function moveTo(target: Vector3) {
	Tween.setMove(shipEntity, 
		Transform.get(shipEntity).position, 
		target, 
		2000,
		EasingFunction.EF_EASEOUTQUAD
	)
}

pointerEventsSystem.onPointerDown(
	{
		entity: padA,
		opts: { button: InputAction.IA_POINTER, hoverText: 'Move here' },
	},
	() => moveTo(Vector3.create(2, 1, 2))
)

pointerEventsSystem.onPointerDown(
	{
		entity: padB,
		opts: { button: InputAction.IA_POINTER, hoverText: 'Move here' },
	},
	() => moveTo(Vector3.create(14, 1, 14))
)
```

要在实体到达目的地时运行逻辑，请参见 [Tween 完成时](#on-tween-finished).

{% hint style="warning" %}
**📔 注意**：仅在偶发事件时这样重新设定目标，例如点击或到达某个航点时。如果目标持续变化，例如实体跟随玩家，不要每帧都创建新的 tween：实体会明显抖动。参见 [跟随移动目标](#follow-a-moving-target).
{% endhint %}

## 非线性 Tween

补间可以遵循不同的 **缓动函数** ，这些会影响随时间变化的速率。 **线性的** 函数，意味着变化速度从开始到结束都是恒定的。可供选择的选项很多，它们会绘制出不同形状的曲线，取决于开头和/或结尾是否缓慢开始，以及缓慢到什么程度。有一个 **easeinexpo** 曲线开始时慢，结束时快，以指数方式加速；相反， **easeoutexpo** 曲线开始时快，结束时慢。

{% hint style="info" %}
**💡 提示**：尝试不同的运动曲线。差异通常很细微，但我们会潜意识地从事物的运动方式中解读信息，例如重量、摩擦，甚至个性。
{% endhint %}

```ts
Tween.setScale(myEntity, 
	Vector3.create(1, 1, 1), 
	Vector3.create(4, 4, 4), 
	2000,
	EasingFunction.EF_EASEOUTBOUNCE
)

```

可选的 `easingFunction` 参数取值来自 `EasingFunction` 枚举，提供以下选项：

* `EF_EASEBACK`
* `EF_EASEBOUNCE`
* `EF_EASECIRC`
* `EF_EASECUBIC`
* `EF_EASEELASTIC`
* `EF_EASEEXPO`
* `EF_EASEINBACK`
* `EF_EASEINBOUNCE`
* `EF_EASEINCIRC`
* `EF_EASEINCUBIC`
* `EF_EASEINELASTIC`
* `EF_EASEINEXPO`
* `EF_EASEINQUAD`
* `EF_EASEINQUART`
* `EF_EASEINQUINT`
* `EF_EASEINSINE`
* `EF_EASEOUTBACK`
* `EF_EASEOUTBOUNCE`
* `EF_EASEOUTCIRC`
* `EF_EASEOUTCUBIC`
* `EF_EASEOUTELASTIC`
* `EF_EASEOUTEXPO`
* `EF_EASEOUTQUAD`
* `EF_EASEOUTQUART`
* `EF_EASEOUTQUINT`
* `EF_EASEOUTSINE`
* `EF_EASEQUAD`
* `EF_EASEQUART`
* `EF_EASEQUINT`
* `EF_EASESINE`
* `EF_LINEAR`

## 恒定旋转

要让实体持续旋转，请使用 `补间` 组件，并使用 `setRotateContinuous` 函数。

```ts
Tween.setRotateContinuous(myEntity, 
	Quaternion.fromEulerDegrees(0, -1, 0), 
	45
)
```

持续旋转 Tween 需要以下信息：

* `实体`：要旋转的实体
* `方向`：用于确定旋转轴的 Quaternion。只有该旋转的轴向重要，其角度大小会被忽略。例如， `Quaternion.fromEulerDegrees(0, -1, 0)` 和 `Quaternion.fromEulerDegrees(0, -90, 0)` 效果相同，都会围绕 *y* 轴向负方向旋转。
* `速度`：实体每秒旋转多少度。负值会朝相反方向旋转。

此外，还提供以下可选参数：

* `持续时间`：维持旋转多久的毫秒数。此时间过后，旋转会停止。

## 恒定移动

要让实体沿相同方向持续移动，请使用 `补间` 组件，并使用 `setMoveContinuous` 函数。

```ts
Tween.setMoveContinuous(myEntity, 
	Vector3.create(0, 0, 1), 
	0.7
)
```

持续移动 Tween 需要以下信息：

* `实体`：要移动的实体
* `方向`：用于移动的 Vector3
* `速度`：实体每秒移动多少米

此外，还提供以下可选参数：

* `持续时间`：维持移动多久的毫秒数。此时间过后，移动会停止。

持续移动 Tween 需要以下信息：

### 跟随移动目标

`setMoveContinuous` 当目标不断变化时，这也是正确的工具，例如实体跟随玩家时。人们很容易想改为每帧创建一个新的 `setMove` tween，目标指向玩家的最新位置，但这会造成明显卡顿。

原因是你从 `Transform` 组件中读取的位置，是引擎最后一次回传给场景的位置，因此它会比实体真实所在的位置落后几帧。当你把这个值作为新 tween 的 `起始` 时，引擎会立即应用它，而实体会跳回几帧前所在的位置。一次这样的修正几乎察觉不到，但如果每秒重复很多次就会很明显。

`setMoveContinuous` 则可以完全避免这个问题，因为你提供的是方向和速度，而不是起始点。引擎会持续使用实体的真实位置，因此通过替换 tween 来改变方向时，实体不会跳跃。

```ts
const CHASE_SPEED = 3
const STOP_DISTANCE = 1

engine.addSystem(() => {
	const playerPosition = Transform.get(engine.PlayerEntity).position
	const myPosition = Transform.get(myEntity).position

	if (Vector3.distance(myPosition, playerPosition) <= STOP_DISTANCE) {
		// 足够接近：持续 tween 没有目的地，因此需要你自己停止它
		if (Tween.has(myEntity)) {
			Tween.deleteFrom(myEntity)
		}
		return
	}

	const direction = Vector3.subtract(playerPosition, myPosition)
	direction.y = 0 // 即使玩家跳跃，也保持在地面上

	Tween.setMoveContinuous(myEntity, Vector3.normalize(direction), CHASE_SPEED)
})
```

{% hint style="warning" %}
**📔 注意**：持续 tween 不会自行结束，因此实体会一直移动，直到你移除 `补间` 组件，使用 `Tween.deleteFrom()`，或者直到可选的 `持续时间` 耗尽为止。另外要注意，每一帧都重新瞄准会导致每帧向引擎发送一次更新。为避免这一点，只在 `setMoveContinuous` 方向变化到足够重要时再调用一次。
{% endhint %}

## Tween 序列

要让实体按顺序播放一系列 tween，请使用 `TweenSequence` 组件。该组件需要两个字段：

* `sequence`：一个包含多个 tween 定义的数组，它们将按顺序执行。数组可以为空，在这种情况下它只播放当前 tween。
* `loop` *（可选）*：如果未提供，序列只播放一次。如果该字段存在，则其值必须为 `TweenLoop` 枚举中的一个值。可接受的值有：
  * `TL_RESTART`：当序列结束时，它会重新开始。如果最后一个状态与第一个状态不匹配，实体会立刻从一个跳到另一个。
  * `TL_YOYO`：当序列结束时，它会反向执行，所有 tween 逆向播放，直到再次回到起点。然后它会再次开始。

### 来回移动

要让平台在两个位置之间持续来回移动，请将 `sequence` 数组留空，并设置 `loop` 为 `TweenLoop.TL_YOYO`

```ts
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
)

TweenSequence.create(myEntity, { sequence: [], loop: TweenLoop.TL_YOYO })
```

实体会在起点和终点之间来回移动，两个方向上的持续时间和缓动函数都相同。

### 沿路径跟随

要让实体沿包含多个点的更复杂路径移动，请在 `sequence` 中的 `TweenSequence` 组件。

```ts
const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)

Tween.setMove(myEntity, 
	Vector3.create(6.5, 7, 4), 
	Vector3.create(6.5, 7, 12), 
	4000
)

TweenSequence.create(myEntity, {
	sequence: [
		{
			duration: 2000,
			easingFunction: EasingFunction.EF_LINEAR,
			mode: Tween.Mode.Move({
				start: Vector3.create(6.5, 7, 12),
				end: Vector3.create(6.5, 10.5, 12),
			}),
		},
		{
			duration: 3000,
			easingFunction: EasingFunction.EF_LINEAR,
			mode: Tween.Mode.Move({
				start: Vector3.create(6.5, 10.5, 12),
				end: Vector3.create(6.5, 10.5, 4),
			}),
		},
		{
			duration: 3000,
			easingFunction: EasingFunction.EF_LINEAR,
			mode: Tween.Mode.Move({
				start: Vector3.create(6.5, 10.5, 4),
				end: Vector3.create(6.5, 7, 4),
			}),
		},
	],
	loop: TweenLoop.TL_RESTART,
})
```

注意，在 TweenSequence 中定义 tween 时，你需要使用更详细的 `Tween.Mode.Move`，或 `Tween.Mode.Rotate`，或 `Tween.Mode.Scale` 格式来定义 tween。在这种更详细的格式中，你需要指定：

* `持续时间`：在两个位置之间移动所需的毫秒数
* `easingFunction`：要使用的缓动函数。参见 [非线性 Tween](#non-linear-tweens)。在这种格式中，该值是必需的。
* `mode`：tween 的模式，可以是 `Tween.Mode.Move`, `Tween.Mode.Rotate`，或 `Tween.Mode.Scale`.

而在 `mode` 字段中，你需要指定：

* `起始`：tween 的起始值
* `结束`：tween 的结束值

## Tween 完成时

使用 `tweenSystem.tweenCompleted` 来检测某个 tween 何时结束。这对于在 tween 结束时执行操作很有用，例如打开电梯门。

```ts
engine.addSystem(() => {
	const tweenCompleted = tweenSystem.tweenCompleted(myEntity)
	if (tweenCompleted) {
		//播放声音
	}
})
```

## 同时 Tween

要让实体在初始状态和结束状态之间移动、旋转和缩放，请创建一个 `补间` 组件，并使用 `setMoveRotateScale` 函数。该函数也可用于这三个参数的任意组合。

```ts
const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(14, 1, 2),
})
MeshRenderer.setBox(myEntity)

Tween.setMoveRotateScale(myEntity, {
	position: { start: Vector3.create(14, 1, 2), end: Vector3.create(14, 3, 2) },
	rotation: { start: Quaternion.fromEulerDegrees(0, 0, 0), end: Quaternion.fromEulerDegrees(0, 180, 90) },
	scale: { start: Vector3.One(), end: Vector3.create(2, 0.5, 2) },
	duration: 2000
})
```

移动 Tween 需要以下信息：

* `实体`：要移动的实体
* `params`：一个包含以下参数的对象
  * `位置`: *（可选）* 一个带有 `起始` 和 `结束` 值的对象，既作为 `Vector3`.
  * `旋转`: *（可选）* 一个带有 `起始` 和 `结束` 值的对象，既作为 `Quaternion`.
  * `缩放`: *（可选）* 一个带有 `起始` 和 `结束` 值的对象，既作为 `Vector3`。至少需要提供 `位置`, `旋转` 或 `缩放` 中的一个。
  * `持续时间`: *（必需）* 在两组值之间过渡需要多少毫秒
  * `easingFunction`: *（可选）* 要使用的缓动函数。参见 [非线性 Tween](#non-linear-tweens)

一个实体只能有一个 `补间` 组件，而每个 tween 组件一次只能执行一种变换。通过 `setMoveRotateScale` tween 类型，你可以让实体横向移动并同时旋转，但这两种运动会遵循同一时间线。如果你需要彼此独立的过渡，可以作为变通方案使用父子实体。例如，你可以让一个不可见的父实体横向移动，同时让一个可见的子实体旋转。

在下面的代码片段中，一个父实体在旋转，而一个子实体在缩放。

```ts
const parentEntity = engine.addEntity()
Transform.create(parentEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(parentEntity)
Tween.setRotate(parentEntity, 
	Quaternion.fromEulerDegrees(0, 0, 0), 
	Quaternion.fromEulerDegrees(0, 170, 0), 
	5000
)

const childEntity = engine.addEntity()
Transform.create(childEntity, {
	position: Vector3.create(0, 0, 0),
	parent: parentEntity,
})
MeshRenderer.setBox(childEntity)
Tween.setScale(childEntity, 
	Vector3.create(1, 1, 1), 
	Vector3.create(4, 4, 4), 
	2000
)
```

## 暂停 tween

要暂停 tween，请将 `playing` 属性改为 false。要恢复它，再把它改回 true。

```ts
pointerEventsSystem.onPointerDown(
	{
		entity: button,
		opts: { button: InputAction.IA_POINTER, hoverText: 'pause' },
	},
	() => {
		let tweenData = Tween.getMutable(myEntity)
		tweenData.playing = !tweenData.playing
	}
)
```

要结束一个不需要继续的 tween，请删除实体上的 `补间` 组件。如果该实体还在使用 `TweenSequence` 组件，也一起删除它。

```ts
pointerEventsSystem.onPointerDown(
	{
		entity: button,
		opts: { button: InputAction.IA_POINTER, hoverText: 'pause' },
	},
	() => {
		if( Tween.has(myEntity)){
			Tween.deleteFrom(myEntity)
		}
		if( TweenSequence.has(myEntity)){
			TweenSequence.deleteFrom(myEntity)
		}
	}
)
```

## 基于系统的 Tween

与其使用 Tween 组件并让引擎处理变换，你也许更愿意通过 [系统](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/systems.md) 在场景中逐帧、增量地完成这个过渡。每次函数运行时都让实体移动一小段。

一方面，这让你在每一帧重新计算运动时拥有更强的控制力。另一方面，代码更复杂，而且性能较差的机器上的玩家可能会觉得 tween 有卡顿感，能察觉到每次增量。

### 通过系统移动

移动实体最简单的方法是逐步修改 *位置* 中存储的值 `Transform` 组件。

```ts
function SimpleMove() {
	let transform = Transform.getMutable(myEntity)
	transform.position = Vector3.add(
		transform.position,
		Vector3.scale(Vector3.Forward(), 0.05)
	)
}

engine.addSystem(SimpleMove)

const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)
```

在这个示例中，我们让实体在游戏循环的每个 tick 中移动 0.05 米。

`Vector3.Forward()` 会返回一个朝前、长度为 1 米的向量。在本例中，我们再使用 `Vector3.scale()`将这个向量缩小到其长度的 1/20。如果我们的场景每秒有 30 帧，那么该实体的移动速度就是每秒 1.5 米。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-7088aee0c804d1902813dd2d5cd4a999e85a5cf8%2Fmove.gif?alt=media\&token=22d02b05-a757-47f7-a2a8-20239219c328)

### 通过系统旋转

让实体旋转的最简单方法，是逐步增量地更改 Transform 组件中的值，并将其作为系统函数的一部分来运行。

```ts
function SimpleRotate() {
	let transform = Transform.getMutable(myEntity)
	transform.rotation = Quaternion.multiply(
		transform.rotation,
		Quaternion.fromAngleAxis(1, Vector3.Up())
	)
}

engine.addSystem(SimpleRotate)

const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)
```

注意，为了将当前旋转与每次增量组合起来，我们使用 `Quaternion.multiply`。在四元数数学中，你是通过相乘而不是相加来组合两个旋转的。一个四元数乘以另一个四元数所得到的旋转，将等同于先执行一个旋转再执行另一个旋转后的最终旋转。

在这个示例中，我们会在游戏循环的每个 tick 中，让实体向上旋转 1 度。

{% hint style="info" %}
**💡 提示**：如果要让一个实体始终旋转并朝向玩家，你可以添加一个 [`广告牌` 组件](/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/entity-positioning.md#face-the-user).
{% endhint %}

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-cadf115f527e7bd6bda804cd4051adedca5f40ef%2Frotate.gif?alt=media\&token=1d4332ad-29a1-4977-a13d-1c4e1777aa71)

### 通过系统绕枢轴点旋转

当旋转实体时，旋转总是相对于实体的中心坐标。要使用另一组坐标作为枢轴点来旋转实体，请创建第二个（不可见）实体，将枢轴点作为其位置，并让它成为你要旋转的实体的父级。

当旋转父实体时，它的子实体也会都围绕父实体的位置旋转。注意， `位置` 子实体的位置是相对于父实体的位置而言的。

```ts
function SimpleRotate() {
	let transform = Transform.getMutable(pivotEntity)
	transform.rotation = Quaternion.multiply(
		transform.rotation,
		Quaternion.fromAngleAxis(1, Vector3.Up())
	)
}

engine.addSystem(SimpleRotate)

const pivotEntity = engine.addEntity()
Transform.create(pivotEntity, {
	position: Vector3.create(4, 1, 4),
})

const childEntity = engine.addEntity()
Transform.create(childEntity, {
	position: Vector3.create(1, 0, 0),
	parent: pivotEntity,
})
MeshRenderer.setBox(childEntity)
```

注意，在这个示例中，系统正在旋转 `pivotEntity` 实体，它是 `childEntity` 实体，从玩家摄像机位置向前追踪一条射线。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-9d8e4fb2bbc8007ef87c4918cc2b44f3c0cd4a54%2Fpivot-rotate.gif?alt=media\&token=a22ecc83-1398-4936-88ff-fe29799e00b4)

### 调整移动以延迟时间

假设访问你场景的玩家难以跟上帧率的节奏。这可能会导致移动看起来跳动，因为并非所有帧的时间间隔都相同，但每一帧都让实体移动相同的距离。

你可以使用 `dt` 参数来调整移动的缩放比例，以补偿这种不均匀的时间间隔。

```ts
function SimpleMove(dt: number) {
	let transform = Transform.getMutable(myEntity)
	transform.position = Vector3.add(
		transform.position,
		Vector3.scale(Vector3.Forward(), dt)
	)
}

engine.addSystem(SimpleMove)

const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})
MeshRenderer.setBox(myEntity)
```

上面的示例会让移动速度与前面的移动示例大致相同，即使帧率下降也是如此。当以每秒 30 帧运行时， `dt` 的值是 1/30。

你也可以通过将旋转量乘以 `dt`.

### 通过系统在两个点之间移动

如果你想让实体在两个点之间平滑移动，请使用 *lerp* （线性插值）算法。这个算法在游戏开发中非常有名，因为它非常实用。

该 `lerp()` 函数接受三个参数：

* 表示起始位置的向量
* 表示目标位置的向量
* amount，一个从 0 到 1 的值，表示要进行多少比例的位移。

```ts
const originVector = Vector3.Zero()
const targetVector = Vector3.Forward()

let newPos = Vector3.lerp(originVector, targetVector, 0.6)
```

线性插值算法会在两条向量之间的路径中找到一个与所提供比例相匹配的中间点。

例如，如果起始向量是 *(0, 0, 0)* 而目标向量是 *(10, 0, 10)*:

* 使用 0 的比例会返回 *(0, 0, 0)*
* 使用 0.3 的比例会返回 *(3, 0, 3)*
* 使用 1 的比例会返回 *(10, 0, 10)*

要在 `lerp()` 你的场景中实现这一点，我们建议创建一个 [自定义组件](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/custom-components.md) 来存储所需信息。你还需要定义一个系统，用于在每一帧中实现渐进式移动。

```ts
// 定义自定义组件
const MoveTransportData = {
	start: Schemas.Vector3,
	end: Schemas.Vector3,
	fraction: Schemas.Float,
	speed: Schemas.Float,
}

export const LerpTransformComponent = engine.defineComponent(
	'LerpTransformComponent',
	MoveTransportData
)

// 定义系统
function LerpMove(dt: number) {
	let transform = Transform.getMutable(myEntity)
	let lerp = LerpTransformComponent.getMutable(myEntity)
	if (lerp.fraction < 1) {
		lerp.fraction += dt * lerp.speed
		transform.position = Vector3.lerp(lerp.start, lerp.end, lerp.fraction)
	}
}

engine.addSystem(LerpMove)

// 创建实体
const myEntity = engine.addEntity()

Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})

MeshRenderer.setBox(myEntity)

LerpTransformComponent.create(myEntity, {
	start: Vector3.create(4, 1, 4),
	end: Vector3.create(8, 1, 8),
	fraction: 0,
	speed: 1,
})
```

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-821f48b2d4a6afbb451813d7df73aa4196228c0f%2Flerp-move.gif?alt=media\&token=648743af-b3c3-4503-9c9c-c81670b839fe)

### 通过系统在两个角度之间旋转

如果要在两个角度之间平滑旋转，请使用 *slerp* (*球面* 线性插值）算法。这个算法与一个 *lerp*非常相似，但它处理的是四元数旋转。

该 `slerp()` 函数接受三个参数：

* 该 [四元数](https://en.wikipedia.org/wiki/Quaternion) 起始旋转的角度
* 该 [四元数](https://en.wikipedia.org/wiki/Quaternion) 目标旋转的角度
* amount，一个从 0 到 1 的值，表示要进行多少比例的位移。

{% hint style="info" %}
**💡 提示**：你可以使用 [欧拉](https://en.wikipedia.org/wiki/Euler_angles) 角度（0 到 360）来传入旋转值，使用 `Quaternion.fromEulerDegrees()`.
{% endhint %}

```ts
const originRotation = Quaternion.fromEulerDegrees(0, 90, 0)
const targetRotation = Quaternion.fromEulerDegrees(0, 0, 0)

let newRotation = Quaternion.slerp(originRotation, targetRotation, 0.6)
```

要在你的场景中实现这一点，我们建议将传入 `Slerp()` 函数的数据存储在一个 [自定义组件](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/custom-components.md)中。你还需要定义一个系统，用于在每一帧中实现渐进式旋转。

```ts
// 定义自定义组件
const RotateSlerpData = {
	start: Schemas.Quaternion,
	end: Schemas.Quaternion,
	fraction: Schemas.Float,
	speed: Schemas.Float,
}

export const SlerpData = engine.defineComponent('SlerpData', RotateSlerpData)

// 定义系统
function SlerpRotate(dt: number) {
	let transform = Transform.getMutable(myEntity)
	let slerpData = SlerpData.getMutable(myEntity)
	if (slerpData.fraction < 1) {
		slerpData.fraction += dt * slerpData.speed
		transform.rotation = Quaternion.slerp(
			slerpData.start,
			slerpData.end,
			slerpData.fraction
		)
	}
}

engine.addSystem(SlerpRotate)

// 创建实体
const myEntity = engine.addEntity()

Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
})

MeshRenderer.setBox(myEntity)

SlerpData.create(myEntity, {
	start: Quaternion.fromEulerDegrees(0, 0, 0),
	end: Quaternion.fromEulerDegrees(0, 180, 0),
	fraction: 0,
	speed: 0.3,
})
```

{% hint style="warning" %}
**📔 注意**：你也可以改用欧拉角值来表示旋转，并使用一个 `Vector3` 值，然后使用 `Lerp()` 函数，但这意味着每一帧都要从 `Vector3` 为 `Quaternion` 进行转换。旋转值在内部以四元数形式存储于 `Transform` 组件中，因此在场景中使用四元数会更高效。
{% endhint %}

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-eb8b524701a7b454c514384eb70c3f48ac0a70b5%2Flerp-rotate.gif?alt=media\&token=cfab88d5-ac0d-4cc1-aa75-c866ec8a8a80)

一种更简单但效率较低的方法是利用 `Quaternion.rotateTowards` 函数，并且避免使用任何自定义组件。

```ts
function SimpleRotate(dt: number) {
	let transform = Transform.getMutable(myEntity)
	transform.rotation = Quaternion.rotateTowards(
		transform.rotation,
		Quaternion.fromEulerDegrees(90, 0, 0),
		dt * 10
	)
	if (
		Quaternion.angle(transform.rotation, Quaternion.fromEulerDegrees(90, 0, 0)) < 0.01
	) {
		console.log('done')
		engine.removeSystem(SimpleRotate)
	}
}

engine.addSystem(SimpleRotate)

const myEntity = engine.addEntity()
Transform.create(myEntity, {
	position: Vector3.create(4, 1, 4),
	rotation: Quaternion.fromEulerDegrees(0, 0, 90),
})

MeshRenderer.setBox(myEntity)
```

在上面的示例中 `Quaternion.rotateTowards` 接受三个参数：初始旋转、所需的最终旋转，以及每帧允许的最大增量。在这种情况下，由于最大增量为 `dt * 10` 度，旋转将会在大约 9 秒的时间内完成。

请注意，该系统还会检查旋转是否完成，如果完成就会将系统从引擎中移除。否则，即使旋转已经完成，系统也会在每一帧持续进行计算。

### 通过系统在两种尺寸之间改变缩放

如果你想让实体平滑地改变大小而不改变其比例，请使用 *lerp* （线性插值）算法的 `Scalar` 对象。

否则，如果你想以不同的比例改变各个轴，请使用 `Vector3` 来表示起始缩放和目标缩放，然后使用 *lerp* 的函数 `Vector3`.

该 `lerp()` 的函数 `Scalar` 对象接受三个参数：

* 起始缩放的数值
* 目标缩放的数值
* amount，一个从 0 到 1 的值，表示要进行多少比例的缩放。

```ts
const originScale = 1
const targetScale = 10

let newScale = Scalar.lerp(originScale, targetScale, 0.6)
```

要在你的场景中实现这个 lerp，我们建议创建一个自定义组件来存储所需信息。你还需要定义一个系统，用于在每一帧中实现渐进式缩放。

```ts
// 定义自定义组件
const ScaleTransportData = {
	start: Schemas.Number,
	end: Schemas.Number,
	fraction: Schemas.Float,
	speed: Schemas.Float,
}

export const ScaleTransformComponent = engine.defineComponent(
	'ScaleTransformComponent',
	ScaleTransportData
)

// 定义系统
function LerpMove(dt: number) {
	let transform = Transform.getMutable(myEntity)
	let lerp = ScaleTransformComponent.getMutable(myEntity)
	if (lerp.fraction < 1) {
		lerp.fraction += dt * lerp.speed
		const newScale = Scalar.lerp(lerp.start, lerp.end, lerp.fraction)
		transform.scale = Vector3.create(newScale, newScale, newScale)
	}
}

engine.addSystem(LerpMove)

// 创建实体
const myEntity = engine.addEntity()

Transform.create(myEntity, {
	position: { x: 4, y: 1, z: 4 },
})

MeshRenderer.setBox(myEntity)

ScaleTransformComponent.create(myEntity, {
	start: 1,
	end: 2,
	fraction: 0,
	speed: 1,
})

Vector3.create(1, 1, 1)
```

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-860402e1bd825057ab2b47d06e95ba6061dc5327%2Flerp-scale.gif?alt=media\&token=094e5b91-4fab-4223-941c-c2107ad96c5d)

### 通过系统在两个点之间以不规则速度移动

在使用 lerp 方法时，你可以让移动速度变为非线性。在前面的示例中，我们在每一帧中将 lerp 数值增加固定 مقدار，但我们也可以使用数学函数让数值呈指数增长，或采用其他方式来赋予不同的移动节奏。

你也可以使用会产生周期性结果的函数，比如正弦函数，来描述一种来回往复的移动。

通常，这些非线性过渡能为场景注入大量生机。沿曲线加速或逐渐减速的移动，能够很好地说明一个物体或角色的性质。你甚至可以利用会产生弹跳效果的数学函数。

```ts
// 定义自定义组件
const MoveTransportData = {
	start: Schemas.Vector3,
	end: Schemas.Vector3,
	fraction: Schemas.Float,
	speed: Schemas.Float,
}

export const LerpTransformComponent = engine.defineComponent(
	'LerpTransformComponent',
	MoveTransportData
)

// 定义系统
function LerpMove(dt: number) {
	let transform = Transform.getMutable(myEntity)
	let lerp = LerpTransformComponent.getMutable(myEntity)
	if (lerp.fraction < 1) {
		lerp.fraction += dt * lerp.speed
		const interpolatedValue = interpolate(lerp.fraction)
		transform.position = Vector3.lerp(lerp.start, lerp.end, interpolatedValue)
	}
}

// 将 lerp 比例映射到指数曲线
function interpolate(t: number) {
	return t * t
}

engine.addSystem(LerpMove)

// 创建实体
const myEntity = engine.addEntity()

Transform.create(myEntity, {
	position: { x: 4, y: 1, z: 4 },
})

MeshRenderer.setBox(myEntity)

LerpTransformComponent.create(myEntity, {
	start: Vector3.create(4, 1, 4),
	end: Vector3.create(8, 1, 8),
	fraction: 0,
	speed: 1,
})
```

上面的示例与我们之前展示的线性 lerp 示例非常类似，但 `fraction` 字段在每个 tick 中都会映射到一个非线性值。这个非线性值用于计算 `lerp` 函数，从而产生沿指数曲线运动的效果。

你也可以像上面那样，通过将线性过渡映射到曲线来以同样方式处理旋转或缩放的过渡。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-91fe24f5e5561834eea21e052ee0036a2987d747%2Flerp-speed-up.gif?alt=media\&token=3ba54949-0936-4dc5-a55d-9feebbbff2ca)

### 通过系统沿路径移动

你可以让一个实体在一组向量数组上循环，在每个向量之间执行 lerp 移动，从而沿更复杂的路径行进。

```ts
// 定义自定义组件
const PathTransportData = {
	path: Schemas.Array(Schemas.Vector3),
	start: Schemas.Vector3,
	end: Schemas.Vector3,
	fraction: Schemas.Float,
	speed: Schemas.Float,
	pathTargetIndex: Schemas.Int,
}

export const LerpTransformComponent = engine.defineComponent(
	'LerpTransformComponent',
	PathTransportData
)

// 定义系统
function PathMove(dt: number) {
	let transform = Transform.getMutable(myEntity)
	let lerp = LerpTransformComponent.getMutable(myEntity)
	if (lerp.fraction < 1) {
		lerp.fraction += dt * lerp.speed
		transform.position = Vector3.lerp(lerp.start, lerp.end, lerp.fraction)
	} else {
		lerp.pathTargetIndex += 1
		if (lerp.pathTargetIndex >= lerp.path.length) {
			lerp.pathTargetIndex = 0
		}
		lerp.start = lerp.end
		lerp.end = lerp.path[lerp.pathTargetIndex]
		lerp.fraction = 0
	}
}

engine.addSystem(PathMove)

// 创建实体
const myEntity = engine.addEntity()

Transform.create(myEntity, {
	position: Vector3.create(1, 1, 1),
})

MeshRenderer.setBox(myEntity)

const point1 = Vector3.create(1, 1, 1)
const point2 = Vector3.create(8, 1, 3)
const point3 = Vector3.create(8, 4, 7)
const point4 = Vector3.create(1, 1, 7)

const myPath = [point1, point2, point3, point4]

LerpTransformComponent.create(myEntity, {
	path: myPath,
	start: Vector3.create(4, 1, 4),
	end: Vector3.create(8, 1, 8),
	fraction: 0,
	speed: 1,
	pathTargetIndex: 1,
})
```

上面的示例定义了一条由四个 3D 向量组成的 3D 路径。 `PathTransportData` 自定义组件保存的数据与上面示例中的自定义组件所使用的数据相同，但增加了一个 *lerp* path `path` 数组，其中包含路径中的所有点，以及一个 `pathTargetIndex` 字段，用于跟踪当前正在使用路径的哪个分段。

这个系统与 *lerp* 示例中的系统非常相似，但当一个 lerp 动作完成时，它会设置 `目标` 和 `origin` 字段为新值。如果到达路径末尾，我们就返回到路径中的第一个值。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-5ca6c0f3c12c8838c80b848e718d463c4e5fa441%2Flerp-path.gif?alt=media\&token=ab11f133-f26e-40a6-b7a9-740c70c05cd7)

## 纹理补间

要让纹理平滑滑动，请使用 `补间` 组件，并使用 `setTextureMove` 函数。

```ts
Tween.setTextureMove(myEntity, 
	Vector2.create(0, 0), 
	Vector2.create(1, 0), 
	2000
)
```

纹理补间接受以下信息：

* `实体`：要移动其纹理的实体
* `起始`：起始位置的 Vector2
* `结束`：结束位置的 Vector2
* `持续时间`：在两个位置之间移动所需的毫秒数

此外，还提供以下可选参数：

* `movementType`：（可选），定义移动是在 offset 还是 tiling 字段上。默认使用 offset。
* `easingFunction`：要使用的缓动函数。参见 [非线性 Tween](#non-linear-tweens).

## 恒定纹理移动

要让纹理持续滑动，请使用 `补间` 组件，并使用 `setTextureMoveContinuous` 函数。

```ts
Tween.setTextureMoveContinuous(myEntity, 
	Vector2.create(0, 1), 
	0.7
)
```

连续纹理补间接受以下信息：

* `实体`：要移动其纹理的实体
* `方向`：用于移动的 Vector2
* `速度`：实体每秒移动多少单位

此外，还提供以下可选参数：

* `movementType`：定义移动是在 offset 还是 tiling 字段上。默认使用 offset。
* `持续时间`：维持移动多久的毫秒数。此时间过后，移动会停止。

在以下内容中了解更多有关纹理补间的信息： [Texture Tweens](/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/materials.md#texture-tweens) 部分。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/3d-nei-rong-ji-chu/move-entities.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
