player-avatar

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Player and Avatar System in Decentraland

Decentraland 中的玩家与 Avatar 系统

CRITICAL: The player Transform is READ-ONLY from scene code

关键:玩家 Transform 从场景代码中是只读的

Transform
on
engine.PlayerEntity
is engine-controlled. Mutations from scene code are silently ignored — your code compiles, runs, no error is thrown, and nothing moves in-world. This is the most common bug when trying to lift, push, knock back, float, or teleport the player.
typescript
// WRONG — compiles cleanly, runs, does NOTHING in-world
const t = Transform.getMutable(engine.PlayerEntity)
t.position.y += 0.1                      // ignored
t.position = Vector3.create(8, 0, 8)     // ignored
Transform.createOrReplace(engine.PlayerEntity, { ... }) // ignored
Symptom to recognize: TypeScript accepts the code, the system ticks, no console error, but the avatar never moves. If you wrote
Transform...PlayerEntity
and expected motion, this is your bug.
Correct API by intent:
GoalUseSkill
Instant teleport / smooth slide to a point
movePlayerTo
from
~system/RestrictedActions
this skill, see below
Lift / float / launch / jump pad / knockback / push / wind / repulsion
Physics.*
from
@dcl/sdk/ecs
player-physics
Restrict / freeze movement
InputModifier
on
engine.PlayerEntity
advanced-input
Change run speed / jump height
AvatarLocomotionSettings
on
engine.PlayerEntity
this skill, see below
Transform.get(engine.PlayerEntity)
is valid for reading position and rotation only.
engine.PlayerEntity
上的
Transform
由引擎控制。来自场景代码的修改会被静默忽略——代码可以编译和运行,不会抛出错误,但场景中不会有任何东西移动。这是在尝试抬起、推动、击退、漂浮或传送玩家时最常见的错误。
typescript
// WRONG — compiles cleanly, runs, does NOTHING in-world
const t = Transform.getMutable(engine.PlayerEntity)
t.position.y += 0.1                      // ignored
t.position = Vector3.create(8, 0, 8)     // ignored
Transform.createOrReplace(engine.PlayerEntity, { ... }) // ignored
需要识别的症状: TypeScript 可以正常接受代码,系统也会持续运行,控制台没有错误,但 Avatar 永远不会移动。如果你编写了
Transform...PlayerEntity
并期望产生运动,问题就出在这里。
按目标选择正确的 API:
目标使用方式Skill
立即传送或平滑移动到某个点
~system/RestrictedActions
中的
movePlayerTo
本 Skill,见下文
抬起、漂浮、发射、跳跃板、击退、推动、风力或排斥力
@dcl/sdk/ecs
中的
Physics.*
player-physics
限制或冻结移动在
engine.PlayerEntity
上使用
InputModifier
advanced-input
更改奔跑速度或跳跃高度在
engine.PlayerEntity
上使用
AvatarLocomotionSettings
本 Skill,见下文
Transform.get(engine.PlayerEntity)
仅可用于读取位置和旋转。

Player Position and Movement (Reading)

玩家位置与移动(读取)

Access the player's position via the reserved
engine.PlayerEntity
:
typescript
import { engine, Transform } from '@dcl/sdk/ecs'

function trackPlayer() {
	if (!Transform.has(engine.PlayerEntity)) return

	const playerTransform = Transform.get(engine.PlayerEntity)
	console.log('Player position:', playerTransform.position)
	console.log('Player rotation:', playerTransform.rotation)
}

engine.addSystem(trackPlayer)
Always check
Transform.has(engine.PlayerEntity)
before reading player data — it may not be ready on the first frame.
通过保留实体
engine.PlayerEntity
访问玩家位置:
typescript
import { engine, Transform } from '@dcl/sdk/ecs'

function trackPlayer() {
	if (!Transform.has(engine.PlayerEntity)) return

	const playerTransform = Transform.get(engine.PlayerEntity)
	console.log('Player position:', playerTransform.position)
	console.log('Player rotation:', playerTransform.rotation)
}

engine.addSystem(trackPlayer)
读取玩家数据前始终检查
Transform.has(engine.PlayerEntity)
——在第一帧时它可能尚未准备就绪。

Distance-Based Logic

基于距离的逻辑

typescript
import { Vector3 } from '@dcl/sdk/math'

function proximityCheck() {
	const playerPos = Transform.get(engine.PlayerEntity).position
	const npcPos = Transform.get(npcEntity).position
	const distance = Vector3.distance(playerPos, npcPos)

	if (distance < 5) {
		console.log('Player is near the NPC')
	}
}

engine.addSystem(proximityCheck)
typescript
import { Vector3 } from '@dcl/sdk/math'

function proximityCheck() {
	const playerPos = Transform.get(engine.PlayerEntity).position
	const npcPos = Transform.get(npcEntity).position
	const distance = Vector3.distance(playerPos, npcPos)

	if (distance < 5) {
		console.log('Player is near the NPC')
	}
}

engine.addSystem(proximityCheck)

Player Profile Data

玩家个人资料数据

Get the player's name, wallet address, and guest status:
typescript
import { getPlayer } from '@dcl/sdk/src/players'

function main() {
	const player = getPlayer()
	if (player) {
		console.log('Name:', player.name)
		console.log('User ID:', player.userId)
		console.log('Is guest:', player.isGuest)
	}
}
  • userId
    — the player's Ethereum wallet address (or guest ID)
  • isGuest
    —
    true
    if the player hasn't connected a wallet
Check
isGuest
before attempting any wallet-dependent feature (guests have no on-chain identity).
获取玩家的名称、钱包地址和访客状态:
typescript
import { getPlayer } from '@dcl/sdk/src/players'

function main() {
	const player = getPlayer()
	if (player) {
		console.log('Name:', player.name)
		console.log('User ID:', player.userId)
		console.log('Is guest:', player.isGuest)
	}
}
  • userId
    ——玩家的 Ethereum 钱包地址(或访客 ID)
  • isGuest
    ——如果玩家尚未连接钱包,则为
    true
尝试使用任何依赖钱包的功能之前,请先检查
isGuest
(访客没有链上身份)。

Profile Data for Off-Scene Users (Catalyst)

场景外用户的个人资料数据(Catalyst)

getPlayer(userId)
only returns data for users currently connected to this scene. For any other address (parcel owner, NFT holder, leaderboard entry, off-scene claimant), fetch from the catalyst:
GET https://peer.decentraland.org/lambdas/profile/<wallet-address>
  • Always use
    peer.decentraland.org
    — it is the canonical catalyst regardless of realm/world. Worlds servers do NOT expose
    /lambdas
    , so do not blindly read
    realmInfo.baseUrl
    .
  • Response shape:
    json.avatars[0].avatar.{ bodyShape, wearables, eyes:{color}, hair:{color}, skin:{color} }
    (NOT the
    json[0].metadata.avatars...
    shape from older docs).
  • Unknown address returns
    { avatars: [], timestamp: 0 }
    — handle the empty array.
  • Colors come as
    { r, g, b, a }
    floats in
    [0,1]
    . Build a
    Color3
    and pass it directly to
    AvatarShape.skinColor
    /
    hairColor
    /
    eyeColor
    — these fields take a raw
    Color3
    , NOT
    { color: Color3 }
    (wrapping causes TS2322).
AvatarShape.create({ id: address })
with only an
id
does NOT auto-fetch wearables — the avatar renders undressed unless you supply
bodyShape
,
wearables
, and the color fields explicitly.
Which API to use:
  • Local or in-scene player →
    getPlayer(userId)
    (sync, includes wearables/emotes).
  • Off-scene address →
    fetchAvatarFromCatalyst(address)
    (async HTTP).
For the full helper (
fetchAvatarFromCatalyst
), end-to-end usage example, and gotchas, see
{baseDir}/references/catalyst-profile-fetch.md
.
getPlayer(userId)
仅返回当前连接到此场景的用户的数据。对于任何其他地址(地块所有者、NFT 持有者、排行榜条目、场景外领取者),请从 Catalyst 获取数据:
GET https://peer.decentraland.org/lambdas/profile/<wallet-address>
  • 始终使用
    peer.decentraland.org
    ——无论 realm 或 world 如何,它都是标准 Catalyst。Worlds 服务器不提供
    /lambdas
    ,因此不要直接读取
    realmInfo.baseUrl
    。
  • 响应结构:
    json.avatars[0].avatar.{ bodyShape, wearables, eyes:{color}, hair:{color}, skin:{color} }
    (不是旧版文档中的
    json[0].metadata.avatars...
    结构)。
  • 未知地址会返回
    { avatars: [], timestamp: 0 }
    ——请处理空数组的情况。
  • 颜色以
    [0,1]
    范围内的浮点数
    { r, g, b, a }
    返回。创建
    Color3
    并直接传给
    AvatarShape.skinColor
    /
    hairColor
    /
    eyeColor
    ——这些字段接收原始
    Color3
    ,而不是
    { color: Color3 }
    (包装后会引发 TS2322)。
仅使用
id
调用
AvatarShape.create({ id: address })
不会自动获取 wearables——除非显式提供
bodyShape
、
wearables
和颜色字段,否则 Avatar 会以未穿戴装备的状态渲染。
应使用的 API:
  • 本地玩家或场景内玩家 →
    getPlayer(userId)
    (同步,包含 wearables 和 Emotes)。
  • 场景外地址 →
    fetchAvatarFromCatalyst(address)
    (异步 HTTP)。
有关完整辅助函数
fetchAvatarFromCatalyst
、端到端使用示例和注意事项,请参阅
{baseDir}/references/catalyst-profile-fetch.md
。

Avatar Attachments

Avatar 附件

Attach 3D objects to a player's avatar:
typescript
import {
	engine,
	Transform,
	GltfContainer,
	AvatarAttach,
	AvatarAnchorPointType,
} from '@dcl/sdk/ecs'

const hat = engine.addEntity()
GltfContainer.create(hat, { src: 'models/hat.glb' })
Transform.create(hat, {})

// Attach to the local player's avatar
AvatarAttach.create(hat, {
	anchorPointId: AvatarAnchorPointType.AAPT_NAME_TAG,
})
AvatarAttach
requires the target player to be in the same scene — attachments disappear when the player leaves.
Before picking
AvatarAttach
, decide whether the item is cosmetic or aim-critical.
Bone anchors inherit avatar skeleton animation (idle bob, walk cycle, gesture) — great for hats/backpacks/halos, bad for held weapons, aiming reticles, or anything where relative position must stay stable. See Held items vs cosmetic items below.
将 3D 对象附加到玩家的 Avatar:
typescript
import {
	engine,
	Transform,
	GltfContainer,
	AvatarAttach,
	AvatarAnchorPointType,
} from '@dcl/sdk/ecs'

const hat = engine.addEntity()
GltfContainer.create(hat, { src: 'models/hat.glb' })
Transform.create(hat, {})

// Attach to the local player's avatar
AvatarAttach.create(hat, {
	anchorPointId: AvatarAnchorPointType.AAPT_NAME_TAG,
})
AvatarAttach
要求目标玩家位于同一场景中——玩家离开后,附件也会消失。
选择
AvatarAttach
前,先判断物品是装饰性的还是对瞄准敏感的。
骨骼锚点会继承 Avatar 骨骼动画(待机起伏、行走循环、手势),非常适合帽子、背包、光环等物品,不适合手持武器、瞄准准星或任何需要保持相对位置稳定的内容。详见下文的“手持物品与装饰性物品”。

Held items vs cosmetic items —
AvatarAttach
vs parenting to
engine.CameraEntity
/
engine.PlayerEntity

手持物品与装饰性物品——
AvatarAttach
与父级设为
engine.CameraEntity
/
engine.PlayerEntity

SDK7 gives you three distinct mechanisms for "an entity that follows the player". They are not interchangeable — picking the wrong one is the single most common mistake when porting "held item" patterns from SDK6, and the most common subtle failure is parenting an aim-sensitive item (gun, reticle, flashlight) to
engine.PlayerEntity
and discovering the item does not track camera pitch when the player looks up or down.
Default for any aim-sensitive held item:
Transform.parent = engine.CameraEntity
.
Use
engine.PlayerEntity
only when you specifically want yaw-only / no-pitch behavior (a body-fixed item the player carries but never aims with).
GoalUseTracksReason
Aim-sensitive held item — gun, aiming reticle, flashlight, anything pointed by looking around. Recommended default for held gameplay items.
Transform.parent = engine.CameraEntity
(plus local position offset for "in front of and below" the camera)
Camera yaw + pitchFollows the camera's full transform, so the item points where the player is looking — including up/down. This is the SDK7 analogue of SDK6's
Attachable.FIRST_PERSON_CAMERA
. Aim stable (no animation jitter).
Yaw-only / body-fixed item — a held shield the player doesn't aim, a static torch, a fixed-position carry item that should stay level regardless of where the player looks.
Transform.parent = engine.PlayerEntity
(plus local offset for hand-height / forward distance)
Player root: feet position + body yaw only (no pitch)Follows the player's root transform. Stable (no animation), but stays level when the player looks up/down — wrong default for guns/aim items, correct for items meant to ride the body orientation only.
Cosmetic item — hat, halo, backpack, name plate, glow effect, torch visible to other players riding the avatar.
AvatarAttach
with an
anchorPointId
(e.g.
AAPT_HEAD
,
AAPT_SPINE
,
AAPT_LEFT_HAND
)
The actual animated boneItem moves naturally with idle bob, walk cycle, and gestures — visually correct for cosmetics attached to the body. Not for aim — animation jitter makes aim-sensitive items unusable.
Why
engine.CameraEntity
is the right default for aim-sensitive items:
engine.PlayerEntity
only tracks the player's root (foot position + body yaw). Body yaw is NOT camera pitch — when the player tilts the camera up to aim at a flying target, the player root rotation does not change, so a gun parented to
PlayerEntity
stays flat and the muzzle doesn't track the look direction. Parenting to
engine.CameraEntity
inherits both yaw and pitch, so the gun aims where the camera looks. This matches the SDK6
Attachable.FIRST_PERSON_CAMERA
behavior creators expect when porting.
Why bone anchors break aim: anchor points like
AAPT_RIGHT_HAND
,
AAPT_SPINE
,
AAPT_HEAD
are positions on the animated avatar skeleton. Every frame the engine pulls the bone's current world transform — which includes the procedural idle bob and any active animation clip. An entity parented there inherits all of that motion. For a weapon, this reads as jitter and makes aiming feel uncontrollable.
SDK7 提供了三种不同的“跟随玩家的实体”机制。它们不可互换——将 SDK6 的“手持物品”模式迁移过来时,选错机制是最常见的错误;而更隐蔽的常见错误是把对瞄准敏感的物品设为
engine.PlayerEntity
的子实体,却没有发现该物品无法跟随相机的俯仰角度。
任何对瞄准敏感的手持物品都应默认使用:
Transform.parent = engine.CameraEntity
。
只有当你明确需要仅跟随偏航角或完全不跟随俯仰角时,才使用
engine.PlayerEntity
(即物品固定在身体上,但不会随视线瞄准)。
目标使用方式跟随内容原因
对瞄准敏感的手持物品——枪械、瞄准准星、手电筒,以及任何需要随视线指向的物品。持有类游戏物品的推荐默认方案。
Transform.parent = engine.CameraEntity
(另加本地位置偏移,以位于相机“前方且下方”)
相机偏航 + 俯仰跟随相机的完整变换,因此物品会指向玩家所看的方向,包括向上和向下。它是 SDK6 中
Attachable.FIRST_PERSON_CAMERA
的 SDK7 对应方案。瞄准稳定(无动画抖动)。
仅跟随偏航角/固定于身体的物品——玩家不会瞄准的手持盾牌、静态火炬,或无论视线朝何处都应保持水平的固定位置携带物品。
Transform.parent = engine.PlayerEntity
(另加本地偏移,以设置手部高度或前方距离)
玩家根节点:脚部位置 + 身体偏航角(不含俯仰角)跟随玩家根节点的变换。没有动画,因此十分稳定,但玩家向上或向下看时物品仍会保持水平。对于枪械和瞄准物品,这是错误方案;对于仅跟随身体朝向的物品则是正确方案。
装饰性物品——帽子、光环、背包、名称牌、发光效果,以及其他玩家也能看到的 Avatar 随身火炬。使用
AvatarAttach
并设置
anchorPointId
(例如
AAPT_HEAD
、
AAPT_SPINE
、
AAPT_LEFT_HAND
)
实际动画骨骼物品会自然跟随待机起伏、行走循环和手势动画——作为身体装饰品时视觉效果正确。不适用于瞄准——动画抖动会使对瞄准敏感的物品无法正常使用。
为什么
engine.CameraEntity
是对瞄准敏感物品的正确默认方案:
engine.PlayerEntity
只跟踪玩家根节点(脚部位置 + 身体偏航角)。身体偏航角不是相机俯仰角——当玩家将相机向上倾斜以瞄准飞行目标时,玩家根节点的旋转不会改变,因此设为
PlayerEntity
子实体的枪械会保持平直,枪口也不会跟踪视线方向。将实体设为
engine.CameraEntity
的子实体会同时继承偏航角和俯仰角,因此枪械始终瞄准相机所看的方向。这与创作者迁移时预期的 SDK6
Attachable.FIRST_PERSON_CAMERA
行为一致。
为什么骨骼锚点会破坏瞄准:
AAPT_RIGHT_HAND
、
AAPT_SPINE
、
AAPT_HEAD
等锚点位于 Avatar 的动画骨骼上。引擎每帧都会获取骨骼的当前世界变换,其中包括程序化待机起伏和当前播放的动画片段。设为这些锚点子实体的物品会继承所有运动。对于武器而言,这会表现为抖动,让瞄准难以控制。

Example — gun held in first person (aim follows camera pitch)

示例——第一人称手持枪械(瞄准跟随相机俯仰角)

typescript
import { engine, Transform, GltfContainer, CameraModeArea, CameraType } from '@dcl/sdk/ecs'
import { Vector3, Quaternion } from '@dcl/sdk/math'

const gun = engine.addEntity()
GltfContainer.create(gun, { src: 'assets/Models/blaster.glb' })
Transform.create(gun, {
	parent: engine.CameraEntity,           // gun follows camera (yaw + pitch) — aim tracks where you look
	position: Vector3.create(0.25, -0.2, 0.5), // right, down, forward of camera
	rotation: Quaternion.fromEulerDegrees(0, 0, 0),
	scale: Vector3.One(),
})
If the user does NOT want the item to track pitch (e.g. a held torch that should stay level, not point up when looking up), swap
engine.CameraEntity
for
engine.PlayerEntity
. Do not pick
PlayerEntity
for a gun
— the result is a flat-pointing weapon that ignores look direction.
Pair with
CameraModeArea
or a forced camera mode when equipping a held gun
, so the player is in first-person while aiming. See [[camera-control]].
typescript
import { engine, Transform, GltfContainer, CameraModeArea, CameraType } from '@dcl/sdk/ecs'
import { Vector3, Quaternion } from '@dcl/sdk/math'

const gun = engine.addEntity()
GltfContainer.create(gun, { src: 'assets/Models/blaster.glb' })
Transform.create(gun, {
	parent: engine.CameraEntity,           // gun follows camera (yaw + pitch) — aim tracks where you look
	position: Vector3.create(0.25, -0.2, 0.5), // right, down, forward of camera
	rotation: Quaternion.fromEulerDegrees(0, 0, 0),
	scale: Vector3.One(),
})
如果用户不希望物品跟随俯仰角(例如,手持火炬应在玩家向上看时保持水平,而不是一同向上),请将
engine.CameraEntity
替换为
engine.PlayerEntity
。枪械不要使用
PlayerEntity
——这样武器会始终平直指向,无法跟踪视线方向。
装备手持枪械时,请搭配使用
CameraModeArea
或强制相机模式
,让玩家在瞄准时处于第一人称视角。参阅 [[camera-control]]。

Anti-pattern (what NOT to do for a held weapon)

反模式(手持武器不应采用的方式)

typescript
// WRONG — gun jitters with every idle/walk/gesture animation frame
AvatarAttach.create(gun, {
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})
This looks like the right SDK7 way to "put a gun in the avatar's hand" because the API name reads that way — but the hand bone is animated, so the gun is unaimable in practice. Use parenting instead.
typescript
// SUBTLY WRONG for a gun — looks correct in hip-fire, fails the moment the player aims up
Transform.create(gun, { parent: engine.PlayerEntity, position: ... })
engine.PlayerEntity
inherits body yaw but NOT camera pitch. The gun stays flat when the player looks up to aim at a high target. Use
engine.CameraEntity
instead for any aim-sensitive item.
PlayerEntity
is correct only for body-fixed items that should stay level regardless of where the camera points (e.g. a carried lantern, a non-aimed shield).
typescript
// WRONG — gun jitters with every idle/walk/gesture animation frame
AvatarAttach.create(gun, {
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})
这看起来是 SDK7 中把“枪械放到 Avatar 手中”的正确方式——毕竟 API 名称看起来就是如此——但手部骨骼带有动画,因此枪械在实际使用中无法稳定瞄准。请改用父级关系。
typescript
// SUBTLY WRONG for a gun — looks correct in hip-fire, fails the moment the player aims up
Transform.create(gun, { parent: engine.PlayerEntity, position: ... })
engine.PlayerEntity
会继承身体偏航角,但不会继承相机俯仰角。玩家向上看以瞄准高处目标时,枪械会保持平直。任何对瞄准敏感的物品都应使用
engine.CameraEntity
。只有当物品固定于身体且无论相机指向何处都应保持水平时,才适合使用
PlayerEntity
(例如携带的灯笼或无需瞄准的盾牌)。

SDK6 porting note

SDK6 迁移说明

In SDK7 you have a choice that didn't exist in SDK6. SDK6's
Attachable.FIRST_PERSON_CAMERA
/
Attachable.AVATAR
mapped to coarse follow modes only. If you are porting a held item from an SDK6 scene that used
Attachable.FIRST_PERSON_CAMERA
, the SDK7 equivalent is parenting to
engine.CameraEntity
(NOT
AvatarAttach
to a hand anchor, and NOT
engine.PlayerEntity
—
PlayerEntity
loses camera pitch). See [[migrate-sdk6-to-sdk7]].
SDK7 提供了 SDK6 中不存在的选择。SDK6 的
Attachable.FIRST_PERSON_CAMERA
/
Attachable.AVATAR
只映射到粗粒度的跟随模式。如果正在迁移使用
Attachable.FIRST_PERSON_CAMERA
的 SDK6 场景中的手持物品,SDK7 中的等效方案是将实体设为
engine.CameraEntity
的子实体,而不是使用
AvatarAttach
连接到手部锚点,也不是使用
engine.PlayerEntity
(因为
PlayerEntity
会丢失相机俯仰角)。参阅 [[migrate-sdk6-to-sdk7]]。

Anchor Points

锚点

typescript
AvatarAnchorPointType.AAPT_NAME_TAG // Above the head
AvatarAnchorPointType.AAPT_RIGHT_HAND // Right hand
AvatarAnchorPointType.AAPT_LEFT_HAND // Left hand
AvatarAnchorPointType.AAPT_POSITION // [DEPRECATED] Avatar root position — protocol recommends parenting to `engine.PlayerEntity` (body-fixed) or `engine.CameraEntity` (aim-sensitive) instead
AvatarAnchorPointType.AAPT_HEAD
AvatarAnchorPointType.AAPT_NECK
AvatarAnchorPointType.AAPT_SPINE
AvatarAnchorPointType.AAPT_SPINE1
AvatarAnchorPointType.AAPT_SPINE2
AvatarAnchorPointType.AAPT_HIP
AvatarAnchorPointType.AAPT_LEFT_SHOULDER
AvatarAnchorPointType.AAPT_LEFT_ARM
AvatarAnchorPointType.AAPT_LEFT_FOREARM
AvatarAnchorPointType.AAPT_LEFT_HAND_INDEX
AvatarAnchorPointType.AAPT_RIGHT_SHOULDER
AvatarAnchorPointType.AAPT_RIGHT_ARM
AvatarAnchorPointType.AAPT_RIGHT_FOREARM
AvatarAnchorPointType.AAPT_RIGHT_HAND_INDEX
AvatarAnchorPointType.AAPT_LEFT_UP_LEG
AvatarAnchorPointType.AAPT_LEFT_LEG
AvatarAnchorPointType.AAPT_LEFT_FOOT
AvatarAnchorPointType.AAPT_LEFT_TOE_BASE
AvatarAnchorPointType.AAPT_RIGHT_UP_LEG
AvatarAnchorPointType.AAPT_RIGHT_LEG
AvatarAnchorPointType.AAPT_RIGHT_FOOT
AvatarAnchorPointType.AAPT_RIGHT_TOE_BASE
AvatarAnchorPointType.AAPT_NAME_TAG
Anchor points inherit bone animation. Bone-targeted anchors (
AAPT_RIGHT_HAND
,
AAPT_SPINE
,
AAPT_HEAD
, etc.) follow the animated skeleton — idle bob, walk cycle, and gesture animations all propagate to the attached entity. This is correct for cosmetic items (hats, halos, backpacks) and wrong for gameplay items where aim stability matters (guns, reticles). For aim-sensitive items parent to
engine.CameraEntity
(yaw + pitch — the default for guns/reticles/flashlights); for yaw-only body-fixed items parent to
engine.PlayerEntity
. See "Held items vs cosmetic items" above.
typescript
AvatarAnchorPointType.AAPT_NAME_TAG // Above the head
AvatarAnchorPointType.AAPT_RIGHT_HAND // Right hand
AvatarAnchorPointType.AAPT_LEFT_HAND // Left hand
AvatarAnchorPointType.AAPT_POSITION // [DEPRECATED] Avatar root position — protocol recommends parenting to `engine.PlayerEntity` (body-fixed) or `engine.CameraEntity` (aim-sensitive) instead
AvatarAnchorPointType.AAPT_HEAD
AvatarAnchorPointType.AAPT_NECK
AvatarAnchorPointType.AAPT_SPINE
AvatarAnchorPointType.AAPT_SPINE1
AvatarAnchorPointType.AAPT_SPINE2
AvatarAnchorPointType.AAPT_HIP
AvatarAnchorPointType.AAPT_LEFT_SHOULDER
AvatarAnchorPointType.AAPT_LEFT_ARM
AvatarAnchorPointType.AAPT_LEFT_FOREARM
AvatarAnchorPointType.AAPT_LEFT_HAND_INDEX
AvatarAnchorPointType.AAPT_RIGHT_SHOULDER
AvatarAnchorPointType.AAPT_RIGHT_ARM
AvatarAnchorPointType.AAPT_RIGHT_FOREARM
AvatarAnchorPointType.AAPT_RIGHT_HAND_INDEX
AvatarAnchorPointType.AAPT_LEFT_UP_LEG
AvatarAnchorPointType.AAPT_LEFT_LEG
AvatarAnchorPointType.AAPT_LEFT_FOOT
AvatarAnchorPointType.AAPT_LEFT_TOE_BASE
AvatarAnchorPointType.AAPT_RIGHT_UP_LEG
AvatarAnchorPointType.AAPT_RIGHT_LEG
AvatarAnchorPointType.AAPT_RIGHT_FOOT
AvatarAnchorPointType.AAPT_RIGHT_TOE_BASE
AvatarAnchorPointType.AAPT_NAME_TAG
锚点会继承骨骼动画。 面向骨骼的锚点(
AAPT_RIGHT_HAND
、
AAPT_SPINE
、
AAPT_HEAD
等)会跟随动画化的骨架——待机起伏、行走循环和手势动画都会传递给附加实体。对于装饰性物品(帽子、光环、背包)这是正确行为,但对于需要瞄准稳定性的游戏物品(枪械、准星)则是错误行为。对于对瞄准敏感的物品,请将实体设为
engine.CameraEntity
的子实体(偏航角 + 俯仰角,也是枪械和准星的默认方案);对于仅跟随偏航角、固定于身体的物品,请设为
engine.PlayerEntity
的子实体。参阅上文“手持物品与装饰性物品”。

Attach to a Specific Player

附加到特定玩家

typescript
AvatarAttach.create(hat, {
	avatarId: '0x123...abc', // Target player's wallet address
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})
avatarId
is the target player's wallet address. To attach to every player in the scene (including remote ones), iterate
engine.getEntitiesWith(PlayerIdentityData)
and read
player.address
for each — guard with a marker component so you attach only once per player:
typescript
import { PlayerIdentityData } from '@dcl/sdk/ecs'

engine.addSystem(() => {
	for (const [entity, player] of engine.getEntitiesWith(PlayerIdentityData)) {
		// player.address is the wallet address to pass as avatarId
	}
})
To attach the local player's own held item, get the address from
getPlayer()
/
await getPlayer()
(
.userId
). Omitting
avatarId
attaches to the local player. For multiplayer visibility of a held/attached item, sync the anchor entity's
AvatarAttach
component (see [[multiplayer-sync]]).
typescript
AvatarAttach.create(hat, {
	avatarId: '0x123...abc', // Target player's wallet address
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})
avatarId
是目标玩家的钱包地址。若要附加到场景中的每位玩家(包括远程玩家),请遍历
engine.getEntitiesWith(PlayerIdentityData)
并读取每位玩家的
player.address
——应使用标记组件进行保护,确保每个玩家只附加一次:
typescript
import { PlayerIdentityData } from '@dcl/sdk/ecs'

engine.addSystem(() => {
	for (const [entity, player] of engine.getEntitiesWith(PlayerIdentityData)) {
		// player.address is the wallet address to pass as avatarId
	}
})
若要附加本地玩家自己的手持物品,请从
getPlayer()
/
await getPlayer()
获取地址(
.userId
)。省略
avatarId
会将物品附加到本地玩家。若要让其他玩家也能看到手持或附加物品,请同步锚点实体的
AvatarAttach
组件(参阅 [[multiplayer-sync]])。

Triggering Emotes

触发 Emote

Default Emotes

默认 Emote

typescript
import { triggerEmote } from '~system/RestrictedActions'

// Play a built-in emote
triggerEmote({ predefinedEmote: 'robot' })
triggerEmote({ predefinedEmote: 'wave' })
triggerEmote({ predefinedEmote: 'clap' })
typescript
import { triggerEmote } from '~system/RestrictedActions'

// Play a built-in emote
triggerEmote({ predefinedEmote: 'robot' })
triggerEmote({ predefinedEmote: 'wave' })
triggerEmote({ predefinedEmote: 'clap' })

Custom Scene Emotes

自定义场景 Emote

⚠️ CRITICAL FILE NAMING REQUIREMENT: The emote
.glb
file MUST end with
_emote.glb
(case-insensitive). This is not optional and not just a convention — the runtime rejects files that don't match this suffix.
Why this matters: Scenes with incorrectly named emote files often work fine in
npm run start
preview
but silently fail in production once deployed. Preview is more permissive; the deployed runtime is strict. Always rename the file on disk (e.g.
SnowballThrow.glb
→
SnowballThrow_emote.glb
) before deploying.
Valid:
wave_emote.glb
,
Snowball_Throw_emote.glb
,
dance_EMOTE.GLB
Invalid:
wave.glb
,
emote_wave.glb
,
wave_emote_v2.glb
typescript
import { triggerSceneEmote } from '~system/RestrictedActions'

// File MUST end with _emote.glb — rename it on disk if it doesn't
triggerSceneEmote({
	src: 'animations/Snowball_Throw_emote.glb',
	loop: false,
})
Notes:
  • Emotes play only while the player is standing still — walking or jumping interrupts them
  • If you don't want a player to interrupt an emote, use the
    InputModifier
    component to freeze the player for the duration of the emote
  • Both
    triggerEmote
    and
    triggerSceneEmote
    require the scene to declare the
    ALLOW_TO_TRIGGER_AVATAR_EMOTE
    permission in
    scene.json
    requiredPermissions
    .
  • Both accept an optional
    mask
    (upper-body-only animation) — see "Emote masks" below.
⚠️ 关键的文件命名要求: Emote 的
.glb
文件必须以
_emote.glb
结尾(不区分大小写)。这不是可选项,也不只是约定——运行时会拒绝不符合此后缀的文件。
为什么这很重要: 文件命名错误的场景在
npm run start
预览中往往运行正常,但部署到生产环境后会静默失败。预览环境更宽松,而已部署的运行时执行严格校验。部署前,务必在磁盘上重命名文件(例如
SnowballThrow.glb
→
SnowballThrow_emote.glb
)。
有效:
wave_emote.glb
、
Snowball_Throw_emote.glb
、
dance_EMOTE.GLB

无效:
wave.glb
、
emote_wave.glb
、
wave_emote_v2.glb
typescript
import { triggerSceneEmote } from '~system/RestrictedActions'

// File MUST end with _emote.glb — rename it on disk if it doesn't
triggerSceneEmote({
	src: 'animations/Snowball_Throw_emote.glb',
	loop: false,
})
注意:
  • Emote 仅在玩家站立不动时播放——行走或跳跃会中断 Emote。
  • 如果不希望玩家中断 Emote,请使用
    InputModifier
    组件在 Emote 期间冻结玩家。
  • triggerEmote
    和
    triggerSceneEmote
    都要求场景在
    scene.json
    的
    requiredPermissions
    中声明
    ALLOW_TO_TRIGGER_AVATAR_EMOTE
    权限。
  • 两者都接受可选的
    mask
    (仅上半身动画),详见下文的“Emote mask”。

Stopping an emote

停止 Emote

stopEmote({})
from
~system/RestrictedActions
stops the local player's currently playing emote (built-in or scene emote). Useful to end a looping scene emote (
triggerSceneEmote({ src, loop: true })
) on demand — e.g. a "pick up / put down" toggle.
typescript
import { stopEmote } from '~system/RestrictedActions'
stopEmote({})
~system/RestrictedActions
中的
stopEmote({})
会停止本地玩家当前正在播放的 Emote(内置或场景 Emote)。它适用于按需结束循环播放的场景 Emote(例如
triggerSceneEmote({ src, loop: true })
),例如实现“拿起/放下”开关。
typescript
import { stopEmote } from '~system/RestrictedActions'
stopEmote({})

Emote masks (upper-body only)

Emote mask(仅上半身)

triggerEmote
and
triggerSceneEmote
both accept an optional
mask
(enum
AvatarMask
, imported from
@dcl/sdk/ecs
) that limits which bones the animation drives. Use it to restrict a looping emote to the upper body so the player can keep walking around while the upper body animates (e.g. carrying/juggling an object).
typescript
import { AvatarMask } from '@dcl/sdk/ecs'
import { triggerSceneEmote } from '~system/RestrictedActions'

triggerSceneEmote({ src: 'animations/Carry_emote.glb', loop: true, mask: AvatarMask.AM_UPPER_BODY })
  • Only value:
    AvatarMask.AM_UPPER_BODY
    (= 0). Omitting
    mask
    plays the full-body animation (the default) — there is no
    AM_FULL_BODY
    value in the enum.
  • mask
    applies to
    triggerEmote
    and
    triggerSceneEmote
    only.
    stopEmote({})
    takes no arguments (
    StopEmoteRequest
    is empty).
  • Loop + mask interaction:
    loop: false
    with
    mask: AM_UPPER_BODY
    plays the upper-body animation exactly once, then returns the upper body to locomotion.
    loop: true
    with the mask repeats until
    stopEmote({})
    is called. The loop flag is respected regardless of the mask. Verified against sdk7-test-scenes
    88,-13-avatar-masks
    and
    80,-1-scene-emotes
    (commit
    1c0f394
    ).
  • Verified against protocol
    restricted_actions.proto
    /
    common/avatar_mask.proto
    (pinned in
    @dcl/sdk
    via protocol
    0010e70
    ) and sdk7-test-scenes
    88,-13-avatar-masks
    (2026-07-16). Earlier speculative names
    AvatarEmoteMask
    /
    AEM_UPPER_BODY
    /
    AEM_FULL_BODY
    were never released — do not use them.
triggerEmote
和
triggerSceneEmote
都接受可选的
mask
(枚举
AvatarMask
,从
@dcl/sdk/ecs
导入),用于限制动画驱动的骨骼范围。可以借此将循环 Emote 限制在上半身,使玩家在播放上半身动画时仍能四处走动(例如携带或抛接物品)。
typescript
import { AvatarMask } from '@dcl/sdk/ecs'
import { triggerSceneEmote } from '~system/RestrictedActions'

triggerSceneEmote({ src: 'animations/Carry_emote.glb', loop: true, mask: AvatarMask.AM_UPPER_BODY })
  • 唯一有效值:
    AvatarMask.AM_UPPER_BODY
    (= 0)。省略
    mask
    会播放全身动画(默认行为);枚举中没有
    AM_FULL_BODY
    值。
  • mask
    仅适用于
    triggerEmote
    和
    triggerSceneEmote
    。
    stopEmote({})
    不接受参数(
    StopEmoteRequest
    为空)。
  • 循环 + mask 的交互:
    loop: false
    与
    mask: AM_UPPER_BODY
    搭配时,上半身动画会精确播放一次,然后上半身恢复移动状态。
    loop: true
    与 mask 搭配时,会持续重复,直到调用
    stopEmote({})
    。无论是否设置 mask,loop 标志都会被正确遵守。已对照 sdk7-test-scenes 的
    88,-13-avatar-masks
    和
    80,-1-scene-emotes
    (提交
    1c0f394
    )进行验证。
  • 已对照协议
    restricted_actions.proto
    /
    common/avatar_mask.proto
    (通过协议
    0010e70
    固定在
    @dcl/sdk
    中)以及 sdk7-test-scenes 的
    88,-13-avatar-masks
    (2026-07-16)进行验证。早期推测的名称
    AvatarEmoteMask
    /
    AEM_UPPER_BODY
    /
    AEM_FULL_BODY
    从未发布——请勿使用。

NPC Avatars

NPC Avatar

For creating NPCs (characters, shopkeepers, guards, etc.), see the npcs skill. It covers both the NPC Toolkit library (GLB-based, with dialogue and movement) and
AvatarShape
-based avatar NPCs.
如需创建 NPC(角色、店主、守卫等),请参阅 npcs Skill。该 Skill 涵盖 NPC Toolkit 库(基于 GLB,支持对话和移动)以及基于
AvatarShape
的 Avatar NPC。

Avatar Modifier Areas

Avatar Modifier Area

Modify how avatars appear or behave in a region.
typescript
import {
	engine,
	Transform,
	AvatarModifierArea,
	AvatarModifierType,
} from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const modifierArea = engine.addEntity()
Transform.create(modifierArea, {
	position: Vector3.create(8, 1.5, 8),
	scale: Vector3.create(4, 3, 4),
})

AvatarModifierArea.create(modifierArea, {
	area: Vector3.create(4, 3, 4),
	modifiers: [AvatarModifierType.AMT_HIDE_AVATARS],
	excludeIds: ['0x123...abc'], // Optional: exclude specific players
})
修改某个区域内 Avatar 的外观或行为。
typescript
import {
	engine,
	Transform,
	AvatarModifierArea,
	AvatarModifierType,
} from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const modifierArea = engine.addEntity()
Transform.create(modifierArea, {
	position: Vector3.create(8, 1.5, 8),
	scale: Vector3.create(4, 3, 4),
})

AvatarModifierArea.create(modifierArea, {
	area: Vector3.create(4, 3, 4),
	modifiers: [AvatarModifierType.AMT_HIDE_AVATARS],
	excludeIds: ['0x123...abc'], // Optional: exclude specific players
})

Available Modifiers

可用的 Modifier

typescript
AvatarModifierType.AMT_HIDE_AVATARS // Hide all avatars in the area
AvatarModifierType.AMT_DISABLE_PASSPORTS // Disable clicking on avatars to see profiles
AvatarModifierType.AMT_HIDE_NAMETAGS // Hide the name tag above avatars in the area
modifiers
is an array — combine several, e.g.
[AMT_HIDE_NAMETAGS, AMT_DISABLE_PASSPORTS]
. The
AvatarModifierArea
component takes both an
area: Vector3
field (the region size) AND the entity's
Transform.scale
; set both to the same size.
excludeIds
is an array of wallet addresses that stay unaffected (e.g. keep the scene owner visible); mutate it at runtime via
AvatarModifierArea.getMutable(entity).excludeIds = [...]
.
AMT_HIDE_AVATARS
hides both avatars AND nametags — do not combine it with
AMT_HIDE_NAMETAGS
(redundant). Use
AMT_HIDE_NAMETAGS
only when you want nametags hidden while keeping avatars visible (e.g. stages, presentations, clean visual experiences).
AMT_HIDE_NAMETAGS
is combinable with
AMT_DISABLE_PASSPORTS
.
Nametag hiding is head/torso based: the nametag is hidden only while the player's head or torso is inside the area. If the area is too short, a player who double-jumps above it will have their nametag briefly reappear. Make the area tall enough to cover the expected range of movement.
typescript
AvatarModifierType.AMT_HIDE_AVATARS // Hide all avatars in the area
AvatarModifierType.AMT_DISABLE_PASSPORTS // Disable clicking on avatars to see profiles
AvatarModifierType.AMT_HIDE_NAMETAGS // Hide the name tag above avatars in the area
modifiers
是一个数组——可以组合多个 Modifier,例如
[AMT_HIDE_NAMETAGS, AMT_DISABLE_PASSPORTS]
。
AvatarModifierArea
组件同时需要
area: Vector3
字段(区域大小)和实体的
Transform.scale
;请将二者设为相同大小。
excludeIds
是一个钱包地址数组,这些玩家不会受到影响(例如,可让场景所有者保持可见);可在运行时通过
AvatarModifierArea.getMutable(entity).excludeIds = [...]
修改。
AMT_HIDE_AVATARS
会同时隐藏 Avatar 和名称标签——不要将它与
AMT_HIDE_NAMETAGS
组合使用(属于冗余设置)。仅当需要隐藏名称标签但保留 Avatar 可见时,才使用
AMT_HIDE_NAMETAGS
(例如舞台、演示或简洁的视觉体验)。
AMT_HIDE_NAMETAGS
可以与
AMT_DISABLE_PASSPORTS
组合使用。
名称标签的隐藏基于头部/躯干位置: 只有当玩家的头部或躯干位于区域内时,名称标签才会隐藏。如果区域高度太低,进行双重跳跃的玩家其名称标签可能会短暂重新出现。请将区域设置得足够高,以覆盖预期的移动范围。

Avatar Locomotion Settings

Avatar 移动设置

Adjust the player's movement speed and jump height:
typescript
import { engine, AvatarLocomotionSettings } from '@dcl/sdk/ecs'

// Modify run speed and jump height (set only the fields you want to change)
AvatarLocomotionSettings.createOrReplace(engine.PlayerEntity, {
	runSpeed: 14, // default is 10
	jumpHeight: 3, // default is 1
})
Fields (all
float
, optional) with client defaults — verified against unity-explorer
origin/main
CharacterControllerSettings.asset
:
walkSpeed
(1.5),
jogSpeed
(8, the default movement speed),
runSpeed
(10),
jumpHeight
(1),
runJumpHeight
(1.5),
doubleJumpHeight
(2),
glidingSpeed
(6),
glidingFallingSpeed
(1),
hardLandingCooldown
(0.75s). See
references/avatar-apis.md
.
glidingFallingSpeed
is a max descent cap — it limits how fast the player falls while gliding, but does not limit upward motion. While gliding, continuous scene forces are 1.5× stronger and can lift the player; see the
player-physics
skill ("Forces while gliding").
调整玩家的移动速度和跳跃高度:
typescript
import { engine, AvatarLocomotionSettings } from '@dcl/sdk/ecs'

// Modify run speed and jump height (set only the fields you want to change)
AvatarLocomotionSettings.createOrReplace(engine.PlayerEntity, {
	runSpeed: 14, // default is 10
	jumpHeight: 3, // default is 1
})
字段均为可选的
float
类型,并具有以下客户端默认值——已对照 unity-explorer
origin/main
中的
CharacterControllerSettings.asset
验证:
walkSpeed
(1.5)、
jogSpeed
(8,默认移动速度)、
runSpeed
(10)、
jumpHeight
(1)、
runJumpHeight
(1.5)、
doubleJumpHeight
(2)、
glidingSpeed
(6)、
glidingFallingSpeed
(1)、
hardLandingCooldown
(0.75 秒)。参阅
references/avatar-apis.md
。
glidingFallingSpeed
是最大下降速度限制——它会限制玩家滑翔时的下落速度,但不会限制向上的运动。滑翔期间,持续施加的场景力会增强至 1.5 倍,并可将玩家托起;参阅
player-physics
Skill(“滑翔时的力”)。

Restrict Locomotion (InputModifier)

限制移动(InputModifier)

Use
InputModifier
on
engine.PlayerEntity
to freeze or selectively restrict the player's movement — useful for cutscenes, locked interactions, or controlled game mechanics.
typescript
import { InputModifier, engine } from '@dcl/sdk/ecs'

// Freeze all movement
InputModifier.create(engine.PlayerEntity, {
	mode: InputModifier.Mode.Standard({ disableAll: true }),
})

// Remove restrictions
InputModifier.deleteFrom(engine.PlayerEntity)
Behavior when frozen: gravity and external forces still apply, camera rotation stays available, global input events are still detectable, restrictions lift automatically when the player leaves scene bounds.
Standard-mode flags (all boolean, on
InputModifier.Mode.Standard({...})
):
disableAll
,
disableWalk
,
disableJog
,
disableRun
,
disableJump
,
disableEmote
. Protocol also defines
disableDoubleJump
and
disableGliding
. Note
disableJog
is separate from
disableWalk
/
disableRun
— jog is the default movement speed, so disabling only walk+run still lets the player jog.
The
mode
can be built two equivalent ways — the
InputModifier.Mode.Standard({...})
helper, or the raw discriminated union
{ $case: 'standard', standard: {...} }
.
Tip: Combine with
triggerSceneEmote
— freeze the player during an animation, then remove InputModifier when it ends.
For the cutscene pattern, see the advanced-input skill.
在
engine.PlayerEntity
上使用
InputModifier
可冻结或选择性地限制玩家移动——适用于过场动画、锁定的交互或受控游戏机制。
typescript
import { InputModifier, engine } from '@dcl/sdk/ecs'

// Freeze all movement
InputModifier.create(engine.PlayerEntity, {
	mode: InputModifier.Mode.Standard({ disableAll: true }),
})

// Remove restrictions
InputModifier.deleteFrom(engine.PlayerEntity)
冻结时的行为: 重力和外部力仍会生效,相机旋转仍可使用,全局输入事件仍可检测;当玩家离开场景边界时,限制会自动解除。
Standard 模式标志(均为布尔值,位于
InputModifier.Mode.Standard({...})
上):
disableAll
、
disableWalk
、
disableJog
、
disableRun
、
disableJump
、
disableEmote
。协议还定义了
disableDoubleJump
和
disableGliding
。请注意,
disableJog
与
disableWalk
/
disableRun
相互独立——jog 是默认移动速度,因此仅禁用 walk 和 run 仍允许玩家以 jog 速度移动。
mode
可以通过两种等效方式构建——使用
InputModifier.Mode.Standard({...})
辅助函数,或使用原始可辨识联合
{ $case: 'standard', standard: {...} }
。
提示: 将其与
triggerSceneEmote
结合使用——在动画期间冻结玩家,动画结束后再移除 InputModifier。
有关过场动画模式,请参阅 advanced-input Skill。

Teleporting the Player

传送玩家

movePlayerTo
from
~system/RestrictedActions
is the only way to relocate the player to a position.
Setting
Transform.getMutable(engine.PlayerEntity).position
does NOT work (see the read-only warning at the top of this file). For sustained forces (lift, knockback, push, wind), use the
player-physics
skill instead —
movePlayerTo
is for explicit teleports/slides, not for forces.
movePlayerTo
accepts:
  • newRelativePosition
    — where to move the player (scene-relative
    Vector3
    )
  • cameraTarget
    (optional) — a point in space for the camera to face after moving
  • avatarTarget
    (optional) — a point in space for the avatar to face after moving
  • duration
    (optional) — transition time in seconds; if provided, movement can be awaited
Constraints:
  • The player must already be inside the scene's bounds for this to work
  • The target position must also be within the scene's bounds
  • During the transition the avatar passes through colliders (verified: a
    CL_PHYSICS
    obstacle placed in the path is passed through)
  • Requires the
    ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE
    permission in
    scene.json
    requiredPermissions
  • All fields except
    newRelativePosition
    are optional —
    cameraTarget
    and
    avatarTarget
    may each be omitted or used independently
  • The target Y may be elevated (e.g.
    y: 12
    ) to place the player on a raised platform, not just ground level
~system/RestrictedActions
中的
movePlayerTo
是将玩家移动到指定位置的唯一方式。
设置
Transform.getMutable(engine.PlayerEntity).position
不起作用(参阅本文顶部的只读警告)。若需持续施加力(抬起、击退、推动、风力),请改用
player-physics
Skill——
movePlayerTo
用于明确的传送或滑动,而非物理力。
movePlayerTo
接受:
  • newRelativePosition
    ——玩家要移动到的位置(相对于场景的
    Vector3
    )
  • cameraTarget
    (可选)——移动后相机要朝向的空间点
  • avatarTarget
    (可选)——移动后 Avatar 要朝向的空间点
  • duration
    (可选)——过渡时间,单位为秒;如果提供该值,可以等待移动完成
限制:
  • 玩家必须已经位于场景边界内,该操作才能生效
  • 目标位置也必须位于场景边界内
  • 过渡期间,Avatar 会穿过碰撞体(已验证:路径中的
    CL_PHYSICS
    障碍物会被穿过)
  • 需要在
    scene.json
    的
    requiredPermissions
    中声明
    ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE
    权限
  • 除
    newRelativePosition
    外,所有字段均为可选——
    cameraTarget
    和
    avatarTarget
    可以分别省略或独立使用
  • 目标 Y 值可以更高(例如
    y: 12
    ),用于将玩家放置在抬高的平台上,而不仅仅限于地面高度

Instant teleport

立即传送

typescript
import { movePlayerTo } from '~system/RestrictedActions'

void movePlayerTo({
	newRelativePosition: Vector3.create(8, 0, 8),
	cameraTarget: Vector3.create(8, 1, 12),
	avatarTarget: Vector3.create(8, 1, 12),
})
typescript
import { movePlayerTo } from '~system/RestrictedActions'

void movePlayerTo({
	newRelativePosition: Vector3.create(8, 0, 8),
	cameraTarget: Vector3.create(8, 1, 12),
	avatarTarget: Vector3.create(8, 1, 12),
})

Smooth transition with duration

带时长的平滑过渡

When
duration
is set,
movePlayerTo
is awaitable. The resolved value has a
success
boolean —
false
if the player interrupted the movement with input.
typescript
import { movePlayerTo } from '~system/RestrictedActions'

async function teleport() {
	const result = await movePlayerTo({
		newRelativePosition: Vector3.create(1, 0, 1),
		cameraTarget: Vector3.create(8, 1, 8),
		duration: 2,
	})
	if (!result.success) {
		console.log('Movement was interrupted by the player')
	}
}
设置
duration
后,可以等待
movePlayerTo
完成。解析后的结果包含一个
success
布尔值——如果玩家通过输入中断了移动,则为
false
。
typescript
import { movePlayerTo } from '~system/RestrictedActions'

async function teleport() {
	const result = await movePlayerTo({
		newRelativePosition: Vector3.create(1, 0, 1),
		cameraTarget: Vector3.create(8, 1, 8),
		duration: 2,
	})
	if (!result.success) {
		console.log('Movement was interrupted by the player')
	}
}

Prevent the player from interrupting a transition

防止玩家中断过渡

Combine
InputModifier
with
movePlayerTo
to lock movement for the duration:
typescript
import { movePlayerTo } from '~system/RestrictedActions'
import { InputModifier, engine } from '@dcl/sdk/ecs'

async function lockedTeleport() {
	InputModifier.create(engine.PlayerEntity, {
		mode: InputModifier.Mode.Standard({ disableAll: true }),
	})

	await movePlayerTo({
		newRelativePosition: Vector3.create(1, 0, 1),
		cameraTarget: Vector3.create(8, 1, 8),
		duration: 2,
	})

	InputModifier.deleteFrom(engine.PlayerEntity)
}
将
InputModifier
与
movePlayerTo
结合使用,在过渡期间锁定移动:
typescript
import { movePlayerTo } from '~system/RestrictedActions'
import { InputModifier, engine } from '@dcl/sdk/ecs'

async function lockedTeleport() {
	InputModifier.create(engine.PlayerEntity, {
		mode: InputModifier.Mode.Standard({ disableAll: true }),
	})

	await movePlayerTo({
		newRelativePosition: Vector3.create(1, 0, 1),
		cameraTarget: Vector3.create(8, 1, 8),
		duration: 2,
	})

	InputModifier.deleteFrom(engine.PlayerEntity)
}

Avatar Change Listeners

Avatar 变化监听器

React to avatar changes in real-time:
typescript
import {
	AvatarEmoteCommand,
	AvatarBase,
	AvatarEquippedData,
} from '@dcl/sdk/ecs'

// Detect when the Explorer reports an emote playing on a player.
// AvatarEmoteCommand is written BY THE EXPLORER to report emote playback
// TO the scene -- it is NOT a signal from scene to renderer. It is appended
// to every player entity (local and remote alike).
AvatarEmoteCommand.onChange(engine.PlayerEntity, (cmd) => {
	if (cmd) console.log('Emote played:', cmd.emoteUrn)
})

// Detect avatar appearance changes (wearables, skin color, etc.)
AvatarBase.onChange(engine.PlayerEntity, (base) => {
	if (base) console.log('Avatar name:', base.name)
})

// Detect equipment changes
AvatarEquippedData.onChange(engine.PlayerEntity, (equipped) => {
	if (equipped) console.log('Wearables changed:', equipped.wearableUrns)
})
实时响应 Avatar 变化:
typescript
import {
	AvatarEmoteCommand,
	AvatarBase,
	AvatarEquippedData,
} from '@dcl/sdk/ecs'

// Detect when the Explorer reports an emote playing on a player.
// AvatarEmoteCommand is written BY THE EXPLORER to report emote playback
// TO the scene -- it is NOT a signal from scene to renderer. It is appended
// to every player entity (local and remote alike).
AvatarEmoteCommand.onChange(engine.PlayerEntity, (cmd) => {
	if (cmd) console.log('Emote played:', cmd.emoteUrn)
})

// Detect avatar appearance changes (wearables, skin color, etc.)
AvatarBase.onChange(engine.PlayerEntity, (base) => {
	if (base) console.log('Avatar name:', base.name)
})

// Detect equipment changes
AvatarEquippedData.onChange(engine.PlayerEntity, (equipped) => {
	if (equipped) console.log('Wearables changed:', equipped.wearableUrns)
})

Additional Anchor Points

其他锚点

Beyond the commonly used anchor points, the full list includes:
  • AvatarAnchorPointType.AAPT_POSITION
    — avatar feet position
  • AvatarAnchorPointType.AAPT_NAME_TAG
    — above the name tag
  • AvatarAnchorPointType.AAPT_LEFT_HAND
    /
    AAPT_RIGHT_HAND
  • AvatarAnchorPointType.AAPT_HEAD
    — head bone
  • AvatarAnchorPointType.AAPT_NECK
    — neck bone
Need to check the player's wallet before showing avatar items? See the nft-blockchain skill for wallet checks with
getPlayer()
and
isGuest
.
除常用锚点外,完整列表还包括:
  • AvatarAnchorPointType.AAPT_POSITION
    ——Avatar 脚部位置
  • AvatarAnchorPointType.AAPT_NAME_TAG
    ——名称标签上方
  • AvatarAnchorPointType.AAPT_LEFT_HAND
    /
    AAPT_RIGHT_HAND
  • AvatarAnchorPointType.AAPT_HEAD
    ——头部骨骼
  • AvatarAnchorPointType.AAPT_NECK
    ——颈部骨骼
需要在显示 Avatar 物品前检查玩家钱包吗? 有关使用
getPlayer()
和
isGuest
进行钱包检查,请参阅 nft-blockchain Skill。

Example scenes

示例场景

Engine-team test scenes (exercised against the real engine):
  • 100,102-avatar-attach-test —
    AvatarAttach
    on multiple anchor points; enumerates every player via
    PlayerIdentityData
    and attaches to
    player.address
    ; a follower entity reconstructs the attached world position from
    PlayerEntity
    + attached Transform.
  • 80,-1-scene-emotes —
    triggerEmote
    ,
    triggerSceneEmote
    (with a deliberately mis-named non-
    _emote.glb
    file shown NOT playing),
    stopEmote
    ,
    mask: AvatarMask.AM_UPPER_BODY
    , plus
    loop: false
    + mask (plays once, returns to locomotion) and
    loop: true
    + mask (repeats until stopped).
  • 11,0-move-player-to-duration —
    movePlayerTo
    with
    duration
    , reading
    result.success
    via
    .then()
    ,
    InputModifier
    locking input during the slide, and a
    CL_PHYSICS
    obstacle the avatar passes through mid-transition.
  • 9,99-modifier-areas —
    AvatarModifierArea
    (
    AMT_HIDE_AVATARS
    ) with runtime-mutated
    excludeIds
    , alongside
    CameraModeArea
    .
  • 10,99-avatar-modifier-hide-nametags —
    AvatarModifierArea
    with
    AMT_HIDE_NAMETAGS
    : hides nametags while keeping avatars visible.
  • 0,1-input-modifier —
    InputModifier
    toggling every Standard flag (
    disableAll/Walk/Jog/Run/Jump/Emote
    ), both via the helper and the raw
    $case
    form.
  • 80,-4-restricted-actions —
    movePlayerTo
    (incl. elevated
    y
    ,
    avatarTarget
    -only turns),
    triggerEmote
    ,
    triggerSceneEmote
    ,
    teleportTo
    ,
    openExternalUrl
    .
  • 88,-13-avatar-masks — emote masks: looping
    AvatarMask.AM_UPPER_BODY
    scene emote +
    AvatarAttach
    anchor to hold a synced crate,
    stopEmote
    to release. Also includes
    loop: false
    + mask pair (plays once then returns upper body to locomotion) and
    loop: true
    + mask pair (repeats until stopped) for verifying the masked-emote loop flag is respected.
For component field details, see
{baseDir}/../sdk-scenes/references/components-reference.md
. For anchor points, emote names, and event callbacks, see
{baseDir}/references/avatar-apis.md
.
引擎团队测试场景(已在真实引擎中验证):
  • 100,102-avatar-attach-test——在多个锚点上使用
    AvatarAttach
    ;通过
    PlayerIdentityData
    枚举每位玩家,并附加到
    player.address
    ;跟随实体根据
    PlayerEntity
    和附加的 Transform 重建所附加对象的世界位置。
  • 80,-1-scene-emotes——
    triggerEmote
    、
    triggerSceneEmote
    (包含一个故意命名错误且不以
    _emote.glb
    结尾的文件,用于演示其无法播放)、
    stopEmote
    、
    mask: AvatarMask.AM_UPPER_BODY
    ,以及
    loop: false
    + mask(播放一次后恢复移动)和
    loop: true
    + mask(重复播放直到停止)的组合。
  • 11,0-move-player-to-duration——带
    duration
    的
    movePlayerTo
    ,通过
    .then()
    读取
    result.success
    ,使用
    InputModifier
    在滑动期间锁定输入,并包含一个 Avatar 在过渡期间会穿过的
    CL_PHYSICS
    障碍物。
  • 9,99-modifier-areas——
    AvatarModifierArea
    (
    AMT_HIDE_AVATARS
    )及运行时修改的
    excludeIds
    ,并与
    CameraModeArea
    一同使用。
  • 10,99-avatar-modifier-hide-nametags——
    AvatarModifierArea
    (
    AMT_HIDE_NAMETAGS
    ):隐藏名称标签,同时保持 Avatar 可见。
  • 0,1-input-modifier——使用辅助函数和原始
    $case
    形式切换所有 Standard 标志(
    disableAll/Walk/Jog/Run/Jump/Emote
    )。
  • 80,-4-restricted-actions——
    movePlayerTo
    (包括升高的
    y
    和仅使用
    avatarTarget
    转向)、
    triggerEmote
    、
    triggerSceneEmote
    、
    teleportTo
    、
    openExternalUrl
    。
  • 88,-13-avatar-masks——Emote mask:循环播放带
    AvatarMask.AM_UPPER_BODY
    的场景 Emote,并通过
    AvatarAttach
    锚点持有同步木箱,使用
    stopEmote
    释放。还包含
    loop: false
    + mask 组合(播放一次后恢复上半身移动)和
    loop: true
    + mask 组合(重复播放直到停止),用于验证带 mask 时循环标志是否得到正确遵守。
有关组件字段详情,请参阅
{baseDir}/../sdk-scenes/references/components-reference.md
。 有关锚点、Emote 名称和事件回调,请参阅
{baseDir}/references/avatar-apis.md
。