mcpField
M
migrate-sdk6-to-sdk7

migrate-sdk6-to-sdk7

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

migrate-sdk6-to-sdk7 について

既存の Decentraland SDK6 シーンを SDK7 に移行するコーディングエージェント向けスキルです。SDK バージョンの確認や API の変換を案内し、既存のシーンのエントリーパスを維持します。

主な機能

  1. 01SDK6 プロジェクトの確認
  2. 02SDK6 から SDK7 への API 対応表
  3. 03順序立てた移行手順
  4. 04エンティティとシステムの変換案内
  5. 05静的シーンの任意移行

活用シーン

  1. 01既存の SDK6 シーンの移行
  2. 02エンティティとコンポーネント変換の確認
  3. 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 startsdk-commands start、sdk-commands build
scene.jsonruntimeVersion がない、または 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.instanceengine.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 に残します。

作業の順序:

  1. まず、SDK7 の TypeScript への移植を実行できる状態にします(エンティティは引き続きコードで作成します)。
  2. その後、必要に応じて静的エンティティを assets/scene/main.composite に移します。コンポジット優先のルールについては [[create-scene]] を、フォーマットについては [[composites]] を参照してください。

概念の転換 — コードを書く前にお読みください

SDK6 では、エンティティはコンポーネントがアタッチされたオブジェクトとしてモデル化されていました。SDK7 は真の ECS であり、エンティティは不透明な ID にすぎず、コンポーネントは純粋なデータです。

概念SDK6SDK7
エンティティの作成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.entitiesfor (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 を参照してください。最も一般的な変換は次のとおりです。

SDK6SDK7
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 = 1TextShape.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 / .SECONDARYInputAction.IA_POINTER / .IA_PRIMARY / .IA_SECONDARY
Camera.instance.position / .rotationTransform.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/ フォルダーの下にアセットを置く必要があります。コードを移行する前に、この移動を独立した手順として行ってください。そうすれば、後の手順でパスを書き換える際に、最終的な配置先を直接指定できます。

  1. SDK6 プロジェクトのトップレベルにあるアセットフォルダーをすべて特定します。SDK6 でよく使われる名前:models/、images/、audio/、sounds/、textures/、videos/。
  2. 他のスキルで使われているカテゴリ名を大文字で始める命名規則に従って、assets/ の下に新しい構成を作成します([[create-scene]]、[[add-3d-models]]、[[audio-video]] を参照)。
    • models/ → assets/Models/
    • images/ → assets/Images/
    • audio/ または sounds/ → assets/Audio/
    • videos/ → assets/Videos/
    • textures/ → assets/Images/(テクスチャは画像です。ユーザーが別のフォルダーを希望しない限り、まとめてください)
  3. ファイルを新しい構成に移動します。重複したコピーを残さないでください。移動が完了したら、古いトップレベルのフォルダーを削除します。コピーが二重に存在すると、Creator Hub が古いパスをインデックスし、デプロイ時のシーンサイズがほぼ倍になります(両方のコピーがバンドルされます)。
  4. 移動したパスへのコード参照をすべて書き換えます。ソースツリー全体で古いフォルダー名を 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 ファイル内で、古いフォルダー名を含む文字列リテラルすべて。
  5. 完了条件: プロジェクト内で古いフォルダー名を 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)を実行します。以下のすべてを満たした場合にのみ、移行は完了です。

  1. npm run build が終了コード 0 で終了する。
  2. 最終確認の grep -rnE "decentraland-ecs|@Component\(|implements ISystem|Input\.instance|Camera\.instance" src/ の結果が 0 件である — 作業が中断されて再開された場合、手順ごとの grep では見落としが生じるため、これがエンドツーエンドの確認になります。
  3. プレビューがコンソールにエラーを出さずに読み込まれる。
  4. 表示されるレイアウト(位置/回転)が 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 の順序で書いたカスタムヘルパーは、そもそもヘルパーを作成するという元の誤りに拍車をかける既知の落とし穴です。ネイティブの JavaScript setTimeout/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 の UICanvas UI システムを置き換えます)
  • [[advanced-rendering]] — PBR マテリアル、TextShape、Billboard
  • [[multiplayer-sync]] — マルチプレイヤー用の syncEntity(SDK6 の MessageBus/同期ライブラリを置き換えます)

参考資料

  • {baseDir}/references/api-mapping.md — SDK6 から SDK7 へのシンボル変換をまとめた一覧
  • {baseDir}/references/migration-example.md — 2048 ゲームシーンを使った移行前後の注釈付き例(エンティティのセットアップ、コンポーネント、システム、ポインターイベント、アニメーション)

コメント