mcpField
P
player-avatar

player-avatar

github.com/decentraland開発4289 回インストール今日更新
リポジトリ

player-avatar について

Decentraland SDK7 のシーン開発者向けに、プレイヤーとアバターの機能を解説するスキルです。位置や移動、プロフィール、アタッチ方法を扱い、読み取り専用の Transform と追従方式の違いを説明します。

主な機能

  1. 01プレイヤーの位置と速度の読み取り
  2. 02対応 API によるプレイヤー移動
  3. 03プロフィールとアバターデータの取得
  4. 04アバターへのオブジェクト装着
  5. 05カメラ追従とボーン追従の選択

活用シーン

  1. 01距離判定や速度ロジックの実装
  2. 02プレイヤーのテレポートや移動設定の変更
  3. 03アバター装飾や照準用アイテムの追加

インストール

コマンドをコピーしてターミナルで実行します。

$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 のプレイヤーとアバターのシステム

重大:プレイヤーの 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 はコードを受け付け、システムも実行され、コンソールエラーも出ないのに、アバターがまったく動きません。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秒続くという条件は一度も満たされませんでしたが、同じしきい値を0.5秒間のウィンドウで判定したところ、最初の試行で検出されました。

代わりに値を蓄積し、ウィンドウが終了したらリセットします。

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)

プレイヤープロフィールデータ

プレイヤーの名前、ウォレットアドレス、ゲストかどうかを取得します。

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 を確認してください(ゲストにはオンチェーンの ID がありません)。

モバイル、他のプレイヤーの位置: モバイルでは、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 を使用してください。レルムやワールドにかかわらず、ここが正規の Catalyst です。ワールドサーバーは /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 が発生します)。

AvatarShape.create({ id: address }) は、id だけを指定してもウェアラブルを自動取得しません。bodyShape、wearables、色フィールドを明示的に指定しないと、アバターは何も着ていない状態で描画されます。

使用する API:

  • ローカルまたはシーン内のプレイヤー → getPlayer(userId)(同期、ウェアラブル/エモートを含む)。
  • シーン外のアドレス → fetchAvatarFromCatalyst(address)(非同期 HTTP)。

ヘルパー関数 fetchAvatarFromCatalyst の全容、使用方法のエンドツーエンドの例、注意点については、{baseDir}/references/catalyst-profile-fetch.md を参照してください。

アバターへのオブジェクトの取り付け

3D オブジェクトをプレイヤーのアバターに取り付けます。

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 を選ぶ前に、アイテムが装飾用か、照準に影響するものかを判断してください。 ボーンアンカーはアバターのスケルトンアニメーション(アイドル時の揺れ、歩行サイクル、ジェスチャー)を引き継ぎます。帽子/バックパック/光輪には最適ですが、手持ちの武器、照準レティクル、相対位置を安定させる必要があるものには適していません。以下の 手持ちアイテムと装飾アイテムを参照してください。

手持ちアイテムと装飾アイテム — AvatarAttach と engine.CameraEntity/engine.PlayerEntity への親設定

SDK7 には、「プレイヤーに追従するエンティティ」を実現する方法が 3 つあります。これらは互換性がありません。誤った方法を選ぶことは、SDK6 の「手持ちアイテム」のパターンを移植する際に最もよくある間違いです。また、最も見落としやすい失敗は、照準に影響するアイテム(銃、レティクル、懐中電灯)を engine.PlayerEntity の子にして、プレイヤーが上や下を向いてもアイテムがカメラのピッチに追従しないことに気づくケースです。

照準に影響する手持ちアイテムのデフォルト:Transform.parent = engine.CameraEntity。 engine.PlayerEntity を使うのは、ヨーのみ/ピッチなしの動作(プレイヤーが持ち運ぶが、照準には使わない、体に固定されたアイテム)が特に必要な場合に限ります。

目的使用するもの追従対象理由
照準に影響する手持ちアイテム — 銃、照準レティクル、懐中電灯など、周囲を見回すことで向きが変わるもの。ゲームプレイ用の手持ちアイテムの推奨デフォルトです。Transform.parent = engine.CameraEntity(さらに、カメラの「前方かつ下」の位置にローカル位置オフセットを設定)カメラのヨー + ピッチカメラの完全なトランスフォームに追従するため、上や下を向いた場合も含め、プレイヤーが見ている方向にアイテムが向きます。SDK6 の Attachable.FIRST_PERSON_CAMERA に相当します。照準が安定します(アニメーションによる揺れがありません)。
ヨーのみ/体に固定されたアイテム — プレイヤーが照準に使わない手持ちの盾、静的な松明、プレイヤーの向きにかかわらず水平を保つべき固定位置の持ち運びアイテム。Transform.parent = engine.PlayerEntity(さらに、手の高さ/前方距離にローカルオフセットを設定)プレイヤーのルート:足元の位置 + 体の ヨーのみ(ピッチなし)プレイヤーのルートトランスフォームに追従します。安定しています(アニメーションなし)が、プレイヤーが上や下を向いても水平を保ちます。銃や照準アイテムには不適切なデフォルトですが、体の向きだけに追従させたいアイテムには適しています。
装飾アイテム — 帽子、光輪、バックパック、ネームプレート、発光エフェクト、アバターに取り付けてほかのプレイヤーにも見える松明。anchorPointId(例:AAPT_HEAD、AAPT_SPINE、AAPT_LEFT_HAND)を指定した AvatarAttach実際のアニメーションボーン待機時の揺れ、歩行サイクル、ジェスチャーに合わせてアイテムが自然に動くため、体に取り付ける装飾として視覚的に適切です。照準には使えません — アニメーションによる揺れで、照準に影響するアイテムは使いものになりません。

照準に影響するアイテムのデフォルトとして 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,
})

API 名からそのまま解釈すると、「アバターの手に銃を持たせる」ための正しい SDK7 の方法に見えますが、手のボーンはアニメーションするため、実際には銃の照準を合わせられません。代わりに親子付けを使用してください。

// 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 は、大まかな追従モードにのみ対応していました。Attachable.FIRST_PERSON_CAMERA を使用していた SDK6 のシーンから手持ちアイテムを移行する場合、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' })
カスタムシーンエモート

⚠️ 重要なファイル名の要件: エモートの .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 は、プロジェクトルートからの相対パスであり、ディスク上のファイルの場所と完全に一致する必要があります。これはファイル名や _emote サフィックスによる検索ではなく、コンテンツの完全一致検索(SceneContent.TryGetHash(src))です。パスが間違っていると、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-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 が 'boolean' ではなく 'object' と表示される場合、ランタイムが値ではなく未解決の Promise をシーンに渡しています。これはクライアント/SDK の不具合であり、シーンの不具合ではありません。この動作を検出する目的もあって作られた 80,-6-explorer-ui-events テストシーンで確認済みです。

モバイル: stopEmote はモバイルアプリ v1.15.0(ドキュメントのコミット 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 — older clients)
			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)をデフォルトにしてください。古いクライアントのエントリーにはこのフィールドがなく、古いクライアントは FINISHED/INTERRUPTED も送信しません。そのため、フォールバックなしで終了シグナルを必須にしてゲームプレイを妨げないでください。
  • **ローカルプレイヤーのマスク付き(部分的な体の)エモートでは、ライフサイクルイベントは報告されません。**既知の制限です。
  • 再生完了通知に対応した DCL 2.0 デスクトップクライアントが必要です。
  • プロトコルの avatar_emote_command.proto(コミット 215d09c、フィールド 5、任意)および js-sdk-toolchain(コミット f858f905)で確認済みです。
エモートマスク(上半身のみ)

全身エモートは、プレイヤーが歩いたりジャンプしたりすると中断され、デフォルトの移動アニメーションに置き換わります。AvatarMask.AM_UPPER_BODY はアニメーションを腰から上に制限し、脚は移動アニメーションによって制御されます。つまり、上半身でアニメーションを再生しながら、プレイヤーは歩く、走る、ジャンプすることができます。使用例:木箱を運ぶ、たいまつを持つ、ジャグリングする、走りながら応援する。

triggerEmote と triggerSceneEmote はどちらも、アニメーションが動かすボーンを制限する省略可能な mask(@dcl/sdk/ecs からインポートする列挙型 AvatarMask)を受け取ります。

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: AM_UPPER_BODY で loop: false にすると、上半身のアニメーションが1回だけ再生され、その後、上半身は移動アニメーションに戻ります。マスクを指定して loop: true にすると、stopEmote({}) が呼び出されるまで繰り返されます。マスクの有無にかかわらず、ループフラグは適用されます。sdk7-test-scenes の 88,-13-avatar-masks および 80,-1-scene-emotes で検証済みです(コミット 1c0f394)。
  • モバイル対応: アバターマスク(上半身のみのエモート)は v1.13.0 以降、モバイルで利用可能 であり、もはや機能差はありません。また、v1.15.0 以降、マスク付きエモートは他のモバイルプレイヤーにも全身ではなく上半身のエモートとして表示されます。古いモバイルクライアントではマスク付きエモートが全身として再生されるため、全身で再生されても見た目に問題がないようにしてください。未解決の点:他のクライアントから届くエモート(Bevy-Web プレイヤーのシーンエモートや、LiveKit 経由の Unity プレイヤーのエモート)は、モバイルではまったく再生されない場合があります。docs のコミット 5f212e3(2026年10月、モバイル v1.15.0)で検証済みです。
  • マスク付きエモートは他のエモートと重ねられません: マスク付きエモートを開始すると、再生中の全身エモート(椅子に座るエモートを含む)が停止し、全身エモートを開始するとマスク付きエモートがキャンセルされます。マスクは移動アニメーションにのみ重なります。座っているプレイヤーには、座ったポーズを含む全身クリップを再生してください。作成方法の詳細は avatar-animations →「上半身(マスク付き)アニメーション」を参照してください。
  • protocol restricted_actions.proto / common/avatar_mask.proto(protocol 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 で作成してください。ベイク済みモデルは使わないでください。そうすることで、プレイヤーがその時点で着用しているものが反映されます。フィールドのマッピング(avatar?.eyesColor → eyeColor、avatar?.bodyShapeUrn → bodyShape)、expressionTriggerId によるコピーのポーズ設定、アバターが VisibilityComponent を無視することについては、npcs →「プレイヤーのコピー」を参照してください。

アバター修飾エリア

領域内でのアバターの外観や動作を変更します。

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]のように複数を組み合わせられます。AvatarModifierAreaコンポーネントには、領域のサイズを指定するarea: Vector3フィールドと、エンティティのTransform.scaleの両方が必要です。両方に同じサイズを設定してください。excludeIdsは影響を受けないウォレットアドレスの配列です(シーンの所有者を表示したままにする場合など)。実行時にAvatarModifierArea.getMutable(entity).excludeIds = [...]で変更できます。

AMT_HIDE_AVATARSはアバターとネームタグの両方を非表示にします。AMT_HIDE_NAMETAGSと組み合わせないでください(冗長です)。アバターを表示したままネームタグだけを非表示にしたい場合(ステージ、プレゼンテーション、視覚的にすっきりした体験など)に限り、AMT_HIDE_NAMETAGSを使用してください。AMT_HIDE_NAMETAGSはAMT_DISABLE_PASSPORTSと組み合わせられます。

ネームタグの非表示は頭部と胴体に基づきます: プレイヤーの頭部または胴体が領域内にある間だけ、ネームタグが非表示になります。領域の高さが足りないと、プレイヤーが二段ジャンプでその上に飛び越えたとき、ネームタグが一時的に再表示されます。想定される動きの範囲をカバーできる高さにしてください。

Creator Hub/インスペクターのサポート: Creator Hubには、AvatarModifierArea専用のインスペクターパネルが追加されました。モディファイアー(Hide Avatars、Disable Passports)を選択できる複数選択ドロップダウンと、excludeIds用のウォレットアドレスリストエディターが用意されています。アセットカタログのutilsカテゴリには、「Avatar Modifier Area」スマートアイテム(半透明のプレースホルダーキューブ)があります。エディターはareaフィールドをエンティティのTransform.scaleと同期して、見えない状態で維持します(ランタイムでは領域のサイズとしてscaleではなくareaが読み取られます)。そのため、ギズモでエンティティのサイズを変更すると、モディファイアー領域も自動的に更新されます。注:インスペクターパネルのドロップダウンで設定できるのはAMT_HIDE_AVATARSとAMT_DISABLE_PASSPORTSのみです。現時点ではAMT_HIDE_NAMETAGSはSDK専用です。creator-hubのコミットa843390aで確認済みです。

アバターの移動設定

プレイヤーの移動速度とジャンプの高さを調整します。

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)。
  • スマートウェアラブルよりシーンが優先されます。 シーンとスマートウェアラブルの両方がフィールドを設定した場合、シーンの値が優先されます。
  • シーンの範囲内に限られます。 このコンポーネントがプレイヤーに適用されるのは、そのプレイヤーがシーンの範囲内にいる間だけで、ローカルプレイヤーに限られます。別のアバターの移動を変更するには、そのアバターのクライアント上でコードを実行する必要があります。
  • デフォルト値を適用することにも、実用的な用途があります。 パルクールやレースのシーンでは、デフォルト値を明示的に設定することで、スマートウェアラブルによって所有者が有利にならないようにできます。

Creator Hub/Inspector のサポート: Smart Items パックには、**「Locomotion Settings」**スマートアイテム(utils カテゴリ、id f0fdd9ac-5451-4d84-9964-ea437b11b211)が含まれています。これは asset-packs::Script アイテム(locomotion-settings.ts、クラス LocomotionSettings)で、9つのパラメーターは上記のSDKのデフォルト値を初期値とする Slider フィールドです。そのため、コードを書かないユーザーも、TypeScriptを記述せずに同じコンポーネントを利用できます。start() は AvatarLocomotionSettings.createOrReplace(engine.PlayerEntity, {...}) を呼び出します。また、2つの @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 — 8つすべてがSDKの型(input_modifier.gen.d.ts)に含まれ、ドキュメントにも記載されています。プロトコル専用ではありません。disableJog は disableWalk/disableRun とは別のフラグです。ジョグがデフォルトの移動速度なので、歩行と走行だけを無効にしても、プレイヤーはジョグで移動できます。

mode は同等の2つの方法で作成できます。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 (optional) — 移動後にカメラを向ける空間上の点
  • avatarTarget (optional) — 移動後にアバターを向ける空間上の点
  • duration (optional) — 秒単位の遷移時間。指定すると、移動を await できます

制約:

  • この機能を使用するには、プレイヤーがすでにシーンの境界内にいる必要があります
  • 移動先の位置もシーンの境界内にある必要があります
  • 遷移中、アバターはコライダーを通り抜けます(確認済み:経路上に配置した 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 を指定したスムーズな遷移

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)
}
アバター変更リスナー

アバターの変更にリアルタイムで反応します。

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 — アバターの足元の位置
  • AvatarAnchorPointType.AAPT_NAME_TAG — ネームタグの上
  • AvatarAnchorPointType.AAPT_LEFT_HAND / AAPT_RIGHT_HAND
  • AvatarAnchorPointType.AAPT_HEAD — 頭部のボーン
  • AvatarAnchorPointType.AAPT_NECK — 首のボーン

アバターアイテムを表示する前にプレイヤーのウォレットを確認する必要がありますか? getPlayer() と isGuest を使ったウォレット確認については、nft-blockchain スキルを参照してください。

シーンの例

エンジンチームのテストシーン(実際のエンジンで検証済み):

  • 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 + マスク(1 回再生して移動アニメーションに戻る)と loop: true + マスク(停止するまで繰り返す)。
  • 11,0-move-player-to-duration — duration を指定した movePlayerTo、.then() による result.success の読み取り、スライド中に入力をロックする InputModifier、および遷移の途中でアバターが通過する CL_PHYSICS 障害物。
  • 9,99-modifier-areas — 実行時に excludeIds を変更する AvatarModifierArea(AMT_HIDE_AVATARS)と CameraModeArea。
  • 10,99-avatar-modifier-hide-nametags — AMT_HIDE_NAMETAGS を指定した AvatarModifierArea:アバターを表示したままネームタグを非表示にします。
  • 0,1-input-modifier — ヘルパーと生の $case 形式の両方を使い、Standard フラグ(disableAll/Walk/Jog/Run/Jump/Emote)をすべて切り替える InputModifier。
  • 80,-4-restricted-actions — movePlayerTo(高い y 値、avatarTarget のみの旋回を含む)、triggerEmote、triggerSceneEmote、teleportTo、openExternalUrl。
  • 88,-13-avatar-masks — エモートマスク:ループする AvatarMask.AM_UPPER_BODY のシーンエモートと、同期した木箱を保持する AvatarAttach アンカー、および解放する stopEmote。また、マスク付きエモートのループフラグが正しく反映されることを検証するため、loop: false + マスクの組み合わせ(1 回再生後、上半身が移動アニメーションに戻る)と、loop: true + マスクの組み合わせ(停止するまで繰り返す)も含まれます。

コンポーネントのフィールドの詳細については、{baseDir}/../sdk-scenes/references/components-reference.md を参照してください。 アンカーポイント、エモート名、イベントコールバックについては、{baseDir}/references/avatar-apis.md を参照してください。

コメント