Loading...
Loading...
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 skill4agent add decentraland/sdk-skills npcs| Approach | Use when |
|---|---|
NPC Toolkit ( | GLB model, needs dialogue, walking, state machine behavior |
| AvatarShape | Needs to look like a Decentraland avatar (wearables, expressions) |
npm i dcl-npc-toolkitimport * 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.mdctextWrap: 'nowrap'"Yes""No thanks""Tell me more""Decline""I'm not interested.""I'm not interes"fontSizesizereferences/npc-library.mdcnpc.createaddDialog(entity)npcDataComponent.set(entity, ...)createDialogBubble(entity)talkBubblecreateDialogWindow()npcDataComponent"Cannot read properties of undefined (reading 'theme')"dcl-npc-toolkit/distcreateDialogWindow(portrait, sound)addDialog(...)npcDialogComponentnpcDataComponentopenDialogWindowactiveNPCnpcDialogComponentisActiveNpcSet()getTheme()npcDataComponent.get(activeNPC).themecreate()createNPCnpcDataComponentcreateDialogWindownpcDataComponentimport { 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: truefaceUserdcl-npc-toolkit/dist/faceUserSystem.jsfaceUserTransform.rotationTrackUserFlagfaceUserSystemengine.defineComponent(...)engine.defineComponent"Engine is already sealed. No components can be added at this stage"@dcl/ecs/dist/engine/index.jsawait import('./client-setup')importindex.tsawait import()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: 'npc-1', // unique identifier (required)
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 },
})idname: ''TransformTransformTweenTween.Mode.MoveTweenSequenceexpressionTriggerTimestampgetPlayer()userData.wearablesuserData.emotesAvatarShape.creategetPlayer()userData.emotes.length > 0AvatarShape.getMutable(npc).expressionTriggerId = 'wave'
AvatarShape.getMutable(npc).expressionTriggerTimestamp = 1expressionTriggerId'robot''wave'.glb'animations/Snowball_Throw_emote.glb'_emote.glbtriggerSceneEmotetriggerSceneEmoteexpressionTriggerId = userData.emotes[i]expressionTriggerIdexpressionTriggerTimestampAvatarShape.create(mannequin, {
id: 'mannequin-1',
name: 'Display',
wearables: ['urn:decentraland:matic:collections-v2:0x...:0'],
showOnlyWearables: true,
})AvatarShape{baseDir}/../player-avatar/references/avatar-apis.mdpointerEventsSystemimport { 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')
}
)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
}
})AvatarShapename: ''TransformTweenTweenSequenceAvatarAttachidgetPlayer().wearables.emotesexpressionTriggerIdexpressionTriggerId_emote.glbAvatarShapedcl-npc-toolkit