mcpField
P
player-avatar

player-avatar

github.com/decentraland开发4289 次安装今天更新
代码仓库

关于 player-avatar

面向 Decentraland SDK7 场景开发者的玩家与 Avatar 技能,涵盖玩家位置读取、移动、资料和附属物。它特别说明 Transform 的只读限制,以及不同跟随方式的适用场景。

主要功能

  1. 01读取玩家位置与速度
  2. 02使用 API 移动玩家
  3. 03获取玩家资料与 Avatar 数据
  4. 04将物件附加到 Avatar
  5. 05区分摄像机与骨骼跟随

适用场景

  1. 01实现距离检测与玩家速度逻辑
  2. 02按玩家意图传送或改变移动参数
  3. 03为 Avatar 添加装饰或瞄准物件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/decentraland/sdk-skills --skill player-avatar

安装后在 Claude Code 中输入/player-avatar确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
avatar-apis.md
avatar-nametag.md
catalyst-profile-fetch.md

详细介绍

Decentraland 中的玩家与 Avatar 系统

重要:场景代码只能读取 Player Transform,不能修改

engine.PlayerEntity 上的 Transform 由引擎控制。场景代码进行的修改会被静默忽略——代码可以编译、运行,不会抛出错误,但游戏世界中的任何对象都不会移动。这是尝试抬起、推开、击退、漂浮或传送玩家时最常见的问题。

// 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:

目标使用技能
瞬间传送 / 平滑滑动到某个位置来自 ~system/RestrictedActions 的 movePlayerTo本技能,见下文
抬起 / 漂浮 / 发射 / 跳板 / 击退 / 推动 / 风力 / 排斥来自 @dcl/sdk/ecs 的 Physics.*player-physics
限制 / 冻结移动engine.PlayerEntity 上的 InputModifieradvanced-input
更改奔跑速度 / 跳跃高度engine.PlayerEntity 上的 AvatarLocomotionSettings本技能,见下文

Transform.get(engine.PlayerEntity) 只能用于读取位置和旋转。

玩家位置与移动(读取)

通过保留的 engine.PlayerEntity 访问玩家的位置:

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)——首帧时数据可能尚未就绪。

玩家速度——在时间窗口内取平均值,切勿逐帧计算

玩家的 Transform 不会在每个场景刻中更新。 因此,逐帧计算的速度 Vector3.distance(pos, lastPos) / dt 会在未更新的刻中为 0,并在下一次更新时达到实际速度的约 2 倍;任何“速度高于 X 并持续 N 秒”的检测都无法通过。实测于游戏世界中(SDK 7.30.1,2026-10-12):约 9 m/s 的冲刺未能满足逐帧检测条件 speed > 7.5 且持续 0.5 s;使用相同阈值并按 0.5 s 窗口计算时,首次尝试就触发了检测。

改为累积数据,并在窗口结束时重置窗口:

let winDist = 0, winTime = 0
let last = Vector3.Zero()

function speedSystem(dt: number) {
	if (!Transform.has(engine.PlayerEntity)) return
	const pos = Transform.get(engine.PlayerEntity).position
	const step = Vector3.distance(pos, last)
	last = pos
	if (step > 3) return          // teleport / respawn — discard, don't accumulate
	winDist += step
	winTime += dt
	if (winTime < 0.5) return     // window must be >= ~0.5 s to be stable
	const speed = winDist / winTime
	winDist = 0; winTime = 0
	// ... compare `speed` against your threshold here
}

忽略任何超过约 3 m 的单次位移:那是传送或重生,而不是移动,会导致平均值飙升。

基于距离的逻辑
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)
注意:远程玩家的 Transform 使用世界坐标

本地玩家的 Transform.get(engine.PlayerEntity).position 使用场景局部坐标(相对于场景所在的基础地块)。另一个玩家的实体——通过 engine.getEntitiesWith(PlayerIdentityData) 获取的实体——报告的位置使用世界坐标。混用这两种坐标会导致远程玩家的位置无声无息地偏移数百米。

通过减去基础地块坐标乘以 16 进行转换:

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

// scene.json base parcel 33,20
const SCENE_ORIGIN = Vector3.create(33 * 16, 0, 20 * 16)
const sceneLocal = Vector3.subtract(worldPosition, SCENE_ORIGIN)

33,20-spectate-mode 会有条件地进行减法——仅当位置超出场景的 AABB 时才减去偏移量。这使它无需判断玩家是本地还是远程,就能通过一条代码路径同时处理两者。如果你始终知道该实体是远程玩家,则可以无条件减去偏移量。

从 scene.json 读取基础地块,而不要将其硬编码,否则场景重新部署到其他位置时,坐标转换就会失效。

玩家资料数据

获取玩家姓名、钱包地址和访客状态:

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 — 玩家的以太坊钱包地址(或访客 ID)
  • isGuest — 如果玩家尚未连接钱包,则为 true

尝试使用任何依赖钱包的功能前,请先检查 isGuest(访客没有链上身份)。

移动端、其他玩家的位置: 在移动端,getPlayer({ userId }).position、onEnterScene 和附加到其他玩家身上的物品会报告场景相对坐标——与本地玩家使用同一坐标系——但仅适用于移动应用 v1.15.0 及更高版本(文档提交 5f212e3,2026 年 10 月)。在较旧的移动端版本中,这些值使用不同的坐标系,因此基于其他玩家位置执行的距离检查和排行榜在这些版本上会出错。任何安全关键逻辑都应根据每个客户端上本地玩家自身的位置计算,而不是使用远程玩家的位置。

非场景内用户的资料数据(Catalyst)

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 }) 不会自动获取穿戴物品——除非显式提供 bodyShape、wearables 和颜色字段,否则头像会呈现为未着装状态。

应使用哪个 API:

  • 本地或场景内玩家 → getPlayer(userId)(同步,包含穿戴物品/表情动作)。
  • 场景外地址 → fetchAvatarFromCatalyst(address)(异步 HTTP)。

如需了解完整辅助函数(fetchAvatarFromCatalyst)、端到端用法示例及注意事项,请参阅 {baseDir}/references/catalyst-profile-fetch.md。

Avatar 附件

将 3D 对象附加到玩家的 Avatar 上:

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 要求目标玩家处于同一场景中——玩家离开后,附件会消失。

附加实体自身的 Transform 位置和旋转会被忽略。 Explorer 每帧都会将该实体的世界位置和旋转设为锚点的对应值。只有 scale 会保留(unity-explorer AvatarAttachUtils.ApplyAnchorPointTransformValues,main 2026-10)。随后,它会将姿态写回实体的 Transform,相对于本地玩家:position 是锚点的世界位置减去玩家的位置(使用世界坐标轴,不会根据玩家的朝向旋转),rotation 则相对于玩家的旋转(AvatarAttachHandlerSystem.UpdateAvatarAttachedEntitySDKTransform)。子对象会保留各自的本地 Transform,因此若要调整附件的位置,请将 AvatarAttach 放在空根对象上,并将可见部分设为其子对象:

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

const anchor = engine.addEntity()
Transform.create(anchor, {})
AvatarAttach.create(anchor, { anchorPointId: AvatarAnchorPointType.AAPT_HIP })

const padlock = engine.addEntity()
GltfContainer.create(padlock, { src: 'assets/Models/padlock.glb' })
Transform.create(padlock, { parent: anchor, position: Vector3.create(0.15, -0.1, 0.12) }) // offset from the hip

测量座椅偏移,不要凭目测调整。 在 Locksmith 场景(2026-10)中,所有者在两天内六次调整了坐姿玩家的位置。最终解决办法是:播放 sittingChair 时,臀部大约位于 Avatar 根节点后方 0.37 m 处,因此座椅偏移设为 0.38 m。这个数值仅适用于一个身体模型和一个表情动作。请自行测量:将空实体附加到 AAPT_HIP,并播放坐姿表情动作。然后读取它的 Transform(在场景代码中使用 Transform.get,或使用 Explorer MCP 的 get_entity_details,即 unity-explorer-mcp)。根据上述写回规则,它的 position 是臀部相对于玩家根节点、沿世界坐标轴的偏移量,因此请结合玩家朝向,将其转换为座椅坐标系中的偏移量。然后调整玩家根节点的位置,使臀部落在座位上。

选择 AvatarAttach 之前,请先判断物品是装饰物还是对瞄准至关重要。 骨骼锚点会继承 Avatar 骨架动画(待机晃动、行走循环、手势)——非常适合帽子、背包和光环,但不适合手持武器、瞄准准星,或任何要求相对位置保持稳定的物品。请参阅下文的 手持物品与装饰物品。

手持物品与装饰物品——AvatarAttach 与挂接到 engine.CameraEntity / engine.PlayerEntity

SDK7 提供了三种不同的机制来实现“跟随玩家的实体”。它们不可互换——在将 SDK6 中的“手持物品”模式移植过来时,选错机制是最常见的错误;而最常见的隐蔽故障,则是将对瞄准方向敏感的物品(枪、瞄准标记、手电筒)挂接到 engine.PlayerEntity,结果发现玩家抬头或低头时,物品不会跟随相机俯仰。

对任何对瞄准方向敏感的手持物品,默认使用 Transform.parent = engine.CameraEntity。 只有在明确需要仅随偏航转动、不随俯仰变化的效果时(例如固定在身体上的物品,玩家携带但不会用来瞄准),才使用 engine.PlayerEntity。

目标使用方式跟随内容原因
对瞄准方向敏感的手持物品——枪、瞄准标记、手电筒,以及任何会随视线方向指向不同位置的物品。推荐作为手持游戏物品的默认方式。Transform.parent = engine.CameraEntity(再添加局部位置偏移,使物品位于相机前方并略低的位置)相机偏航 + 俯仰跟随相机的完整变换,因此物品会指向玩家的视线方向,包括上方和下方。这是 SDK7 中对应 SDK6 Attachable.FIRST_PERSON_CAMERA 的方式。瞄准稳定(没有动画抖动)。
仅随偏航转动/固定在身体上的物品——玩家不会用来瞄准的手持盾牌、静态火把,或无论玩家看向何处都应保持水平的固定位置携带物品。Transform.parent = engine.PlayerEntity(再添加局部偏移,以调整到手部高度和前方距离)玩家根节点:脚部位置 + 身体仅偏航(无俯仰)跟随玩家的根变换。表现稳定(无动画),但玩家抬头或低头时物品仍保持水平——这不适合作为枪械或瞄准物品的默认方式,但适用于只需跟随身体朝向的物品。
装饰物品——帽子、光环、背包、名牌、发光效果,以及其他玩家佩戴在角色身上时可见的火把。使用带有 anchorPointId 的 AvatarAttach(例如 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 等锚点位于角色动画骨架上。引擎每一帧都会获取骨骼当前的世界变换,其中包含程序生成的待机摆动和任何正在播放的动画片段。挂接到这些锚点的实体会继承所有这些运动。用于武器时,这会表现为抖动,让瞄准变得难以控制。

示例——第一人称手持枪械(瞄准方向跟随相机俯仰)
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]]。

反模式(持握武器时不要这样做)
// WRONG — gun jitters with every idle/walk/gesture animation frame
AvatarAttach.create(gun, {
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})

这看起来像是 SDK7 中“把枪放到 Avatar 手里”的正确做法,因为 API 名称让人这么以为——但手部骨骼会播放动画,因此枪实际上无法稳定瞄准。请改用父子关系。

// 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 移植说明

SDK7 提供了 SDK6 中没有的选择。SDK6 的 Attachable.FIRST_PERSON_CAMERA / Attachable.AVATAR 仅对应粗略的跟随模式。如果要将 SDK6 场景中使用 Attachable.FIRST_PERSON_CAMERA 的持握物品移植到 SDK7,对应做法是将其设为 engine.CameraEntity 的子级(不要使用 AvatarAttach 将其附加到手部锚点,也不要使用 engine.PlayerEntity——PlayerEntity 不会继承摄像机俯仰)。请参阅 [[migrate-sdk6-to-sdk7]]。

锚点
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 的子级。请参阅上文“持握物品与装饰物品”。

附加到特定玩家
AvatarAttach.create(hat, {
	avatarId: '0x123...abc', // Target player's wallet address
	anchorPointId: AvatarAnchorPointType.AAPT_RIGHT_HAND,
})

avatarId 是目标玩家的钱包地址。要将物品附加到场景中的每位玩家(包括远程玩家),请遍历 engine.getEntitiesWith(PlayerIdentityData),并读取每位玩家的 player.address——使用标记组件进行保护,确保每位玩家只附加一次:

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]])。

触发表情动作

默认表情动作
import { triggerEmote } from '~system/RestrictedActions'

// Play a built-in emote
triggerEmote({ predefinedEmote: 'robot' })
triggerEmote({ predefinedEmote: 'wave' })
triggerEmote({ predefinedEmote: 'clap' })
自定义场景表情动作

需要动画片段吗? 在制作之前,请查看 {baseDir}/../avatar-animations/references/animation-catalog.md:这里有每个内置表情动作的动画预览,以及 Creator Hub 的 Avatar Animations 包中 45 个现成的 _emote.glb 文件(坐凳子/软垫/懒人沙发、下棋、钓鱼、搬运、喝饮料、跳舞等),并附有下载链接和播放它们所需的确切 src 路径。

⚠️ 关键文件命名要求: 表情动作 .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

import { triggerSceneEmote } from '~system/RestrictedActions'

// File MUST end with _emote.glb — rename it on disk if it doesn't
// src is the path from the PROJECT ROOT, exactly as on disk (usually under assets/)
triggerSceneEmote({
	src: 'assets/animations/Snowball_Throw_emote.glb',
	loop: false,
})

注意:

  • src 是相对于项目根目录的路径,与文件在磁盘上的实际路径完全一致——会进行精确内容查找(SceneContent.TryGetHash(src)),不会按文件名或 _emote 后缀搜索。路径错误会导致 triggerSceneEmote 返回 false,且不会记录错误,也不会播放任何内容。若文件位于 assets/animations/sit_emote.glb,则需要使用 'assets/animations/sit_emote.glb';仅当该文件夹确实位于项目根目录时,裸路径 'animations/...' 才有效。AvatarShape.expressionTriggerId 也遵循相同规则——请参阅 npcs。
  • 表情动作只会在玩家站立不动时播放——行走或跳跃会中断播放。
  • 如果不希望玩家中断表情动作,请使用 InputModifier 组件,在表情动作持续期间冻结玩家。
  • triggerEmote 和 triggerSceneEmote 都要求场景在 scene.json 的 requiredPermissions 中声明 ALLOW_TO_TRIGGER_AVATAR_EMOTE 权限。
  • 两者都接受可选的 mask(仅上半身动画)——请参阅下文的“表情动作遮罩”。
  • 如需创建 .glb(在 Blender 中摆好官方 Avatar rig 的姿势、导出并验证),请参阅 avatar-animations 技能。
停止表情动作

从 ~system/RestrictedActions 调用 stopEmote({}),可以停止本地玩家当前正在播放的表情动作(内置或场景表情动作)。适用于按需结束循环播放的场景表情动作(triggerSceneEmote({ src, loop: true })),例如“拿起/放下”切换。

import { stopEmote } from '~system/RestrictedActions'

stopEmote({}).then(({ success }) => {
	// `success` is a plain boolean (the RPC returns `SuccessResponse`).
})

如果 typeof success 的结果是 'object' 而不是 'boolean',说明运行时传给场景的是尚未解析的 promise,而不是值——这是客户端/SDK 缺陷,并非场景问题。此问题已在 80,-6-explorer-ui-events 测试场景中得到验证,该场景的部分用途就是捕获此问题。

移动端: 从移动应用 v1.15.0 起支持 stopEmote(文档提交 5f212e3)。同一版本还实现了以下行为:玩家离开场景时,场景启动的上半身表情动作会暂停;玩家返回时恢复;场景卸载时停止;此外,其他移动端玩家也会将这些动作识别为上半身动作。在较早版本的移动端中,无法从场景停止循环播放的场景表情动作,因此不要设计唯一退出方式依赖 stopEmote 的机制;应提供一个操作,让玩家也能通过其他方式恢复姿势。

检测表情动作何时结束

每个表情动作生命周期事件都会追加到玩家实体上的 AvatarEmoteCommand 只增集合中,并带有一个 state 字段(EmoteState 枚举)。适用于场景触发的表情动作(triggerEmote/triggerSceneEmote)、玩家通过表情动作轮盘播放的表情动作,以及其他玩家的表情动作(传入其实体,而非 engine.PlayerEntity)。

import { AvatarEmoteCommand, EmoteState } from '@dcl/sdk/ecs'

AvatarEmoteCommand.onChange(engine.PlayerEntity, (cmd) => {
	if (!cmd) return
	switch (cmd.state ?? EmoteState.ES_STARTED) {
		case EmoteState.ES_STARTED:     // emote started (also the value when `state` is absent)
			break
		case EmoteState.ES_FINISHED:    // non-looping emote played to its natural end
			break
		case EmoteState.ES_INTERRUPTED: // cut short: movement/jump, teleport, another emote, stopEmote(), or scene exit
			break
	}
})
  • state 是协议中的可选字段:始终将其默认设为 ES_STARTED(cmd.state ?? EmoteState.ES_STARTED),不要假定该字段一定存在。
  • 本地玩家的遮罩(身体局部)表情动作不会报告生命周期事件——这是已知限制。需要知道遮罩动画片段何时结束的场景,必须根据片段时长自行计时。
  • 已根据协议 avatar_emote_command.proto(提交 215d09c,字段 5,可选)和 js-sdk-toolchain(提交 f858f905)验证。
表情动作遮罩(仅上半身)

玩家行走或跳跃时,全身表情动作会被中断——默认的移动动画会接管动作。AvatarMask.AM_UPPER_BODY 会将动画限制在腰部以上,让腿部继续由移动动画控制。这意味着玩家可以在上半身播放动画的同时继续行走、奔跑和跳跃。使用场景:搬运板条箱、举着火把、杂耍、边跑边欢呼。

triggerEmote 和 triggerSceneEmote 都接受可选的 mask(枚举 AvatarMask,从 @dcl/sdk/ecs 导入),用于限制动画驱动的骨骼。

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 为空)。
  • 循环与遮罩的交互:loop: false 搭配 mask: AM_UPPER_BODY 时,上半身动画恰好播放一次,然后恢复为移动动画。loop: true 搭配遮罩时,动画会重复播放,直到调用 stopEmote({})。无论是否使用遮罩,循环标志都会生效。已根据 sdk7-test-scenes 88,-13-avatar-masks 和 80,-1-scene-emotes(提交 1c0f394)验证。
  • 移动端支持:Avatar Masks(仅上半身的表情动作)自 v1.13.0 起已在移动端提供——不再存在平台支持差距;自 v1.15.0 起,其他移动端玩家也会将带遮罩的表情动作视为上半身动作,而非全身动作。旧版移动端客户端会将带遮罩的表情动作作为全身动作播放,因此请确保全身动画回退效果在视觉上仍可接受。仍待解决的问题:来自其他客户端的表情动作(Bevy-Web 玩家的场景表情动作、Unity 玩家通过 LiveKit 发送的表情动作)可能完全无法在移动端播放。已根据文档提交 5f212e3(2026 年 10 月,移动端 v1.15.0)验证。
  • **带遮罩的表情动作不会与其他表情动作叠加:**开始带遮罩的表情动作会停止正在播放的全身表情动作(包括椅子的坐姿表情动作);全身表情动作也会取消带遮罩的表情动作。遮罩只会与移动动画叠加。玩家处于坐姿时,请改为播放包含坐姿的全身片段。制作详情见 avatar-animations →“上半身(遮罩)动画”。
  • 已根据协议 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 头像

要创建 NPC(角色、店主、守卫等),请参阅 npcs 技能。该技能涵盖 NPC Toolkit 库(基于 GLB,支持对话和移动)以及基于 AvatarShape 的头像 NPC。

玩家的仿制形象(排队的“你”、摆好姿势的克隆体)使用由 getPlayer() 提供数据的 AvatarShape,而不是烘焙模型,因此会匹配玩家当时穿着的服装。例外情况是必须每帧跟踪玩家的分身(镜像、影子、回放):AvatarShape 的移动由引擎驱动,因此会滞后,并自动面向移动方向——这种情况应使用带有 Animator 的绑定 GLB,参见 npcs →“玩家的副本”。字段映射(avatar?.eyesColor → eyeColor、avatar?.bodyShapeUrn → bodyShape)、使用 expressionTriggerId 为副本摆姿势,以及头像会忽略 VisibilityComponent 这些内容,都在 npcs →“玩家的副本”中。

自定义名牌(AvatarNametag)

在头像原生名牌上方绘制一个带有场景提供文本的名牌板,用于显示场景分配的等级、角色、队伍或头衔(“VIP”、“红队”、“俱乐部老板”)。它不会替代原生名牌。需要 @dcl/sdk 7.28.0+(7.27.0 及更早版本不支持)。

import { AvatarNametag, engine } from '@dcl/sdk/ecs'

AvatarNametag.createOrReplace(engine.PlayerEntity, { label: 'Club Owner' })

这是一个来自 @dcl/sdk/ecs 的普通 LWW 组件——提供标准的 create / createOrReplace / getMutable / getOrNull / deleteFrom,没有辅助函数。label: string 为必填项;labelColor、backgroundColor、borderColor 是可选的 Color3 字段,默认回退到原生名牌的颜色(边框默认与背景相同,因此不会显示边框)。完整字段表、label 边界情况、组件 ID 和多人游戏玩家列表模式:{baseDir}/references/avatar-nametag.md。

有效目标: engine.PlayerEntity、任何远程玩家实体(携带 PlayerIdentityData 的实体),以及任何带有 AvatarShape 的实体(NPC——参见 npcs 技能)。写入任何其他实体都会被静默忽略——不会报错,也不会显示名牌板。

附加到其他玩家

远程玩家是普通实体,因此这两种写法都只需一行:

import { onEnterScene } from '@dcl/sdk/src/players'
import { AvatarNametag } from '@dcl/sdk/ecs'

onEnterScene((player) => {
	AvatarNametag.createOrReplace(player.entity, { label: rankFor(player.userId) })
})
import { engine, AvatarNametag, PlayerIdentityData } from '@dcl/sdk/ecs'

for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
	AvatarNametag.createOrReplace(entity, { label: rankFor(identity.address) })
}

AvatarNametag.deleteFrom(entity) 会立即移除名牌板;原生名牌不受影响。要将颜色恢复为原生默认值,请使用 createOrReplace 并省略该字段——它会清除未指定的字段,而 getMutable 会保留这些字段。

注意事项
  • 无需等待 Avatar 就绪。 向仍在加载的 Avatar 写入是安全的——渲染器会在内部重试,并在 Avatar 就绪后应用铭牌。写入前切勿轮询、延迟或尝试检测 Avatar 是否就绪。
  • 铭牌仅在客户端本地生效,不会转发;玩家实体 ID 会在断开连接后被重新使用——每次写入时都要通过用户 ID 解析实体(上面两种写法都这么做)。要让所有人看到相同的标签,请根据共享数据生成标签(参考中的 roster 模式),或同步分配结果:multiplayer-sync → “不会自动共享各玩家的视觉效果”。
  • 迭代 engine.getEntitiesWith(...) 时,切勿调用 createOrReplace。 替换组件可能会将实体移至不同的原型组,从而使正在使用的迭代器失效。先将实体收集到数组中,再进行写入。
  • 不要每个 tick 都重写组件。 写入前与 AvatarNametag.getOrNull(entity) 的结果进行比较,否则场景会为每位玩家每帧发送一条 CRDT 消息。
  • label 只能有一行。 较长的标签会以省略号截断;不会换行。如需不带文本的纯色铭牌,请使用仅包含空格的标签(label: ' ';空格越多,铭牌越宽)。将 labelColor 设为与 backgroundColor 相同的颜色不会隐藏文本——渲染后的文本颜色与铭牌颜色并不完全一致,因此文字仍会隐约可见。
  • 颜色不支持 Alpha。 仅支持 Color3;没有不透明度字段。
  • 移动端尚未实现此功能(截至 2026 年 10 月的兼容性更新,暂无预计时间)。移动端玩家完全看不到铭牌,因此切勿让它成为信息的唯一载体——请在 TextShape、HUD 列表或 Avatar 已穿戴的颜色上同步显示等级/角色。
  • 使用 AMT_HIDE_NAMETAGS 或 AMT_HIDE_AVATARS 的 AvatarModifierArea 会同时隐藏铭牌和原生名牌(见下文)。无法在隐藏原生名牌的同时保留铭牌。

Avatar Modifier Areas

修改区域内 Avatar 的外观或行为。

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
})
可用修饰符
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 是一个数组——可组合多个修饰符,例如 [AMT_HIDE_NAMETAGS, AMT_DISABLE_PASSPORTS]。区域大小由显式的 area: Vector3 字段指定——运行时会根据非零的 area 设置区域大小,并抵消 Transform.scale 的影响(若 area 为零或未设置,则回退到 scale;若某个 scale 轴为零,则该轴上的区域仍会缩为零——SDKEntityTriggerAreaComponent,unity-explorer main)。**仍应让 Transform.scale 与 area 相同:**编辑实体时,Creator Hub 会将 gizmo 的缩放值写入 area,并根据 scale 绘制占位立方体,因此两者不匹配会显示误导性的方框,且下次保存编辑内容时会悄然将区域大小改为 scale。请将 area 设为所需尺寸,并在 scale 中使用相同数值;切勿只依赖 scale 设置区域大小。excludeIds 是一个钱包地址数组,其中的地址不受影响(例如,让场景所有者保持可见);可在运行时通过 AvatarModifierArea.getMutable(entity).excludeIds = [...] 修改它。

AMT_HIDE_AVATARS 会同时隐藏 Avatar 和名牌——不要将它与 AMT_HIDE_NAMETAGS 组合使用(多余)。这两种修饰符也都会隐藏 AvatarNametag 铭牌(见上文);无法在保留铭牌的同时隐藏原生名牌。只有在希望隐藏名牌、但保留 Avatar 可见时,才使用 AMT_HIDE_NAMETAGS(例如舞台、演示或简洁的视觉体验)。AMT_HIDE_NAMETAGS 可以与 AMT_DISABLE_PASSPORTS 组合使用。

名牌隐藏取决于头部/躯干位置:只有当玩家的头部或躯干位于区域内时,名牌才会隐藏。如果区域不够高,玩家双重跳跃到区域上方时,名牌会短暂重新出现。请确保区域足够高,能够覆盖预期的移动范围。 注意事项——只要组件存在,显式设置的 area 就会优先于 Transform.scale,因此无法通过缩放调整区域大小。 每一帧都会重新应用逆世界缩放:之后缩放区域实体(或它的任何父级——它会补偿 lossyScale)会让区域仍保持 area 指定的大小,玩家的体验也不会改变。某个轴的缩放值为 0 时,该轴上的区域仍会收缩为 0。注意事项:

  • 不要通过动画或补间调整区域实体的缩放来扩大或缩小区域——不会生效。请改为写入 AvatarModifierArea.getMutable(entity).area(并像上文所述一样,将其同步到 scale,供编辑器使用)。
  • 不要将区域设为某个缩放组的子级,例如在激活时会放大的道具装置:视觉对象会变大,区域却保持原位;由于没有 API 可以读取头像是否被隐藏,这种不匹配在世界中无法察觉。
  • 若要可视化区域,请使用一个尺寸取自相同数值的独立盒体实体,就像回归场景中那样——只要其中任一数值发生变化,区域实体本身的可见盒体就会产生误导。
  • TriggerArea 恰恰相反:PBTriggerArea 完全没有尺寸字段(只有 mesh 和 collision_mask),因此其体积由实体的 Transform 决定——调整缩放即可改变触发区,包括之后进行的缩放更改。这两个组件互为兄弟,尺寸规则却截然相反;不要把一个组件的习惯带到另一个上。请参阅 add-interactivity。

回归测试覆盖:9,99-modifier-areas(src/areaScaleDrift.ts)会将一个 2x2x2 的 CameraModeArea 实体缩放至 2.5,并断言摄像机仍会在距中心 2 m 处释放。它只缩放了实体本身;父级场景的情况可从代码(逆 lossyScale)推知,并未通过测试验证。

Creator Hub / Inspector 支持: Creator Hub 现在为 AvatarModifierArea 提供了专用检查器面板,其中包含用于选择多个修饰符的下拉菜单(Hide Avatars、Disable Passports),以及用于编辑 excludeIds 的钱包地址列表编辑器。资产目录中还提供了一个“Avatar Modifier Area”智能项(utils 类别,半透明占位立方体)。编辑器会在后台将 area 字段与实体的 Transform.scale 同步(运行时会根据非零的 area 设置区域大小并抵消 scale;若 area 为零,则回退到 scale),因此通过操作手柄调整实体大小时,修饰符区域会自动更新。这种镜像同步是在编辑器运行时写入的,并非运行时绑定——场景运行后,area 将独立生效(见上方注意事项)。注意:检查器面板的下拉菜单仅提供 AMT_HIDE_AVATARS 和 AMT_DISABLE_PASSPORTS;目前 AMT_HIDE_NAMETAGS 只能通过 SDK 使用。已根据 Creator Hub 提交 a843390a 验证。

Avatar 移动设置

调整玩家的移动速度和跳跃高度:

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.75s)。参见 references/avatar-apis.md。

glidingFallingSpeed 是最大下降速度上限——它限制玩家滑翔时的下落速度,但不限制向上运动。滑翔时,持续作用于场景的力会增强至 1.5 倍,并且可以将玩家向上推;参见 player-physics 技能中的「滑翔时的力」。

  • 不能使用负值。 每个字段都会被限制为 >= 0;负值会变为 0。将字段设为 0 的效果与使用 InputModifier 屏蔽对应按键相同(例如,runSpeed: 0 ≈ disableRun: true)。
  • 场景设置优先于智能穿戴物。 如果场景和智能穿戴物都设置了某个字段,优先使用场景的值。
  • 仅在场景范围内生效。 该组件只会在玩家位于场景范围内时影响玩家,并且只影响本地玩家——要更改另一个 Avatar 的移动方式,代码必须在对方的客户端上运行。
  • 强制使用默认值也是实际需求: 跑酷或竞速场景可以显式写入默认值,避免智能穿戴物让持有者获得优势。

Creator Hub / Inspector 支持: Smart Items 包中提供了一个名为 "Locomotion Settings" 的智能物品(utils 类别,id f0fdd9ac-5451-4d84-9964-ea437b11b211)。它是一个 asset-packs::Script 物品(locomotion-settings.ts,类 LocomotionSettings),其中九个参数均为 Slider 字段,并预置了上述 SDK 默认值,因此无需编写 TypeScript 的用户也能使用相同的组件。start() 会调用 AvatarLocomotionSettings.createOrReplace(engine.PlayerEntity, {...});它提供两个 @action——Apply(重新写入配置值)和 Restore Defaults(AvatarLocomotionSettings.deleteFrom(engine.PlayerEntity)),因此其他智能物品可以触发速度提升并撤销该效果。它的占位物品是一个仅在编辑器中显示的立方体(asset-packs::Placeholder),在世界中不可见。在 Creator Hub 中打开场景时,优先使用它,而不是手动搭建实体(参见 creator-hub-mcp place_smart_item)。已根据 creator-hub 提交 cb097fff 验证。

限制移动(InputModifier)

在 engine.PlayerEntity 上使用 InputModifier,冻结或有选择地限制玩家的移动——适用于过场动画、锁定交互或受控的游戏机制。

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)

冻结时的行为: 重力和外力仍然生效,摄像机仍可旋转,全局输入事件仍可检测,玩家离开场景范围后限制会自动解除。

标准模式标志(均为可选布尔值,设置在 InputModifier.Mode.Standard({...}) 上):disableAll、disableWalk、disableJog、disableRun、disableJump、disableEmote、disableDoubleJump、disableGliding——这八项都包含在 SDK 类型(input_modifier.gen.d.ts)中并有文档说明,并非仅供协议使用。请注意,disableJog 与 disableWalk / disableRun 相互独立——慢跑是默认移动速度,因此仅禁用步行和跑步时,玩家仍然可以慢跑。

mode 可以通过两种等效方式构建——使用 InputModifier.Mode.Standard({...}) 辅助方法,或使用原始判别联合 { $case: 'standard', standard: {...} }。

提示: 与 triggerSceneEmote 搭配使用——在动画期间冻结玩家,动画结束后再移除 InputModifier。

过场动画模式请参见 advanced-input 技能。

传送玩家

~system/RestrictedActions 中的 movePlayerTo 是将玩家移动到某个位置的唯一方法。 设置 Transform.getMutable(engine.PlayerEntity).position 不起作用(参见本文件顶部的只读警告)。如需持续施加力(抬升、击退、推动、风力),请改用 player-physics 技能——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),让玩家到达升高的平台,而不仅仅是地面高度
立即传送
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),
})
在指定时长内平滑过渡

设置 duration 后,movePlayerTo 可通过 await 等待完成。返回值包含一个 success 布尔值——如果玩家通过输入中断移动,则为 false。

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')
	}
}
防止玩家中断过渡

结合使用 InputModifier 和 movePlayerTo,可在过渡期间锁定移动:

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 变更监听器

实时响应 Avatar 变更:

import {
	AvatarEmoteCommand,
	EmoteState,
	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).
// Each entry carries an optional `state` field (EmoteState enum) — see
// "Detecting when an emote finishes" above.
AvatarEmoteCommand.onChange(engine.PlayerEntity, (cmd) => {
	if (cmd) console.log('Emote:', cmd.emoteUrn, 'state:', cmd.state ?? EmoteState.ES_STARTED)
})

// 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)
})
其他锚点

除常用锚点外,完整列表还包括:

  • AvatarAnchorPointType.AAPT_POSITION — Avatar 脚部位置
  • AvatarAnchorPointType.AAPT_NAME_TAG — 名称标签上方
  • AvatarAnchorPointType.AAPT_LEFT_HAND / AAPT_RIGHT_HAND
  • AvatarAnchorPointType.AAPT_HEAD — 头部骨骼
  • AvatarAnchorPointType.AAPT_NECK — 颈部骨骼

在显示 Avatar 物品前需要检查玩家的钱包? 请参阅 nft-blockchain 技能,了解如何使用 getPlayer() 和 isGuest 检查钱包。

示例场景

引擎团队测试场景(针对真实引擎进行测试):

  • 100,102-avatar-attach-test — 在多个锚点上使用 AvatarAttach;通过 PlayerIdentityData 枚举每位玩家,并附加到 player.address;跟随实体通过 PlayerEntity 和已附加的 Transform 重建附加后的世界坐标。
  • 80,-1-scene-emotes
  • 4,23-emote-finish — 表情动作完成检测:AvatarEmoteCommand.onChange(engine.PlayerEntity, ...) 会将每个新增条目记录为 STARTED / FINISHED / INTERRUPTED;若缺少 state,则默认为 ES_STARTED。完整播放表情动作可看到 FINISHED;播放过程中走开可看到 INTERRUPTED。涵盖 triggerEmote(预定义表情动作)和非循环的 triggerSceneEmote。— triggerEmote、triggerSceneEmote(展示了一个故意命名错误、不符合 _emote.glb 格式且未播放的文件)、stopEmote、mask: AvatarMask.AM_UPPER_BODY,以及 loop: false + 遮罩(播放一次后返回移动状态)和 loop: true + 遮罩(重复播放,直到停止)。
  • 11,0-move-player-to-duration — 使用带有 duration 的 movePlayerTo,通过 .then() 读取 result.success,在滑行过程中用 InputModifier 锁定输入,并设置一个让化身在过渡过程中穿过的 CL_PHYSICS 障碍物。
  • 9,99-modifier-areas — AvatarModifierArea(AMT_HIDE_AVATARS)及运行时修改的 excludeIds,同时展示 CameraModeArea。
  • 10,99-avatar-modifier-hide-nametags — 使用 AMT_HIDE_NAMETAGS 的 AvatarModifierArea:隐藏名牌,同时保持化身可见。
  • 4,24-avatar-nametag — 在本地玩家、远程玩家和 AvatarShape NPC 上使用 AvatarNametag:涵盖所有颜色字段及其回退值、标签边缘情况、deleteFrom、AMT_HIDE_NAMETAGS,以及在 references/avatar-nametag.md 中说明的多人游戏玩家列表模块(src/modules/multiplayerRoster.ts)。
  • 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 — 表情动作遮罩:循环播放的 AvatarMask.AM_UPPER_BODY 场景表情动作 + AvatarAttach 锚点,用于固定同步箱子;使用 stopEmote 释放。还包含 loop: false + 遮罩组合(播放一次后上半身返回移动状态)和 loop: true + 遮罩组合(重复播放,直到停止),用于验证遮罩表情动作的循环标志是否生效。

组件字段详情请参阅 {baseDir}/../sdk-scenes/references/components-reference.md。 锚点、表情动作名称和事件回调请参阅 {baseDir}/references/avatar-apis.md。 AvatarNametag 字段、边缘情况和多人游戏玩家列表模式请参阅 {baseDir}/references/avatar-nametag.md。

评论

支持 Markdown