migrate-sdk6-to-sdk7 について
既存の Decentraland SDK6 シーンを SDK7 に移行するコーディングエージェント向けスキルです。SDK バージョンの確認や API の変換を案内し、既存のシーンのエントリーパスを維持します。
主な機能
- 01SDK6 プロジェクトの確認
- 02SDK6 から SDK7 への API 対応表
- 03順序立てた移行手順
- 04エンティティとシステムの変換案内
- 05静的シーンの任意移行
活用シーン
- 01既存の SDK6 シーンの移行
- 02エンティティとコンポーネント変換の確認
- 03シーン設定の安全な更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill migrate-sdk6-to-sdk7インストール後、Claude Code で/migrate-sdk6-to-sdk7を入力して読み込まれたか確認します。
同梱ファイルreferences/api-mapping.mdreferences/migration-example.md
詳細
Decentraland SDK6 シーンを SDK7 に移行する
Creator Hub エージェント内では無効です。 Creator Hub はこのスキルを
SKILL.mdなしで同梱しているため、アプリに組み込まれた AI アシスタントでは実行されません。Creator Hub は独自の UI を通じてプロジェクトのセットアップを行い、SDK7 のみをサポートします。スキルのreferences/ファイルは引き続き同梱されるため、ほかのスキルから相対パスを解決できます。Creator Hub のアシスタントとしてユーザーから依頼された場合は、アプリ独自の手順を案内してください。Creator Hub の外部(Claude Code、Cursor、SDK エージェント)では、スキルは通常どおり動作します。
これは移植用スキルであり、ひな型作成用スキルではありません。 ディスク上に既存の SDK6 プロジェクトがあることを前提とします。新しい SDK7 シーンを作成する場合は、[[create-scene]] を使用してください。
ルール:作業を始める前に、SDK6 であることを必ず確認する
以下のファイルをこの順番で確認してください。プロジェクトを SDK6 として扱うには、SDK6 の判定条件がすべて揃っている必要があります。一部だけ一致する場合は、多くの場合、別の対応が必要な移行途中のプロジェクトです。
| 確認項目 | SDK6 の判定条件 | SDK7 の判定条件 |
|---|---|---|
package.json の依存関係 | decentraland-ecs | @dcl/sdk |
package.json のスクリプト | build-ecs、dcl start | sdk-commands start、sdk-commands build |
scene.json | runtimeVersion がない、または ecs7 がない | "runtimeVersion": "7" および/または "ecs7": true |
| ソースコードのインポート | 明示的な @dcl/sdk/ecs のインポートがない。シンボル(Entity、Transform、Vector3、engine)はグローバル | ほとんどのものが @dcl/sdk/ecs および @dcl/sdk/math からインポートされる |
| ソースコードのパターン | new Entity()、entity.addComponent(...)、@Component('name')、class X implements ISystem、OnPointerDown、GLTFShape、Input.instance | engine.addEntity()、Transform.create(entity, ...)、engine.defineComponent(...)、pointerEventsSystem.onPointerDown、GltfContainer |
ルール:scene.json の main フィールドは変更しない
SDK6 プロジェクトにすでに設定されている main の値(通常は "bin/game.js")を維持してください。新しいシーンにおける SDK7 のデフォルトは bin/index.js ですが、sdk-commands build は scene.json の main が指定するパスにバンドルを書き出します(確認済み:sdk-commands の bundle.ts は出力ファイルを sceneJson.main から設定します)。そのため、既存の bin/game.js は変更せずに引き続き動作します。ユーザーから明示的に依頼されない限り、bin/index.js に名前を変更しないでください。
scene.json に "runtimeVersion": "7" を追加し(ECS6 固有の設定があれば削除し)、そのうえで作業を進めてください。scene.json のスキーマ全体については、[[create-scene]] を参照してください。
ルール:コンポジット優先のルールは移行にも適用されます
移行後、静的なシーン要素(モデル、ライト、プリミティブ、シーン読み込み時に配置するテキスト)はすべて、TypeScript ではなく assets/scene/main.composite に配置します。SDK6 のシーンにはコンポジットがなく、すべてのエンティティがコードで作成されます。それらをコンポジットに移す作業は、別途行う任意の工程です。_最低限の移行_では、まずシーンを実行できるよう、エンティティを engine.addEntity() のまま TypeScript に残します。
作業の順序:
- まず、SDK7 の TypeScript への移植を実行できる状態にします(エンティティは引き続きコードで作成します)。
- その後、必要に応じて静的エンティティを
assets/scene/main.compositeに移します。コンポジット優先のルールについては [[create-scene]] を、フォーマットについては [[composites]] を参照してください。
概念の転換 — コードを書く前にお読みください
SDK6 では、エンティティはコンポーネントがアタッチされたオブジェクトとしてモデル化されていました。SDK7 は真の ECS であり、エンティティは不透明な ID にすぎず、コンポーネントは純粋なデータです。
| 概念 | SDK6 | SDK7 |
|---|---|---|
| エンティティの作成 | const e = new Entity() の後に engine.addEntity(e) | const e = engine.addEntity()(ID を返します) |
| エンティティの型 | メソッド(addComponent、getComponent、setParent、alive)を持つ IEntity/Entity クラス | Entity = 不透明な整数 ID。メソッドはありません |
| コンポーネントの追加 | entity.addComponent(new Transform({...})) — エントリーポイントはエンティティです | Transform.create(entity, {...}) — エントリーポイントはコンポーネント型です |
| コンポーネントの定義 | @Component('name') class GemData { val: number; constructor(...) {...} } | export const GemData = engine.defineComponent('gemData', { val: Schemas.Number }, { val: 2 }) — スキーマとデフォルト値を定義します。メソッドはありません |
コンポーネントのメソッド(reset() など) | 許可されています(クラスメソッド) | 禁止されています。コンポーネントはデータのみです。メソッドはフリー関数またはシステムに移してください |
| コンポーネントデータの読み取り | entity.getComponent(GemData).val(常に変更可能) | GemData.get(entity).val(不変で高速)または GemData.getMutable(entity).val = X(書き込み時のみ) |
| コンポーネントデータの変更 | 返された参照を変更します | 必ず getMutable() を使用してください。.get() は読み取り専用の参照を返します。.get() を使用して変更しても、何も起こりません |
| 親子関係 | child.setParent(parent) | 子の Transform の parent フィールドを設定します:Transform.create(child, { parent: parentEntity }) または Transform.getMutable(child).parent = parentEntity |
| システムの定義 | class MoveGems implements ISystem { update(dt) {...} } の後に engine.addSystem(new MoveGems()) | export function moveGemsSystem(dt: number) { ... } の後に engine.addSystem(moveGemsSystem) |
| エンティティのクエリ | const group = engine.getComponentGroup(Transform, GemData) の後に group.entities | for (const [entity, gemData, transform] of engine.getEntitiesWith(GemData, Transform)) { ... } |
| エンティティの削除 | engine.removeEntity(entity) | engine.removeEntity(entity)(同じ API) |
| コンポーネントのイベントコールバック | entity.addComponent(new OnPointerDown((e) => {...})) | pointerEventsSystem.onPointerDown({entity, opts}, () => {...}) — コンポーネントとは分離されています |
| 静的な「シーンレベル」のオブジェクト | Camera.instance、Input.instance(シングルトン) | engine.PlayerEntity、engine.CameraEntity、engine.RootEntity とコンポーネントの読み取り(Transform.get(engine.PlayerEntity)) |
可変性は単なるスタイルではなく、パフォーマンス上の契約です。 読み取りには常に .getMutable()(書き込み)ではなく .get()(読み取り)を使用してください。読み取り専用なのに .getMutable() を使用すると、フレームごとに CRDT の差分が送信されます。 |
API の対応関係 — クイックリファレンス
完全な表については、{baseDir}/references/api-mapping.md を参照してください。最も一般的な変換は次のとおりです。
| SDK6 | SDK7 |
|---|---|
import { ... } from 'decentraland-ecs' (多くの場合、暗黙的) | import { engine, Transform, GltfContainer, Vector3, Quaternion, Color4 } from '@dcl/sdk/ecs' + '@dcl/sdk/math' |
new Entity() + engine.addEntity(entity) | const entity = engine.addEntity() |
entity.addComponent(new Transform({position, rotation, scale})) | Transform.create(entity, { position, rotation, scale }) |
entity.getComponent(Transform)(読み取り/書き込み) | Transform.get(entity)(読み取り) / Transform.getMutable(entity)(書き込み) |
entity.getComponentOrCreate(Transform) | Transform.getOrCreateMutable(entity) |
entity.addComponentOrReplace(...) | Component.createOrReplace(entity, {...}) |
entity.hasComponent(Transform) | Transform.has(entity) |
entity.removeComponent(Transform) | Transform.deleteFrom(entity) |
child.setParent(parent) | Transform.getMutable(child).parent = parent(または create に parent を渡す) |
new GLTFShape('models/x.glb') + entity.addComponent(shape) | GltfContainer.create(entity, { src: 'models/x.glb' }) |
new BoxShape() / new PlaneShape() / new SphereShape() / new CylinderShape() | MeshRenderer.setBox(entity) / setPlane(entity) / setSphere(entity) / setCylinder(entity)(衝突判定には MeshCollider.setBox(entity) などを追加) |
new Material() + m.albedoColor = Color3.Blue() + entity.addComponent(m) | Material.setPbrMaterial(entity, { albedoColor: Color4.Blue() })(注:SDK7 では Color4) |
new BasicMaterial() + m.texture = new Texture(...) | Material.setBasicMaterial(entity, { texture: Material.Texture.Common({ src }) }) |
new TextShape(text) + t.fontSize = 1 | TextShape.create(entity, { text, fontSize: 1 }) |
new Animator() + new AnimationState('Clip') + animator.addClip(state) | Animator.create(entity, { states: [{ clip: 'Clip', playing: false, loop: false }] });Animator.playSingleAnimation(entity, 'Clip') で再生し、Animator.stopAllAnimations(entity) で停止します |
new OnPointerDown(handler, { button, hoverText }) をエンティティに追加 | pointerEventsSystem.onPointerDown({ entity, opts: { button: InputAction.IA_POINTER, hoverText } }, handler) |
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, false, handler) | システム内で inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_DOWN) を使うか、エンティティごとのイベントには pointerEventsSystem.onPointerDown を使います |
ActionButton.POINTER / .PRIMARY / .SECONDARY | InputAction.IA_POINTER / .IA_PRIMARY / .IA_SECONDARY |
Camera.instance.position / .rotation | Transform.get(engine.CameraEntity).position / .rotation |
new Vector3(x, y, z) | Vector3.create(x, y, z) |
Quaternion.Euler(x, y, z) | Quaternion.fromEulerDegrees(x, y, z) |
Color3.Blue() | Color4.Blue()(SDK7 のコンポーネントフィールドの多くは Color4 を使用) |
Vector3.Lerp(a, b, t) | Vector3.lerp(a, b, t)(小文字の l) |
Scalar.Lerp(a, b, t) | Scalar.lerp(a, b, t) |
vector.setAll(0.5) | Vector3.create(0.5, 0.5, 0.5)(変更用ヘルパーはありません — Vector3 はプレーンデータとして扱われます) |
class X implements ISystem { update(dt) {...} } + engine.addSystem(new X()) | function xSystem(dt: number) { ... } + engine.addSystem(xSystem) |
engine.getComponentGroup(A, B) が .entities を返す | engine.getEntitiesWith(A, B) が [entity, a, b] のタプルのイテレーターを返す |
log(...) | console.log(...) |
entity.addComponent(new utils.Delay(ms, cb))(decentraland-ecs-utils / @dcl-sdk/utils 由来) | timers.setTimeout(cb, ms)(import { timers } from '@dcl/sdk/ecs'、引数の順序は (callback, ms)) |
entity.addComponent(new utils.Interval(ms, cb)) | timers.setInterval(cb, ms)(import { timers } from '@dcl/sdk/ecs') |
フレームごとに dt を累積する独自の timerSystem | 削除してください — 代わりに timers.setTimeout / timers.setInterval を使います |
移行ワークフロー
この順序で作業してください。手順を飛ばすと、デバッグが難しいシーンの破損につながります。
1. SDK6 プロジェクトを監査する
すべてのソースファイルを読み、次の項目を一覧にします。
- すべてのカスタム
@Componentクラス — これらはフラットなスキーマを持つengine.defineComponent(...)に置き換えます。クラスメソッド(reset()など)は、独立した関数またはシステムに切り出す必要があります。 - すべての
class X implements ISystem— これらは独立した関数(dt: number) => voidに置き換えます。 - すべての
OnPointerDown/OnPointerUp/Input.instance.subscribe— これらはpointerEventsSystemの呼び出し、またはシステム内のinputSystem.isTriggered()に置き換えます。 - すべての
GLTFShape、BoxShapeなど — 対応表を参照してください。 - すべての
Camera.instance、Input.instance— これらは静的なエンティティとコンポーネントの読み取りに置き換えます。 - すべての
setParent呼び出し — これらはTransform.parentフィールドへの代入に置き換えます。
この一覧が手順 5〜10 の完了チェックリストになります。各手順は、該当カテゴリのすべての項目を移行し、SDK6 のシンボルがソースに残っていない状態になって初めて完了です。
2. package.json と tsconfig.json を置き換え、scene.json を更新する
decentraland-ecsを@dcl/sdk(最新版)に置き換えます。build-ecs/dcl startスクリプトをsdk-commands start/sdk-commands buildに置き換えます。- 古い
tsconfig.jsonを標準の SDK7 のもの(target: ES2020、module: ESNext、jsx: react-jsx、strict: true)に置き換えます。 scene.jsonに"runtimeVersion": "7"を追加します。ユーザーから依頼されない限り、mainは変更しないでください。
3. assets/ の下にアセットフォルダーを再編成する
SDK6 のシーンでは、プロジェクトのルートにあるトップレベルのフォルダー(models/、images/、audio/(または sounds/)、textures/、videos/)にアセットが置かれていることがよくあります。SDK7 と Creator Hub では、ビジュアルエディターがアセットをインデックスできるよう、トップレベルの assets/ フォルダーの下にアセットを置く必要があります。コードを移行する前に、この移動を独立した手順として行ってください。そうすれば、後の手順でパスを書き換える際に、最終的な配置先を直接指定できます。
- SDK6 プロジェクトのトップレベルにあるアセットフォルダーをすべて特定します。SDK6 でよく使われる名前:
models/、images/、audio/、sounds/、textures/、videos/。 - 他のスキルで使われているカテゴリ名を大文字で始める命名規則に従って、
assets/の下に新しい構成を作成します([[create-scene]]、[[add-3d-models]]、[[audio-video]] を参照)。models/→assets/Models/images/→assets/Images/audio/またはsounds/→assets/Audio/videos/→assets/Videos/textures/→assets/Images/(テクスチャは画像です。ユーザーが別のフォルダーを希望しない限り、まとめてください)
- ファイルを新しい構成に移動します。重複したコピーを残さないでください。移動が完了したら、古いトップレベルのフォルダーを削除します。コピーが二重に存在すると、Creator Hub が古いパスをインデックスし、デプロイ時のシーンサイズがほぼ倍になります(両方のコピーがバンドルされます)。
- 移動したパスへのコード参照をすべて書き換えます。ソースツリー全体で古いフォルダー名を grep し、該当箇所をすべて更新します。
GltfContainer.create(e, { src: 'models/chair.glb' })→... { src: 'assets/Models/chair.glb' }AudioSource.create(e, { audioClipUrl: 'sounds/click.mp3' })→... 'assets/Audio/click.mp3'Material.Texture.Common({ src: 'textures/logo.png' })→... 'assets/Images/logo.png'- React-ECS
<UiEntity uiBackground={{ texture: { src: 'images/icon.png' } }} />→... 'assets/Images/icon.png' - コード、JSON、または
.compositeファイル内で、古いフォルダー名を含む文字列リテラルすべて。
- 完了条件: プロジェクト内で古いフォルダー名を grep しても参照が一つも残っておらず、Creator Hub でシーンを開くと
assets/からアセットツリーが読み込まれていること。
例外 — 既存のレイアウトがあれば再利用する。 プロジェクトにすでに assets/scene/Models/(従来の Creator Hub レイアウト)、または assets/asset-packs//assets/custom/(Creator Hub がアセットパックのアイテムやカスタムアイテム用に追加するフォルダー)がある場合は、assets/Models/ を新たに作成する代わりに、それらのパスを再利用してください。ルールは、アセットの種類ごとに正規の配置先を一つにすることです。分散させないでください。
4. エクスポートされた main() を含む src/index.ts を作成します
SDK7 のシーンは、トップレベルでエクスポートされた main() 関数から始まります。コードの大部分は別のファイル(例:src/game.ts)に記述し、index.ts から呼び出すのが一般的です。
import { initializeGame } from "./game";
export function main() {
initializeGame();
}
5. まずコンポーネントを移植します
各 @Component クラスを engine.defineComponent(name, schema, defaults) に変換します。ネストした型はフラット化してください。Schemas は任意のクラスには対応していませんが、Schemas.Vector3、Schemas.Quaternion、プリミティブ型、配列、ネストされた Schemas.Map(...) には対応しています。SDK6 の Vector2 フィールドは通常、SDK7 では 2 つのスカラー型フィールド(posX、posY)になります。{baseDir}/references/migration-example.md の 2048 の例を参照してください。
完了条件: grep -rn "@Component(" src/ の検索結果が 0 件で、手順 1 の監査リストにあるすべてのコンポーネントに対応する engine.defineComponent 呼び出しがあること。
6. システムを移植します
各 class X implements ISystem { update(dt) {...} } を独立した関数に変換します。engine.getComponentGroup(A, B).entities による反復処理は、for (const [entity, a, b] of engine.getEntitiesWith(A, B)) に置き換えます。ループ内で書き込みを行う場合に限り、.getMutable(entity) を使用してください。
完了条件: grep -rnE "implements ISystem|getComponentGroup" src/ の検索結果が 0 件で、手順 1 の監査リストにあるすべてのシステムが engine.addSystem を通じて登録されていること。
7. エンティティ/コンポーネントのセットアップを移植します(game.ts の大部分)
各 new Entity() ブロックを engine.addEntity() と一連の Component.create(entity, ...) 呼び出しに置き換えます。setParent は Transform の parent フィールドに変換します。すべての src/audioClipUrl/texture.src には、手順 3 で設定した新しい assets/Models/...、assets/Audio/...、assets/Images/... のパスを使用します。
完了条件: grep -rnE "new Entity\(|\.addComponent\(|\.setParent\(" src/ の検索結果が 0 件で、手順 3 より前のフォルダーを参照している src/audioClipUrl/テクスチャのパスが残っていないこと。
8. 入力/ポインターハンドラーを移植します
- エンティティごとのクリックハンドラー(
new OnPointerDown(...))→pointerEventsSystem.onPointerDown({ entity, opts }, handler)。エンティティにコライダーがない場合は追加してください(MeshCollider.setBox(entity)またはGltfContainerのvisibleMeshesCollisionMask: 1)。[[add-interactivity]] を参照してください。 - グローバルキーのサブスクリプション(
Input.instance.subscribe)→ システム内でinputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)を使用します。[[advanced-input]] を参照してください。
完了条件: grep -rnE "OnPointerDown|OnPointerUp|Input\.instance|ActionButton\." src/ の検索結果が 0 件で、手順 1 の監査リストにあるすべてのハンドラーに pointerEventsSystem または inputSystem による対応処理があること。
9. アニメーションを移植します
new Animator() + new AnimationState('clip') + animator.addClip(...) は、単一の Animator.create(entity, { states: [{ clip, playing, loop }] }) に変換します。再生するには Animator.playSingleAnimation(entity, 'clip') を使用します。すべて停止するには Animator.stopAllAnimations(entity) を使用します。[[animations-tweens]] を参照してください。
完了条件: grep -rnE "new Animator|AnimationState|addClip|getClip" src/ の検索結果が 0 件で、SDK6 の playAnimation/getClip 呼び出しに登場するすべてのクリップ名が、いずれかの states[] 配列に含まれていること。states[] にないクリップはエラーなく失敗します(よくある落とし穴の playSingleAnimation に関する注意を参照してください)。
10. サウンドを移植します
new AudioClip(url) + entity.addComponent(new AudioSource(clip)) は AudioSource.create(entity, { audioClipUrl: url, playing: false, ... }) に変換します。再生するには、AudioSource.getMutable(entity).playing = true に切り替えます。[[audio-video]] を参照してください。
完了条件: grep -rnE "new AudioClip|new AudioSource\(" src/ の検索結果が 0 件であること。
11. 検証とテスト
npm install を実行して新しい SDK を取得し、次に sdk-commands start(または npm start)を実行します。以下のすべてを満たした場合にのみ、移行は完了です。
npm run buildが終了コード 0 で終了する。- 最終確認の
grep -rnE "decentraland-ecs|@Component\(|implements ISystem|Input\.instance|Camera\.instance" src/の結果が 0 件である — 作業が中断されて再開された場合、手順ごとの grep では見落としが生じるため、これがエンドツーエンドの確認になります。 - プレビューがコンソールにエラーを出さずに読み込まれる。
- 表示されるレイアウト(位置/回転)が SDK6 のシーンと一致する。
いずれかの確認に失敗した場合は、そのシンボルカテゴリを担当する手順に戻ってください。移行が完了したと報告してはなりません。
よくある落とし穴
.get()の結果を変更する場合:Transform.get(entity).position.x = 5は何も起こらず、変更も反映されません。Transform.getMutable(entity).position.x = 5を使用してください。Transform.createにrotation: undefined/scale: undefinedを明示的に渡すと、シリアライザーがクラッシュします:シリアライズ時に"Cannot read properties of undefined (reading 'x')"が発生します。原因(検証済み —@dcl/ecs/dist/components/manual/Transform.js):Transform.createは値をTransformSchema.extendに渡しますが、そこでデフォルト値の後に...valueを展開するため、明示的なundefinedフィールドがデフォルトの単位回転/単位スケールを上書きし、undefinedとして保存されます。その後、バイナリのserializeがvalue.rotation.xを読み取ろうとして例外を発生させます。ポート時に、undefinedフィールドを含む可能性のある部分オブジェクトから Transform を構築すると、この問題が発生します(例:Transform.create(e, { position, rotation: maybeRot, scale: maybeScale }))。修正 —undefinedを渡すのではなく、そのフィールド自体を省略してください。そうすればスキーマのデフォルト値が適用されます。const out: Partial<TransformType> = { position } if (rotation) out.rotation = rotation if (scale) out.scale = scale if (parent !== undefined) out.parent = parent Transform.create(entity, out)- スキーマを忘れる:
engine.defineComponentにはスキーマが必須です。SDK6 の型なしクラスフィールドは自動推論できません。 - コンポーネントのメソッド:SDK6 のコンポーネントには、
reset()やコンストラクターのロジックが含まれていることがよくありました。SDK7 では、コンポーネントは純粋なデータであるため、これらを必ずフリー関数に移してください。 - Vector2 フィールド:
SchemasにSchemas.Vector2はありません。[未検証 — Schemas オブジェクトのエクスポートを確認してください。]2 つのSchemas.Numberフィールドにフラット化するか、Schemas.Map({ x: Schemas.Number, y: Schemas.Number })を使用してください。 - エンティティオブジェクトに対する
setParent:SDK7 ではエンティティは整数であり、メソッドはありません。子のTransformのparentフィールドを設定してください。 - Color3 と Color4:SDK7 のコンポーネントフィールドの多くは、アルファ値を含む
Color4を受け取ります。フィールドの型が Color3 と明記されていない限り、Color3.X()の呼び出しをColor4.X()に置き換えてください。 - マテリアルはエンティティでもオブジェクトでもありません:参照を保持する
new Material()インスタンスはありません。設定にはMaterial.setPbrMaterial(entity, props)のみを使用します。マテリアルは 1 つのエンティティに付与され、共有オブジェクトとして使うものではありません。 - シェイプエンティティ(BoxShape、PlaneShape など)には MeshRenderer と MeshCollider が必要です:SDK6 では、単一の
BoxShapeがフラグを使って描画と衝突の両方を処理していました。SDK7 ではこれらは別々のコンポーネントです。プリミティブでクリックや物理演算を使う場合は、両方を追加してください。 withCollisions: trueの場合、GLTFShape の可視メッシュはデフォルトでクリック可能でした:SDK7 のGltfContainerでは、デフォルトで不可視の _collider メッシュにのみ衝突が設定されます。この動作に対応できないモデルもあります。コライダーがないためエンティティをクリックできなくなった場合は、クリック用にCL_POINTER(1)、プレイヤーとの衝突用にCL_PHYSICS(2)、または両方用に3を含む値をvisibleMeshesCollisionMaskに明示的に設定する必要があります。[[add-3d-models]] を参照してください。- コンポーネントグループに対する
forEach内ではOnPointerDownコールバックが発火しません:SDK6 ではイベント駆動でした。SDK7 ではpointerEventsSystem.onPointerDown(...)でハンドラーを一度だけ登録します。システムの更新ループ内にこの呼び出しを置かないでください。そうすると毎フレーム新しいハンドラーが登録されます。 Animator.playSingleAnimationはAnimator.statesにないクリップに対して何もせず終了します(コードで Animator を構築した場合):これは、TypeScript でAnimatorを自分で記述し、プログラムからAnimator.playSingleAnimation(entity, clipName)を呼び出す SDK6 からのポートに限った問題です。この場合、clipNameがエンティティのstates配列にすでに存在しなければ、playSingleAnimationはfalseを返して何も行いません(検証済み —@dcl/ecs/dist-cjs/components/extended/Animator.jsの 35~46 行目)。警告は表示されません。SDK6 ではAnimator.getClip(name)が初回呼び出し時にクリップを自動作成していたため、SDK6 のシーンでは'Appear'を事前に宣言せずplayAnimation('Appear')を呼び出すことがよくありました。単純にentity.getComponent(Animator).getClip(name).play()をAnimator.playSingleAnimation(entity, name)に置き換えると、ポート時にstates[]に追加していないクリップ名に対して呼び出すことになり、何も起きずに失敗します。修正方法:SDK6 のソースにあるすべてのplayAnimation/getClip呼び出しを調べ、エンティティごとにクリップ名をすべて集めて、Animator.create時にそのすべてをstates配列に指定してください。states.pushを遅延実行して SDK6 の「設定なしで任意のクリップを再生する」使い勝手を手早く再現する、ポート向けの簡易シムについては、[[animations-tweens]] の PITFALL セクションにあるラッパーを参照してください。このルールは、Creator Hub Inspector で作成され、すべての GLB クリップがあらかじめstates[]に追加されている Animator を使うシーン、statesが設定済みのアセットパック/スマートアイテム、または Animator のないモデルには適用されません。最後の場合は次の箇条書きにある自動再生の動作に依存します。- フレームごとに呼び出される自作 SDK7 ヘルパーへ
anim.stop(); anim.play()をポートすると、不要なシリアライズのオーバーヘッドが発生します:SDK6 のanim.stop(); anim.play()を、Animator.states(s.playing、s.shouldReset)を直接変更する自作 SDK7 ヘルパーに置き換え、そのヘルパーをシステムの更新処理や、入力を押している間毎ティック発火する入力/レイキャストのコールバックなど、フレームごとに呼び出す場合が該当します。アニメーションが停止することはありません(@dcl/ecsの CRDT 変更なし可変値抑制レイヤーが同一の書き込みを検出し、何もせず破棄します)が、毎ティックgetMutableOrNull()を呼び出すと、毎フレーム不要なシリアライズとバイト比較のオーバーヘッドが発生します。修正方法:エンティティごとに最後に適用したクリップを記録し、変更がなければヘルパーを早期終了させてください。同じクリップでnoLoop + revertToIdleを再度呼び出す場合は、状態を再変更せず、戻りタイマーだけを更新してください。戻りコールバックからアイドルクリップを再適用する前に、記録したクリップをクリアしてください。この問題は、状態遷移時にのみヘルパーを呼び出す場合(1 回だけのonPointerDown、エッジトリガー方式のステートマシン)、またはフレームごとの呼び出しではなく状態遷移からAnimator.playSingleAnimationを使用する場合には発生しません。ヘルパーの完全なパターンについては、[[animations-tweens]] のベストプラクティス「Short-circuit clip-switch helpers」を参照してください。 Animatorが付いていない場合、GltfContainerは複数のアニメーションを含む .glb から 1 つのクリップを自動再生します:SDK6 のモデルはデフォルトでバインドポーズのままでしたが、SDK7 では .glb から 1 つのクリップが選ばれて再生されます。これは、クリップを登録していない Inspector シーンが動作する仕組みと同じです。前の箇条書きと矛盾するのではなく、その補完関係にあります。エンティティにAnimatorがない場合はレンダラーが自動再生します。エンティティにAnimatorがあり、playSingleAnimationを呼び出す場合は、クリップがstates[]に含まれている必要があります。レンダラーが自動再生するクリップはモデルによって一定ではありません。観測例として、Animator のない小さなゴーストが GLB からdieを自動再生したハロウィーンのシーンがあります。異なるクリップをデフォルトにしたい場合の修正方法:Animator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] })を付けて制御してください。playSingleAnimationで実行時にクリップを切り替えたい場合の修正方法:切り替えるすべてのクリップをstates[]に指定してください(前の箇条書きを参照)。BUTTON_DOWN/BUTTON_UPを使うInput.instance.subscribe:これに直接対応するコールバックはありません。「このフレームで押された」状態を検出するには、システム内でinputSystem.isTriggered()を使用してください。@dcl/ecs-scene-utilsのトリガーエリア(「Utils ライブラリ」)と自作のフレームごとの AABB 判定:カスタムのフレームごとの位置チェックシステムをポートしたり、SDK7 で一から作り直したりしないでください。SDK6 にはネイティブのトリガーエリアがなかったため、コミュニティの Utils ライブラリや多くのシーンの独自コードは、毎フレームCamera.instance.positionとボックス/球体領域を照合し、onCameraEnter/onCameraExitを発火させていました。SDK7 には@dcl/sdk/ecsのネイティブなTriggerAreaコンポーネントがあります。パターン全体をTriggerArea.setBox(entity)(または.setSphere)とtriggerAreaEventsSystem.onTriggerEnter(entity, cb)/.onTriggerExit/.onTriggerStayに置き換えてください。サイズはTransform.scale、位置はTransform.positionから取得され、親子関係も考慮されます。そのため、回転やオフセットのあるシーンルートを親に持つトリガーも、座標変換を手動で行わずに正しく動作します。動作の互換性:Utils ライブラリはローカルプレイヤーに対してのみ発火していましたが、ネイティブのTriggerAreaはデフォルトでColliderLayer.CL_PLAYERを使い、このレイヤー上のすべてのプレイヤーに対して発火します。ローカルプレイヤーのみを対象にする正しいガードはif (result.trigger?.entity !== engine.PlayerEntity) returnです(result.triggeredEntityではありません。紛らわしい名前ですが、triggeredEntityはトリガーエリア自体のエンティティなので、プレイヤーと比較すると常に一致し、ガードは発火しません)。コンポーネントの詳細については、{baseDir}/references/api-mapping.mdの「Trigger areas」セクションと [[add-interactivity]] を参照してください。decentraland-ecs-utils(後の@dcl-sdk/utils)のutils.Delay/utils.ExpireInと、自作のフレームごとのタイマーシステム:コミュニティのDelayコンポーネントをポートしたり、SDK7 で作り直したり、dtを蓄積するtimerSystemを使ったカスタムsetSceneTimeoutヘルパーを書いたりしないでください。@dcl/sdk/ecsの名前付きエクスポートtimers(import { timers } from '@dcl/sdk/ecs')を使用してください。シーンエンジンに紐付いたsetTimeout/clearTimeout/setInterval/clearIntervalが提供されます。entity.addComponent(new utils.Delay(2000, cb))はtimers.setTimeout(cb, 2000)に置き換え、SDK6 のシーンで Delay コンポーネントを置くためだけに作成していた「タイマーエンティティ」を削除してください。引数の順序は(callback, ms)です。(ms, callback)ではありません。SDK6 のDelay(ms, cb)は JavaScript 標準とは逆の順序であり、SDK6 の順序で書いたカスタムヘルパーは、そもそもヘルパーを作成するという元の誤りに拍車をかける既知の落とし穴です。ネイティブの JavaScriptsetTimeout/setIntervalグローバルは使用しないでください。QuickJS では公開されていますが、シーンエンジンに紐付いておらず、微妙な問題を引き起こす可能性があるため、必ずtimersを使用してください。詳細は [[scene-runtime]] の「Timers」セクションを参照してください。- SDK6 の「インターセプトツール」型スマートアイテム(キー、磁石、インベントリアイテムなど、
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)とエンティティ階層の走査を使って、クリックされた対象に作用するもの)には、SDK7 に直接対応する仕組みがありません。pointerEventsSystem.onPointerDownはエンティティごとのみで、ヒット結果をグローバルに受け取るサブスクライバーはありません。移行パターンは、対象エンティティ名をキーとする小さなレジストリを用意する方法です。対象自身のpointerEventsSystem.onPointerDownでレジストリを確認し、その後、デフォルトの動作に移ります。詳細は{baseDir}/references/api-mapping.md#click-interception-across-entities-keytool-acting-on-a-targetを参照してください。 Vector3.GetAngleBetweenVectors、Vector3.Up()など:SDK6 の Vector3 には、静的メソッドとして多くのヘルパーがありました。SDK7 でも大半は@dcl/sdk/mathにありますが、大文字・小文字の使い方が変わっています(例:Vector3.Lerp→Vector3.lerp)。移行後にVector3.を検索し、各呼び出しを math モジュールで確認してください。- SDK7 では
log()はエクスポートされていません:console.log()に置き換えてください。 - 保持アイテム(銃、照準レティクル、懐中電灯、固定位置のツール)での
Attachable.FIRST_PERSON_CAMERA/Attachable.AVATAR:SDK6 には、大まかな追従モードが 2 つしかありませんでした。Attachable.FIRST_PERSON_CAMERA(カメラに追従)とAttachable.AVATAR(アバターのルートに追従)です。SDK7 では、以前はなかった新しい選択肢として、anchorPointId(例:AAPT_RIGHT_HAND)を使うボーン単位のAvatarAttachがあります。ボーンアンカーは「アバターの手に持たせる」SDK7 での自然な置き換えに見えますが、ボーンアンカーはアバターのアニメーション(アイドル時の揺れ、歩行サイクル、ジェスチャー)を引き継ぎます。AAPT_RIGHT_HANDに付けた銃は毎フレーム揺れ、照準を定められません。保持するゲームプレイアイテムを SDK7 で扱う正しい方法は、AvatarAttachではなく、Transform.parentを使ってエンティティを親にすることです。照準が重要なアイテム(銃、レティクル、懐中電灯など、プレイヤーが周囲を見回して狙いを定めるもの)には、SDK7 でTransform.parent = engine.CameraEntityをデフォルトとして推奨します。これは SDK6 のFIRST_PERSON_CAMERAに相当し、ヨーとピッチの両方に追従するため、銃口がカメラの向いている方向を向きます。Transform.parent = engine.PlayerEntityは別の選択肢であり、よくある微妙な失敗の原因です。プレイヤーのルート(足の位置とヨーのみ。ピッチやアニメーションは含まれません)に追従するため、プレイヤーが照準を上に向けても、PlayerEntityを親にした銃は水平のままです。つまり、アイテムは視線の方向を無視します。PlayerEntityは、照準に追従させない身体に固定するアイテム(保持する盾、固定されたたいまつ、腰の高さに携帯する非照準型のインベントリアイテム)にのみ使用してください。AvatarAttach(boneId)は、アニメーションに追従させたい装飾用アイテム(帽子、光輪、バックパック、ネームプレート)に限定してください。目的に応じて選択してください。engine.CameraEntity(照準が重要なアイテム。保持する武器で推奨)、engine.PlayerEntity(ヨーのみ/ピッチなし。身体に固定するアイテム用)、AvatarAttach(boneId)(装飾用のみ。ボーンのアニメーションに追従)。比較表と銃の実例については、[[player-avatar]] の「Held items vs cosmetic items」セクションを参照してください。 - ビルボード化したモデルがポート後にカメラと反対を向くことがあります:一部の SDK6 → SDK7 ポートで確認されています。SDK6 の
new Billboard(false, true, false)(Y 軸ビルボード)では正面を表示していたモデルが、同じ親子構成の SDK7 のBillboard.create(e, { billboardMode: BillboardMode.BM_Y })では背面をカメラに向けます。SDK6 と SDK7 では、どちらのモデル面(+Z か -Z)をカメラに向けるかが異なるようです。SDK7 のPBBillboardインターフェースにはoppositeDirectionフラグがありません。フィールドはbillboardModeのみです(@dcl/ecsのbillboard.gen.d.tsで確認済み)。そのため、修正するのは Billboard コンポーネントではなくモデルの Transform です。表示するモデルを Y 軸で 180° 回転させてください(Quaternion.fromEulerDegrees(0, 180, 0))。エンティティが(ビルボード化された)親と子モデルの構成の場合は、180° 回転を子のTransform.rotationに適用してください。親エンティティの回転は Billboard コンポーネントが制御しているため、ビルボード化されたエンティティの回転を上書きしても効果はありません。[未検証 — これは明示的な SDK の動作変更で変更履歴に記載されたものなのか、モデルインポーターの別の規約変更によるものなのかは未確認です。ユーザー向けの説明では、特定のバージョンが原因と断定せず、「2 つの SDK ではビルボードの向きが異なります」と表現してください。]SDK7 のコンポーネントの詳細は [[advanced-rendering]](Billboard)を参照してください。 - UI のピクセルサイズは 1 対 1 では引き継がれません:SDK6 の UI(
UICanvas/UIImage/UIText)は固定画面サイズのピクセル単位で作成されていたため、width = 200やpositionX = -350のような位置やサイズは絶対値でした。SDK7 React-ECS はピクセル値を仮想キャンバスに対する相対値として拡大縮小します。また、SDK 7.26.0 以降では、サイズを指定しない場合にプラットフォームごとのデフォルト値(1920x1080、モバイルでは1600x720)が適用されますが、これは SDK6 の UI 作成時に使われたグリッドとは異なる可能性が非常に高い値です。7.26.0 より前にはデフォルトがなく、サイズを指定しないと拡大縮小も行われないため、表示ごとにレイアウトがずれていきます。ReactEcsRenderer.setUiRendererで仮想キャンバスを明示的に設定し、エンジンがピクセル値を UI 作成時の座標として扱うようにしてください。UI をポートする前に SDK6 のソース(従来の ECS は https://github.com/decentraland/ecs にあります)を確認し、元の UI の設計に使われたピクセルグリッドを調べ、その値を仮想サイズとして指定してください。ReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })1920x1080は表示上の妥当なデフォルトですが、正しい virtualWidth/virtualHeight は SDK6 の UI が作成された解像度によって異なります。SDK6 のシーンが別の基準解像度(例:1280x720)を想定していた場合は、その値を使用し、既存のピクセル座標が同じ位置に配置されるようにしてください。7.26.0 以降では、グリッドを意図せず変える可能性がある動作が 2 つあります。シーンがモバイルで実行されると、16:9 のサイズは1600x720に上書きされます。また、<= 0の値を指定すると拡大縮小が完全に無効になります。React-ECS への全面的な書き換えと SDK バージョンの適用条件については、[[build-ui]] を参照してください。
移行が本当に難しいもの
次の項目にはすっきりとした 1 対 1 の対応関係がなく、再設計が必要です。
- 処理を実行するメソッドやコンストラクターを持つコンポーネント(例:
GemData.reset(val, x, y)):データを新しいコンポーネントに分離し、ロジックを(entity: Entity, val: number, ...)を受け取り、Component.getMutable(entity)を呼び出すフリー関数に移します。 - ユーザーが参照を保持し、何度も反復処理するコンポーネントグループ:SDK7 の
engine.getEntitiesWith(...)は呼び出すたびに再計算されます。同じ集合を対象にするホットループでは、システム内にキャッシュするか、そのコストを許容してください。 entity.aliveに依存するオブジェクトプール:SDK7 のエンティティにはaliveフラグがありません。プールでは、カスタムコンポーネントを使うかComponent.has(entity)を確認して、生存状態を追跡する必要があります。Migrated-2048 の例では「常に新しいエンティティを追加する」方式に簡略化しています。スループットが低いケースでは問題ありませんが、実際のプールには明示的な状態管理が必要です。- SDK6 の
UICanvas/UIImage/UIText/UIClickableで構築された UI:SDK7 とは API に互換性がありません。SDK7 の UI は React-ECS(JSX)です。[[build-ui]] を参照してください。多くの場合、最初から書き直す必要があります。移行時には、画面サイズに応じて SDK6 のピクセル座標が予測可能な形で拡大縮小されるよう、ReactEcsRenderer.setUiRendererに仮想キャンバスサイズも設定してください。上記の よくある落とし穴 にある UI のピクセルサイズに関する注意事項を参照してください。 @Component('name')デコレーターのメタデータ:SDK7 のコンポーネント ID は、engine.defineComponent('name', ...)に渡す文字列です。他のツール(コンポジット、マルチプレイヤー同期など)が名前で参照している場合は、SDK6 のデコレーターと同じ名前を使用してください。そうでない場合は、一貫した命名規則を選んでください。
相互参照
- [[create-scene]] — scene.json のスキーマ、コンポジット優先のルール、新しい SDK7 プロジェクトのひな形作成
- [[composites]] — 静的エンティティを TypeScript から移動する任意の第 2 段階で使用する
.compositeファイル形式 - [[add-3d-models]] —
GltfContainer、クリック可能にするためのコライダーマスク - [[add-interactivity]] —
pointerEventsSystem(SDK6 のOnPointerDownを置き換えます) - [[advanced-input]] —
inputSystemによるポーリング(SDK6 のInput.instance.subscribeを置き換えます) - [[scene-runtime]] —
@dcl/sdk/ecsのtimers.setTimeout/timers.setInterval(SDK6 のコミュニティ製decentraland-ecs-utilsのDelay/Intervalを置き換えます)、executeTask、fetch、レルム/シーン情報 - [[animations-tweens]] —
AnimatorとTween(SDK6 のAnimator/AnimationStateを置き換えます) - [[audio-video]] —
AudioSourceとVideoPlayer - [[build-ui]] — React-ECS UI(SDK6 の
UICanvasUI システムを置き換えます) - [[advanced-rendering]] — PBR マテリアル、TextShape、Billboard
- [[multiplayer-sync]] — マルチプレイヤー用の
syncEntity(SDK6 のMessageBus/同期ライブラリを置き換えます)
参考資料
{baseDir}/references/api-mapping.md— SDK6 から SDK7 へのシンボル変換をまとめた一覧{baseDir}/references/migration-example.md— 2048 ゲームシーンを使った移行前後の注釈付き例(エンティティのセットアップ、コンポーネント、システム、ポインターイベント、アニメーション)