> 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/kai-shi-shi-yong/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
)
```

移动补间需要以下信息：

* `实体`：要移动的实体
* `start`：表示起始位置的 Vector3
* `end`：表示结束位置的 Vector3
* `持续时间`：在两个位置之间移动需要多少毫秒

另外还有一个可选参数：

* `easingFunction`：要使用哪种缓动函数。参见 [非线性补间](#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
)
```

旋转补间需要以下信息：

* `start`：表示起始旋转的 Quaternion
* `end`：表示结束旋转的 Quaternion
* `持续时间`：在两个位置之间移动需要多少毫秒

另外还有一个可选参数：

* `easingFunction`：要使用哪种缓动函数。参见 [非线性补间](#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
)	

```

缩放补间需要以下信息：

* `start`：表示起始大小的 Vector3
* `end`：表示结束大小的 Vector3
* `持续时间`：在两个位置之间移动需要多少毫秒

另外还有一个可选参数：

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

## 从当前位置移动

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

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

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

### 在移动途中重新指定目标

调用 `setMove` （或其他任何补间函数）到一个已经在运行补间的实体上时，会用新的补间替换正在运行的补间。由于 `start` 是从实体的实时位置读取的，这会在移动途中平滑地重新定向实体：它会从那一刻所在的位置改变方向，不会有任何跳变。

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

```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: '移动到这里' },
	},
	() => moveTo(Vector3.create(2, 1, 2))
)

pointerEventsSystem.onPointerDown(
	{
		entity: padB,
		opts: { button: InputAction.IA_POINTER, hoverText: '移动到这里' },
	},
	() => moveTo(Vector3.create(14, 1, 14))
)
```

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

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

## 非线性补间

补间可以遵循不同的 **缓动函数** ，它们会影响随时间变化的速度。 **线性** 函数，表示变化速度从开始到结束保持恒定。有很多可选项可供选择，它们会绘制不同形状的曲线，取决于起始和/或结束是否缓慢开始，以及程度如何。一个 **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
)
```

持续旋转补间需要以下信息：

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

另外还有一个可选参数：

* `持续时间`：持续旋转多少毫秒。到这段时间后，旋转将停止。

## 恒定移动

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

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

持续移动补间需要以下信息：

* `实体`：要移动的实体
* `方向`：表示移动方向的 Vector3
* `速度`：实体每秒将移动多少米

另外还有一个可选参数：

* `持续时间`：持续移动多少毫秒。到这段时间后，移动将停止。

持续移动补间需要以下信息：

### 跟随移动目标

`setMoveContinuous` 当目的地不断变化时，这也是合适的工具，例如一个跟随玩家的实体。人们很容易想改为创建一个新的 `setMove` 补间，并在每一帧都对准玩家的最新位置，但那样会产生明显的卡顿。

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

`setMoveContinuous` 完全避免了这个问题，因为你传入的是方向和速度，而不是起点。引擎会一直使用实体的真实位置，因此通过替换补间来改变方向时，实体从不会跳跃。

```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) {
		// 距离足够近：连续补间没有目的地，所以你需要自己停止它
		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.deleteFrom()`，或直到可选的 `持续时间` 耗尽为止。另请注意，每一帧都重新对准会向引擎发送每帧更新。为避免这种情况，只在 `setMoveContinuous` 方向发生足够大的变化、确实有必要时再调用。
{% endhint %}

## 补间序列

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

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

### 来回移动

要让平台在两个位置之间持续来回移动，请将 `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.Mode.Move`，或 `Tween.Mode.Rotate`，或 `Tween.Mode.Scale` 格式来定义补间。在这种更详细的格式中，你需要指定：

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

并且在 `模式` 字段中，你需要指定：

* `start`：补间的起始值
* `end`：补间的结束值

## 补间完成时

使用 `tweenSystem.tweenCompleted` 来检测补间何时完成。这对于在补间结束时执行动作很有用，例如打开电梯门。

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

## 同时补间

要在初始状态和结束状态之间移动、旋转并缩放实体，请创建一个 `补间` 组件，并使用 `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
})
```

移动补间需要以下信息：

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

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

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

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

## 暂停补间

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

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

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

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

## 基于系统的补间

与其使用 Tween 组件并让引擎处理变换，你也可以选择在场景中的一个 [系统](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/systems.md) 系统里，以每帧为单位逐步完成这一过渡。通过每次函数运行时让实体移动一小段距离。

一方面，这让你在每一帧重新计算移动时拥有更多控制权。另一方面，代码会更复杂，而且性能较差设备上的玩家可能会觉得补间有卡顿，并注意到每一次增量。

### 通过系统移动

移动实体最简单的方法，是逐步修改 *位置* 中存储的值 `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)

### 通过系统旋转

旋转实体最简单的方法，是逐步增量地改变 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)

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

在旋转实体时，旋转始终以实体的中心坐标为参考。若要使用另一组坐标作为枢轴点来旋转实体，请创建第二个（不可见的）实体，把枢轴点作为它的位置，并将其设为你想旋转的实体的父实体。

当旋转父实体时，其所有子实体都会以父实体的位置作为枢轴点一起旋转。请注意， `位置` 子实体的位置是参照父实体的位置的。

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

### 根据延迟时间调整移动

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

你可以通过使用 `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()` 函数接受三个参数：

* 原始位置的向量
* 目标位置的向量
* 数量，一个从 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)

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

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

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

* 该 [四元数](https://en.wikipedia.org/wiki/Quaternion) 原始旋转的角度
* 该 [四元数](https://en.wikipedia.org/wiki/Quaternion) 目标旋转的角度
* 数量，一个从 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)

一种更简单但效率较低的方法是利用 `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` 对象接受三个参数：

* 原始缩放的数值
* 目标缩放的数值
* 数量，一个从 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)

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

使用 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)

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

你可以让实体循环遍历一个向量数组，在每两个向量之间执行 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,
})
```

上面的示例定义了一条由四个三维向量组成的三维路径。 `PathTransportData` 自定义组件包含与 *lerp* 上面示例中的自定义组件所使用的相同数据，但增加了一个 `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)

## 纹理补间

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

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

纹理补间接受以下信息：

* `实体`：要移动其纹理的实体
* `start`：表示起始位置的 Vector2
* `end`：表示结束位置的 Vector2
* `持续时间`：在两个位置之间移动需要多少毫秒

另外还有一个可选参数：

* `movementType`：（可选）定义移动作用于偏移字段还是平铺字段。默认使用偏移。
* `easingFunction`：要使用哪种缓动函数。参见 [非线性补间](#non-linear-tweens).

## 恒定纹理移动

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

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

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

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

另外还有一个可选参数：

* `movementType`：定义移动作用于偏移字段还是平铺字段。默认使用偏移。
* `持续时间`：持续移动多少毫秒。到这段时间后，移动将停止。

在以下内容中阅读有关纹理补间的更多信息： [纹理补间](/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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
