npx skills add ...
npx skills add decentraland/sdk-skills --skill npcs
Create NPCs (non-player characters) in Decentraland scenes. Two approaches: the NPC Toolkit library (dcl-npc-toolkit) for GLB NPCs with dialogue, movement, and state machines; and AvatarShape for avatar-look NPCs in wearables. Use when the user wants to add an NPC, shopkeeper, or quest giver — any non-player entity with behavior or dialogue. For live player data (position, profile, wearables) see player-avatar instead.
npx skills add decentraland/sdk-skills --skill npcs
Two approaches — choose based on what the NPC needs to do:
| Approach | Use when |
|---|---|
NPC Toolkit (dcl-npc-toolkit) | GLB model, needs dialogue, walking, state machine behavior |
| AvatarShape | Needs to look like a Decentraland avatar (wearables, expressions) |
The toolkit handles dialogue UI, movement along paths, animations, and interaction out of the box.
Install:
Basic usage:
For full dialogue scripting, movement paths, state machines, and all config options, see {baseDir}/references/npc-library.mdc — it covers:
Button labels are visually truncated. Dialog button labels render with textWrap: 'nowrap' in a fixed-width slot (default font 16, slot ~217px scaled). Anything past ~15 characters is silently clipped — no ellipsis. Use short labels like "Yes", "No thanks", "Tell me more", "Decline". Avoid full sentences and trailing punctuation (e.g. "I'm not interested." renders as "I'm not interes"). To fit longer text, drop fontSize (e.g. 12) or set size on the button. See references/npc-library.mdc "ButtonData fields".
Opening dialogs on an entity not created via npc.create requires addDialog(entity) and a minimal npcDataComponent.set(entity, ...) — see reference for the full setup.
Speech bubbles need createDialogBubble(entity) before talkBubble. Bubbles do not render question buttons; questions are HUD-only.
createDialogWindow() crashes the dialog UI unless you also set npcDataComponent. Symptom: "Cannot read properties of undefined (reading 'theme')" when the window opens. Why (verified against dcl-npc-toolkit/dist): createDialogWindow(portrait, sound) only calls addDialog(...) (sets npcDialogComponent); it does NOT set npcDataComponent. openDialogWindow then sets activeNPC, the npcDialogComponent guard (isActiveNpcSet()) passes, and getTheme() reaches npcDataComponent.get(activeNPC).theme — undefined on a standalone window. NPCs built with create()/createNPC have npcDataComponent set, so they never hit this. Fix — after createDialogWindow, set npcDataComponent with a valid theme and the minimal fields:
faceUser: true — do NOT parent a fixed-world object to a faceUser NPC. Why (verified — dcl-npc-toolkit/dist/faceUserSystem.js): faceUser rewrites the NPC entity's Transform.rotation every frame (via TrackUserFlag + faceUserSystem) to look at the player. Any child placed at a local offset inherits that rotation and orbits the NPC as the player moves, landing in unintended places (behind a door, occluded, unclickable). Fix: spawn such objects unparented at a computed world position (derive a stable world spot from a non-rotating reference like a door, plus the NPC's position). The same caveat applies to any entity whose Transform you rotate every frame.
dcl-npc-toolkit must be statically imported from the entry point. The toolkit calls engine.defineComponent(...) at module-load time. engine.defineComponent throws "Engine is already sealed. No components can be added at this stage" if it runs after the engine seals (verified — @dcl/ecs/dist/engine/index.js). A dynamic await import('./client-setup') defers that registration past the seal point. So any module that imports the toolkit must be reached via a static import from index.ts — not loaded later via await import(). In an authoritative-server scene, keep the client setup (which imports the toolkit) static and dynamically import only the server-only branch. See [[authoritative-server]].
Create an NPC that looks like a Decentraland player avatar, dressed in any wearables.
Notes:
id is required and must be unique per AvatarShape entity.name: '' (empty string) to suppress the name tag above the NPC's head.Transform position causes the NPC to walk/run to the destination (it does not teleport). This mutation DOES take effect — unlike the read-only player Transform. You can also drive movement with Tween (Tween.Mode.Move) + TweenSequence for scripted paths; the avatar plays its walk animation along the tween. For an NPC that continuously chases the player (or another moving target), use Tween.setMoveContinuous — do NOT re-create setMove tweens every frame, which causes visible jitter. See the animations-tweens skill ("PITFALL — a CONSTANTLY changing target") and the 79,-4-tween-following-cube test scene.expressionTriggerTimestamp as a Lamport timestamp to replay the same emote: first play = 0, second play = 1, etc.getPlayer() and pass userData.wearables and userData.emotes straight into AvatarShape.create. getPlayer() may return null / empty emotes on the first frames — poll in a system until userData.emotes.length > 0 before spawning (verified pattern in test scene 4,21).expressionTriggerId accepts either a built-in emote name ('robot', 'wave', ...) OR a scene-emote .glb path (e.g. 'animations/Snowball_Throw_emote.glb') — the same _emote.glb-suffixed files used with triggerSceneEmote. This is how you play a custom emote on an NPC (there is no triggerSceneEmote equivalent for AvatarShape entities). To cycle through the player's own emotes, set expressionTriggerId = userData.emotes[i].
Note: replaying the same expressionTriggerId value back-to-back requires bumping expressionTriggerTimestamp; setting a different id each time replays without touching the timestamp.
Useful for storefronts and wearable displays:
For the full AvatarShape field reference, body shape URNs, and common base wearable URNs, see {baseDir}/../player-avatar/references/avatar-apis.md.
AvatarShape entities are not clickable — they have no collider, so pointer events won't register on them directly. To let players interact with an AvatarShape NPC, use one of these approaches:
Attach an invisible collider to the same entity so pointerEventsSystem can detect clicks (see add-interactivity skill):
Trigger the interaction when the player walks near the NPC instead of requiring a click:
Engine-team test scenes for the AvatarShape approach (exercised against the real engine):
name: '' to hide the name tag.Transform (walks to target) and by Tween/TweenSequence; an AvatarAttach box on the NPC's hand via its id.getPlayer().wearables / .emotes, then cycle emotes via expressionTriggerId.expressionTriggerId to an _emote.glb path.These are AvatarShape-only scenes; they do not use dcl-npc-toolkit.