mcpField
N
npcs

npcs

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

关于 npcs

为 Decentraland 场景创作者提供 NPC 实现指导,涵盖 GLB 对话型 NPC 与 AvatarShape 两种路径,并说明移动、动画和界面方面的注意事项。

主要功能

  1. 01NPC Toolkit 对话与移动
  2. 02路径和状态机行为
  3. 03AvatarShape 外观与表情
  4. 04AvatarNametag 标签牌
  5. 05NPC 动画与移动注意事项

适用场景

  1. 01创建带对话和路径行为的 GLB NPC
  2. 02制作穿戴 Decentraland 服饰的头像 NPC
  3. 03为 NPC 添加表情或名称标签

安装

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

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

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

附带文件references/npc-library.mdc

详细介绍

Decentraland 中的 NPC

有两种方案——根据 NPC 的用途进行选择:

方案适用场景
NPC Toolkit(dcl-npc-toolkit)GLB 模型,需要对话、行走和状态机行为
AvatarShape需要呈现 Decentraland Avatar 的外观(可穿戴物品、表情)

方案 1 — NPC Toolkit(基于 GLB)

该工具包开箱即提供对话 UI、沿路径移动、动画和交互功能。

安装:

npm i dcl-npc-toolkit

基本用法:

import * as npc from 'dcl-npc-toolkit'
import { Vector3, Quaternion } from '@dcl/sdk/math'

const dialogs: npc.Dialog[] = [{ text: 'Hello there!', isEndOfDialog: true }]

const npcEntity = npc.create(
  { position: Vector3.create(8, 0, 8), rotation: Quaternion.fromEulerDegrees(0, 180, 0) },
  {
    type: npc.NPCType.CUSTOM,
    model: { src: 'models/guard.glb' },
    idleAnim: 'Idle',
    walkingAnim: 'Walk',
    hoverText: 'Talk',
    onlyExternalTrigger: false,
    onActivate: () => {
      // called when player activates the NPC
      npc.talk(npcEntity, dialogs)
    },
  }
)

有关完整的对话脚本、移动路径、状态机和所有配置选项,请参阅 {baseDir}/references/npc-library.mdc,其中涵盖:

  • 对话类型(交谈、按钮选项、NPC 回复)
  • 移动到指定位置和沿路径行走
  • 状态管理(任务发布者、守卫、商店模式)
  • 多人游戏注意事项
  • 性能优化
注意事项(NPC Toolkit)
  • 对话按钮标签会被截断。 对话按钮标签在固定宽度的区域内以 textWrap: 'nowrap' 渲染(默认字体大小为 16,区域缩放后约为 217px)。超过约 15 个字符的部分会被静默裁剪,不会显示省略号。请使用简短标签,例如 "Yes"、"No thanks"、"Tell me more"、"Decline"。避免使用完整句子和末尾标点(例如,"I'm not interested." 会显示为 "I'm not interes")。若要容纳较长文本,请减小 fontSize(例如设为 12),或设置按钮的 size。请参阅 references/npc-library.mdc 中的“ButtonData fields”。

  • 在非通过 npc.create 创建的实体上打开对话框,需要调用 addDialog(entity),并执行最精简的 npcDataComponent.set(entity, ...) 设置——完整配置请参阅参考文档。

  • 调用 talkBubble 前,必须先对话实体调用 createDialogBubble(entity)。气泡不会渲染问题按钮;问题仅显示在 HUD 中。

  • createDialogWindow() 会导致对话 UI 崩溃,除非同时设置 npcDataComponent。 症状:窗口打开时出现 "Cannot read properties of undefined (reading 'theme')"。原因(已通过检查 dcl-npc-toolkit/dist 确认):createDialogWindow(portrait, sound) 只会调用 addDialog(...)(设置 npcDialogComponent),不会设置 npcDataComponent。随后 openDialogWindow 会设置 activeNPC,npcDialogComponent 的守卫条件(isActiveNpcSet())通过,而 getTheme() 会访问 npcDataComponent.get(activeNPC).theme——独立窗口中的该值为 undefined。通过 create()/createNPC 创建的 NPC 已设置 npcDataComponent,因此不会遇到此问题。解决方法——调用 createDialogWindow 后,使用有效主题和最精简的必需字段设置 npcDataComponent:

    import { createDialogWindow } from 'dcl-npc-toolkit'
    import { npcDataComponent } from 'dcl-npc-toolkit/dist/npc'
    import { lightTheme } from 'dcl-npc-toolkit/dist/ui'
    
    const window = createDialogWindow(portrait, sound)
    npcDataComponent.set(window, {
      introduced: false, inCooldown: false, coolDownDuration: 5,
      faceUser: undefined, walkingSpeed: 2, walkingAnim: undefined,
      pathData: undefined, currentPathData: [], manualStop: false,
      pathIndex: 0, state: 'standing', idleAnim: 'Idle', hasBubble: false,
      turnSpeed: 2, theme: lightTheme, bubbleXOffset: 0, bubbleYOffset: 0,
      lastPlayedAnim: 'Idle', volume: 0.5,
    })
    
  • faceUser: true——请勿将固定于世界坐标的对象设为 faceUser NPC 的子对象。 原因(已通过检查 dcl-npc-toolkit/dist/faceUserSystem.js 确认):faceUser 会通过 TrackUserFlag 和 faceUserSystem,在每帧重写 NPC 实体的 Transform.rotation,使其朝向玩家。放置在局部偏移位置的任何子对象都会继承此旋转,并随着玩家移动而绕 NPC 转动,最终出现在意料之外的位置(门后、被遮挡、无法点击)。解决方法:在不设置父级的情况下生成此类对象,并使用计算出的世界坐标(从门等不会旋转的参照物推算出稳定的世界位置,再加上 NPC 的位置)。对于任何每帧旋转其 Transform 的实体,同样适用此注意事项。

  • 必须从入口文件静态导入 dcl-npc-toolkit。 该工具包会在模块加载时调用 engine.defineComponent(...)。如果此调用发生在引擎封闭后,engine.defineComponent 会抛出 "Engine is already sealed. No components can be added at this stage"(已通过检查 @dcl/ecs/dist/engine/index.js 确认)。动态执行 await import('./client-setup') 会将此注册推迟到引擎封闭之后。因此,任何导入该工具包的模块都必须通过 index.ts 中的静态 import 引入,不能稍后通过 await import() 加载。在权威服务器场景中,请保持客户端设置(其中导入了该工具包)为静态导入,仅动态导入仅供服务器使用的分支。请参阅 [[authoritative-server]]。


方法 2 — AvatarShape(Decentraland 化身外观)

创建一个外观类似 Decentraland 玩家化身、穿戴任意可穿戴物品的 NPC。

import { engine, Transform, AvatarShape } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const npc = engine.addEntity()
Transform.create(npc, { position: Vector3.create(8, 0, 8) })

AvatarShape.create(npc, {
  id: 'guard-0001',          // unique; its last 4 characters show beside the name
  name: 'Guard',             // display name shown above head
  bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', // or BaseFemale
  wearables: [
    'urn:decentraland:off-chain:base-avatars:eyebrows_00',
    'urn:decentraland:off-chain:base-avatars:mouth_00',
    'urn:decentraland:off-chain:base-avatars:eyes_00',
    'urn:decentraland:off-chain:base-avatars:blue_tshirt',
    'urn:decentraland:off-chain:base-avatars:brown_pants',
    'urn:decentraland:off-chain:base-avatars:classic_shoes',
    'urn:decentraland:off-chain:base-avatars:short_hair',
  ],
  hairColor: { r: 0.92, g: 0.76, b: 0.62 }, // RGB 0–1
  skinColor: { r: 0.94, g: 0.85, b: 0.6 },
})

注意:

  • 始终包含眉毛、嘴巴和眼睛可穿戴物品,否则化身不会渲染面部特征。
  • 每个 AvatarShape 实体都必须设置唯一的 id。Explorer 会在名称后以淡色文本显示 id 的最后 4 个字符,就像未认领的玩家名称会显示其地址一样。NPC 没有个人资料,因此始终会显示后缀,也没有字段可以关闭它:id: 'guard-1' 且 name: 'Guard' 会渲染为“Guard”,后面附加淡色的“rd-1”。请选择最后 4 个字符有意为之的 ID,例如 guard-0001(少于 4 个字符的 ID 会显示 0000)。移除整个标签是去掉后缀的唯一方法(unity-explorer NametagPlacementSystem、NametagElement.BuildName、main 2026-10)。
  • 将 name: ''(空字符串)设为空值,以隐藏 NPC 头顶的名称标签,包括后缀。
  • 移动 Transform 位置会使 NPC 走向或跑向目的地(而非瞬移)。此修改确实生效,与只读的玩家 Transform 不同。你也可以使用 Tween(Tween.Mode.Move)和 TweenSequence 控制移动,创建脚本化路径;化身会在补间动画期间播放行走动画。若要让 NPC 持续追逐玩家(或其他移动目标),请使用 Tween.setMoveContinuous,不要每帧重新创建 setMove 补间动画,否则会出现明显抖动。请参阅 animations-tweens 技能(“陷阱 — 目标持续变化”)和 79,-4-tween-following-cube 测试场景。
  • 使用 expressionTriggerTimestamp 作为 Lamport 时间戳来重播相同的表情动作:第一次播放 = 0,第二次播放 = 1,依此类推。
  • **克隆当前玩家的外观:**读取 getPlayer(),并将 userData.wearables 和 userData.emotes 直接传给 AvatarShape.create。在最初几帧中,getPlayer() 可能返回 null / 空的表情动作列表;请在 system 中轮询,直到 userData.emotes.length > 0 后再生成(已在测试场景 4,21 中验证此模式)。
NPC 名称上方的标签牌(AvatarNametag)

AvatarNametag 可用于任何带有 AvatarShape 的实体,而不仅限于玩家。将它附加到同一实体上:

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

AvatarNametag.create(npcEntity, { label: 'Shopkeeper' })
  • 在 AvatarShape 上设置 name: '',即可只显示标签牌,不会在其下方显示空的原生名称框——只用头衔为 NPC 添加标签。
  • label 为必填项,但可以只包含空白字符。label: ' ' 会绘制一个没有文字的纯色标签牌(空格越多,标签牌越宽),例如用于显示没有标题的队伍颜色徽章。
  • labelColor、backgroundColor、borderColor 是可选的 Color3 字段;未设置时会回退到原生名称标签颜色。
  • 移动端尚未实现此功能——移动端玩家看到的 NPC 不会带有标签牌,因此如果标题很重要,请通过其他方式保持其可见(例如使用 TextShape)。

字段表、边缘情况和注意事项:请参阅 player-avatar 技能(“自定义名称标签牌”)和 {baseDir}/../player-avatar/references/avatar-nametag.md。

在 AvatarShape NPC 上播放表情动作
AvatarShape.getMutable(npc).expressionTriggerId = 'wave'
AvatarShape.getMutable(npc).expressionTriggerTimestamp = 1

expressionTriggerId 可接受内置表情动作名称('robot'、'wave' 等)或场景表情动作 .glb 路径(例如 'animations/Snowball_Throw_emote.glb')——与 triggerSceneEmote 使用的 _emote.glb 后缀文件相同。你可以用这种方式在 NPC 上播放自定义表情动作(AvatarShape 实体没有与之对应的 triggerSceneEmote)。若要循环播放玩家自己的表情动作,请设置 expressionTriggerId = userData.emotes[i]。 在为 NPC 姿势(坐下、闲置变体)编写自定义动画片段之前,请先检查内置表情动作。 Explorer 内置了 21 个表情动作,expressionTriggerId 可按名称接受这些动作;坐姿动作会循环播放并保持姿势,无需重新触发(已在世界中通过 AvatarShape 验证,SDK 7.30.1,2026-10-12)。包含 ID 和循环标记的完整列表(唯一可信来源):{baseDir}/../avatar-animations/references/embedded-emotes.md。内置坐姿通常比手动编写的动画更自然。接下来,Creator Hub 的 Avatar Animations 包提供 45 个可下载的 _emote.glb 动画片段(凳子、坐垫、豆袋椅、矮墙、国际象棋、钓鱼、饮酒、制作物品……),expressionTriggerId 可通过文件路径播放这些动画片段;带预览的目录位于 {baseDir}/../avatar-animations/references/animation-catalog.md。只有在两者都不合适时,才编写自定义动画片段(avatar-animations)。

注意:连续重播相同的 expressionTriggerId 值时,需要递增 expressionTriggerTimestamp;每次设置不同的 ID,则无需修改时间戳即可重播。

自定义动画片段不会在 AvatarShape 上循环播放。 无论动画片段自身的循环标记如何,Explorer 都会将每个面向 AvatarShape 的场景表情动作请求设为 loop: false(AvatarShapeHandlerSystem 中的注释写道:“目前不支持在 SDK AvatarShapes 上循环播放场景表情动作”——unity-explorer main 2026-10)。只有上文提到的内置循环表情动作能够保持姿势。编写表情动作、通过 triggerSceneEmote({ loop: true }) 让玩家在实时场景中循环播放,以及在 AvatarShape 上触发表情动作,是三种不同的播放路径。要让自定义姿势持续保持:编写一个较长的静态动画片段,并在其结束前不久递增 expressionTriggerTimestamp 重新触发。请在世界中验证衔接处——尚未检查重新触发是否会产生明显跳变。

隐藏 AvatarShape 不等于改变位置。 将其移出视野(例如移到地下 60 m)会使其走到那里,而且会被看见,因为 Explorer 会对 AvatarShape 的移动进行插值(参见“移动 NPC 会取消表情动作”)。有效的方法是将 Transform.scale 设为接近零的值(0.001),之后再恢复(世界内测试,Unity Explorer 0.186,2026-10;尚未测试 AvatarShape 上的 VisibilityComponent)。Primitives、GLB、AvatarShape、名称标签、灯光和碰撞体的隐藏方式各不相同——不要假设一个“隐藏此实体”的方案适用于所有对象。

移动 NPC 会取消表情动作。 在表情动作播放期间写入 AvatarShape 的 Transform.position,或对其运行 Tween,都会导致表情动作中断,头像退回到行走动画周期——Explorer 会根据每帧的 Transform 差值重新计算运动混合(SDKAvatarShapesMotionSystem.UpdateAnimations;已对照 unity-explorer Explorer/Assets/DCL/Character/CharacterMotion/Systems/SDKAvatarShapesMotionSystem.cs 验证;这一现象由资产所有者报告,行为已在世界中观察到,Unity Explorer v0.186.0-alpha,SDK 7.30.1,2026-10-10)。要让 NPC 在表情动作播放期间移动——例如尖叫着沉入地板、向一侧瘫倒——请将位移烘焙到 _emote.glb 动画片段中的根节点或臀部动作里;Explorer 会在 AvatarShape 上播放该动画(世界内观察,同一构建版本和日期)。

NPC 不支持蒙版表情动作。 mask?: AvatarMask(仅上半身)只存在于 TriggerSceneEmoteRequest 中,即玩家自己的 triggerSceneEmote;PBAvatarShape 没有 mask 字段,因此 expressionTriggerId 始终作用于全身。已对照 @dcl/protocol decentraland/kernel/apis/restricted_actions.gen.d.ts 和 @dcl/ecs decentraland/sdk/components/avatar_shape.gen.d.ts 验证(7.30.1)。

同一会话中由资产所有者报告,未经验证——在据此进行设计前请先确认:NPC 首次通过 expressionTriggerId 指向场景 _emote.glb 时会出现可见延迟(加载动画片段);曾尝试在场景开始时,于屏幕外的 AvatarShape 上播放一次作为预热缓存的变通方法,效果尚未确认。

玩家副本

玩家的仿制形象——一排「你」、在商店里摆姿势的克隆体、会挥手回应的双胞胎——是一个由实时 getPlayer() 数据驱动的 AvatarShape(字段映射见 player-avatar 上方的克隆体要点),因此它穿戴的会是玩家身上的所有装扮。

AvatarShape 无法逐帧操控。 它的移动由引擎驱动(SDKAvatarShapesMotionSystem.cs,unity-explorer 主线 2026-10):每帧都会以 4 m/s 的速度,从当前位置朝编写的 Transform.position 移动;目标距离达到或超过 1.5 m 后速度会更快——这取决于帧率,因为速度每帧都会重置为 4,并以 100·dt 递增(60 fps 时约为 5.7 m/s,30 fps 时为 7.3 m/s,只有低于约 17 fps 时才会达到名义上的 10 m/s)——并且它会转向面对移动方向(slerp,6.5/s);只有到达后才会应用编写的旋转。在实体位置上添加 Tween 会抑制引擎移动,在实体旋转上添加 Tween 会抑制自动朝向,但一个实体只能持有一个 Tween,因此无法同时控制二者。将玩家的镜像位置传给它,只会得到一个以步行速度落后的分身,而且玩家后退时它会转身——这是所有者在 2026-10-09 报告的镜像分身问题(「延迟让感觉非常奇怪」、「倒着走时它会朝向另一边」);此前已尝试逐帧写入 Transform,以及以镜像速度持续移动的 tween。对于镜像分身、影子、回放幽灵,或任何必须逐帧匹配玩家姿势的对象,请使用带有 Animator 的绑定人形 GLB(根据玩家速度切换 idle / walk 动画片段),并在每帧直接写入它的 Transform。它不会穿戴玩家的外观,这是需要付出的代价。带动画片段的绑定人形模型:add-3d-models → references/model-catalog.md,查找 [anim: …] 列出 idle 和 walk 的 character/… 条目(例如 base-character、berserker-npc;arissa3 有 idle 和 running)。mannequin 返回的是没有动画片段的展示假人。

副本在外观上能有哪些区别,取决于可从 catalyst 读取的可穿戴物类别:

https://peer.decentraland.org/lambdas/collections/wearables?wearableId=<urn>&wearableId=<urn>

每个条目都会返回 data.category、data.hides、data.replaces 和 data.representations[].overrideHides / .overrideReplaces(已通过 curl 验证,2026-10-12:urn:decentraland:off-chain:base-avatars:red_bandana → category: "tiara"、replaces: ["mask","hat","helmet"])。可据此选择确实会显示出来的替换装扮。

同一会话中所有者报告的内容,尚未通过源代码验证——请将其视为线索,围绕这些内容设计前先在游戏内确认:

  • 穿戴的皮肤会替换整个身体,因此新增的可穿戴物、头发、肤色和眼睛颜色都不会显示在副本上。
  • 头盔会隐藏头发、头饰、耳环、眼镜和胡须。
  • AvatarShape 无法强制渲染被隐藏的可穿戴物。

始终有效的备用方案: 改变副本的 Transform 缩放比例——例如,更高 1.07、更矮 0.93、更宽 1.10。它不受可穿戴物影响,并且在一排形象中一眼就能看出来。

假人模式(展示可穿戴物,不显示身体)

适用于店面和可穿戴物展示:

AvatarShape.create(mannequin, {
  id: 'mannequin-0001',
  name: 'Display',
  wearables: ['urn:decentraland:matic:collections-v2:0x...:0'],
  showOnlyWearables: true,
})

关于完整的 AvatarShape 字段参考、身体形状 URN 和常见基础可穿戴物 URN,请参阅 {baseDir}/../player-avatar/references/avatar-apis.md。


为 AvatarShape NPC 添加交互功能

AvatarShape 实体无法点击——它们没有碰撞体,因此指针事件无法直接在其上触发。若要让玩家与 AvatarShape NPC 交互,请使用以下方法之一:

方案 A — 添加 MeshCollider 以支持点击交互

在同一实体上附加一个不可见的碰撞体,以便 pointerEventsSystem 检测点击(请参阅 add-interactivity 技能):

import { MeshCollider, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'

// invisible cylinder collider roughly matching avatar size.
// Pointer clicks require the CL_POINTER collider layer; MeshCollider defaults to
// all layers (which includes CL_POINTER), so an explicit layer is optional here.
// If you set a layer, include ColliderLayer.CL_POINTER or clicks won't register.
MeshCollider.setCylinder(npc)

pointerEventsSystem.onPointerDown(
  { entity: npc, opts: { button: InputAction.IA_POINTER, hoverText: 'Talk' } },
  () => {
    console.log('Player clicked NPC')
  }
)
方案 B — 基于距离的交互

当玩家走近 NPC 时触发交互,而不是要求玩家点击:

import { engine, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const INTERACT_DISTANCE = 4

engine.addSystem(() => {
  const playerPos = Transform.get(engine.PlayerEntity).position
  const npcPos = Transform.get(npc).position
  const dist = Vector3.distance(playerPos, npcPos)
  if (dist < INTERACT_DISTANCE) {
    // start dialogue or other interaction
  }
})

示例场景

引擎团队针对 AvatarShape 方案的测试场景(使用真实引擎进行测试):

  • 4,20-avatar-shape — 生成带有基础 Avatar 可穿戴物品和链上收藏可穿戴物品 URN 的 AvatarShape NPC;设置 name: '' 可隐藏姓名标签。
  • 4,19-avatar-shape-movement — 通过修改 Transform(走向目标)和使用 Tween/TweenSequence 移动 AvatarShape;通过 NPC 手上的 id 添加一个 AvatarAttach 方块。
  • 4,21-avatar-shape-emotes — 使用 getPlayer().wearables / .emotes 将本地玩家克隆到 NPC 上,然后通过 expressionTriggerId 循环播放表情动作。
  • 4,22-avatar-shape-scene-emotes — 将 expressionTriggerId 设置为 _emote.glb 路径,在 NPC 上播放自定义场景表情动作。

这些场景仅使用 AvatarShape,不使用 dcl-npc-toolkit。

评论

支持 Markdown