camera-control について
camera-control は、Decentraland SDK7 開発者向けのカメラ操作ガイドです。カメラ状態の読み取り、視点モードへの対応、領域や仮想カメラによる制御を扱い、シネマティック撮影、ターゲット追跡、切り替え時の注意点も説明します。
主な機能
- 01カメラ位置と回転の読み取り
- 02一人称・三人称モードの検出と対応
- 03領域によるカメラモードの制限
- 04遷移付き仮想カメラの作成
- 05仮想カメラによるターゲット追跡
活用シーン
- 01屋内エリアで一人称視点を強制
- 02カットシーンで視点を切り替え
- 03プレイヤーを追うカメラの作成
- 04カメラ位置に応じたシーン処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill camera-controlインストール後、Claude Code で/camera-controlを入力して読み込まれたか確認します。
同梱ファイルreferences/camera-patterns.md
詳細
Decentraland のカメラ制御
カメラ状態の読み取り
予約済みの engine.CameraEntity を使って、カメラの現在位置と回転にアクセスします。
import { engine, Transform } from '@dcl/sdk/ecs'
function trackCamera() {
if (!Transform.has(engine.CameraEntity)) return
const cameraTransform = Transform.get(engine.CameraEntity)
console.log('Camera position:', cameraTransform.position)
console.log('Camera rotation:', cameraTransform.rotation)
}
engine.addSystem(trackCamera)
engine.CameraEntity は読み取り専用です。Transform に書き込まないでください。シーン側でカメラを制御して移動するには、代わりに VirtualCamera を操作します(以下の VirtualCamera を参照)。
カメラモードの検出
プレイヤーが一人称視点か三人称視点かを確認します。
import { engine, CameraMode, CameraType } from '@dcl/sdk/ecs'
function checkCameraMode() {
if (!CameraMode.has(engine.CameraEntity)) return
const cameraMode = CameraMode.get(engine.CameraEntity)
if (cameraMode.mode === CameraType.CT_FIRST_PERSON) {
console.log('First person camera')
} else if (cameraMode.mode === CameraType.CT_THIRD_PERSON) {
console.log('Third person camera')
}
}
engine.addSystem(checkCameraMode)
カメラモードの値
CameraType.CT_FIRST_PERSON // First-person view
CameraType.CT_THIRD_PERSON // Third-person view (default)
カメラモードの変更への対応
CameraMode.onChange を使うと、プレイヤーが一人称視点と三人称視点を切り替えたときだけ通知を受け取れます。毎フレームポーリングするより効率的です。
import { CameraMode, engine } from '@dcl/sdk/ecs'
CameraMode.onChange(engine.CameraEntity, (camera) => {
if (!camera) return
// camera.mode is 0 for first person, 1 for third person
console.log('Camera mode changed:', camera.mode)
})
CameraModeArea(領域内でカメラを固定)
プレイヤーが領域に入ったときに特定のカメラモードを強制します(例:狭い屋内では一人称視点を強制します)。
import { engine, Transform, CameraModeArea, CameraType } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const fpArea = engine.addEntity()
Transform.create(fpArea, { position: Vector3.create(8, 1.5, 8) })
CameraModeArea.create(fpArea, {
area: Vector3.create(6, 4, 6), // 6x4x6 meter box
mode: CameraType.CT_FIRST_PERSON, // Force first-person inside
})
プレイヤーが領域から出ると、カメラはプレイヤーが設定したモードに戻ります。
注意点 — area がゼロ以外の場合、コンポーネントが存在する間は Transform.scale より area が優先されます。 CameraModeArea.create() の後に領域エンティティ、またはその親のスケールを変更しても、領域のサイズは変わりません。常に area の値と同じメートル単位のサイズになります。ただし、いずれかの軸を 0 にすると、その軸は潰れます。そのため、スケールをトゥイーンしたり、スケールが変化するリグの子にしたりしてもカメラ領域を拡大することはできません。代わりに CameraModeArea.getMutable(entity).area を書き換えます(Creator Hub のギズモ表示と一致させるため、scale にも同じ値を設定してください)。表示用のデバッグボックスは別のエンティティとして作成します。領域エンティティ自体に MeshRenderer を付けると、いずれかの値を変更した時点でボリュームの表示が不正確になります。完全なルールと、反対の動作をする TriggerArea(サイズフィールドがなく、Transform がボリュームになり、後からのスケール変更も反映されます)については、player-avatar → AvatarModifierArea を参照してください。回帰テストは 9,99-modifier-areas → src/areaScaleDrift.ts にあります。
体験全体に対する制限には、体験全体をカバーする必要があります。 スポーン経路だけをカバーする領域では、シーンがテレポートしたとき、プレイヤーがリスポーンしたとき、別の高さに移動したとき、またはシネマティックの後にカメラ制御を戻したときのことは何も確認できません。領域のサイズを、水平方向と垂直方向のプレイ空間全体(デバッグ用のジャンプや死亡・再開位置も含む)に合わせ、スポーン、テレポート、死亡・再開、別の高さへの移動、ロック解除・再ロック、シネマティック後の制御返却といった各遷移をゲーム内でテストしてください。制御を戻すときは、汎用的な三人称視点ではなく、シーンで意図するモード(一人称視点のシーンなら一人称視点)に戻します。
Creator Hub / Inspector のサポート: Creator Hub には CameraModeArea 専用のインスペクターパネルが追加されており、mode を選択するドロップダウン(First Person / Third Person。Cinematic は意図的に省かれています。その場合は VirtualCamera を使用してください)があります。アセットカタログの utils カテゴリには、「Camera Modifier Area」スマートアイテム(半透明のプレースホルダーキューブ)があります。エディターは、エンティティの Transform.scale と area フィールドの同期を画面に表示せずに維持します(ランタイムでは、ゼロ以外の area の値を使って領域のサイズを決定し、scale の影響を補正します。area がゼロの場合は scale が使われます)。そのため、ギズモでエンティティのサイズを変更すると、カメラ領域も自動的に更新されます。この同期はエディターでの書き込みであり、ランタイムのバインディングではありません。ランタイムでは area の値だけが使われます(上記の注意点を参照)。デフォルトの初期値:{ area: {1,1,1}, mode: CameraType.CT_FIRST_PERSON }。Creator Hub のコミット a843390a で確認済みです。player-avatar の AvatarModifierArea と同じサイズ規則が適用されます。明示的な area で領域のサイズを決め、エディター用に Transform.scale も同じ値に保ちます。place_smart_item の後に entity_detail で、エンティティに core::CameraModeArea が含まれていることを確認してください。このパックの composite.json では定義されています(asset-packs::Placeholder + core::CameraModeArea、creator-hub origin/main 2026-10-19)が、配置時に含まれなかった例がありました(2026-10-08、Bevy エディター、再現せず)。その場合は、部屋のサイズに合わせて set_component してください。
VirtualCamera(シネマティックカメラ)
カットシーンや特別な視点用に、スクリプトでカメラ位置を作成します:
import { engine, Transform, VirtualCamera, MainCamera } from '@dcl/sdk/ecs'
import { Vector3, Quaternion } from '@dcl/sdk/math'
const cinematicCam = engine.addEntity()
Transform.create(cinematicCam, {
position: Vector3.create(8, 5, 2),
rotation: Quaternion.fromEulerDegrees(-20, 0, 0),
})
VirtualCamera.create(cinematicCam, {
defaultTransition: {
transitionMode: VirtualCamera.Transition.Speed(1.0),
},
// fov: 45, // optional: field of view in degrees (overrides the Explorer default)
})
// Activate the virtual camera (createOrReplace on first activation:
// getMutable throws if MainCamera doesn't exist yet)
MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: cinematicCam })
// Return to normal camera (component now exists, so getMutable is safe)
MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
トランジションモード
VirtualCamera.Transition.Speed(1.0) // Speed-based smooth transition
VirtualCamera.Transition.Time(2) // Time-based transition (2 seconds)
カメラを快適に動かすには、トランジション速度を0.5から3.0の間に設定してください。
注視ターゲット
仮想カメラでエンティティを追跡します:
const target = engine.addEntity()
Transform.create(target, { position: Vector3.create(8, 1, 8) })
VirtualCamera.create(cinematicCam, {
lookAtEntity: target,
defaultTransition: {
transitionMode: VirtualCamera.Transition.Speed(2.0),
},
})
// Activate
MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: cinematicCam })
lookAtEntity に engine.PlayerEntity を指定すると、移動するプレイヤーにカメラを向け続けることができます(確認済み:2,22-virtual-cameras)。固定の注視方向をカメラ自体の回転に焼き込むには、カメラの Transform で Quaternion.fromLookAt(cameraPosition, targetPosition) を使用します。
設定なしの VirtualCamera.create(entity) も有効です(デフォルト値が適用されます)。Transform を手動で動かすカメラに便利です(以下の操作可能なカメラのパターンを参照)。
VirtualCamera は省略可能な fov?: number フィールドを受け取ります(視野角は度単位)。省略した場合のデフォルト値は 60 です。設定すると、仮想カメラがアクティブな MainCamera.virtualCameraEntity である間、デフォルトのFOVが上書きされます。js-sdk-toolchainの PBVirtualCamera コミット 26cb9251 およびドキュメントのコミット 717227a で確認済みです。
値の選び方:
- 広角(高め、例:90) — 一度に見えるシーンの範囲が広がり、スピード感が生まれます。レースや乗り物のカメラに適しています。
- 望遠(低め、例:35~45) — ズームインし、遠近感が平坦になります。照準を合わせる場面やシネマティックショットに適しています。
- 非常に広角な値ではフレームの端が歪みます。歪みを狙う場合を除き、固定アングルのカメラはデフォルト付近に保ってください。
破棄の順序が重要です。バインドを解除してから、後でエンティティを削除してください。 使用をやめた同じフレーム内に
VirtualCameraエンティティを削除すると、ビューが跳ねます。また、MainCamera.virtualCameraEntityを解除する前に削除すると、エンジンが削除済みのエンティティにバインドされたままになり、ビューがプレイヤーの足元に移ります。安全な手順(
33,20-spectate-modeで確認済み):
MainCamera.virtualCameraEntity = undefinedを設定します。- レンダラーがプレイヤーカメラにブレンドして戻れるよう、
VirtualCameraエンティティ(およびそのリグ親エンティティ)を約1秒間存続させます。- その後、
engine.removeEntity(...)でリグを削除します。let rigDestroyTimer = 0 function rigDestroySystem(dt: number) { rigDestroyTimer += dt if (rigDestroyTimer < 1) return engine.removeSystem(rigDestroySystem) if (rigCamera) engine.removeEntity(rigCamera) if (rigRoot) engine.removeEntity(rigRoot) } // in disableSpectate(): clear MainCamera, restore input, then rigDestroyTimer = 0 engine.addSystem(rigDestroySystem)使い捨てではなくカメラを再利用する場合は、エンティティをそのまま保持し、削除を省略してください。
戻る際には
defaultTransitionは適用されません。 一人称、三人称、またはドローンカメラに戻るときは、常に固定の0.5秒のイーズイン・イーズアウトブレンドが使われます(Explorerのカメラブレンド設定、unity-explorer main 2026-10)。プレイヤーから離れた位置のショットから戻ると、跳ねたように見えます。滑らかに戻すには、仮想カメラをプレイヤーカメラのポーズまでトゥイーンし、到着したらvirtualCameraEntityを解除します(Locksmithシーンでは0.9秒のトゥイーンが機能しました。2026-10):
- 戻すためのトゥイーンを始める直前に
Transform.get(engine.CameraEntity)からポーズを読み取ってください。ショット中にアバターが移動していた場合、開始時に読み取ったポーズは古くなっています。- カメラのワールドポーズを保持するエンティティをトゥイーンしてください。スペクテイト用のリグのように親子付けされたリグでは、まずカメラの親子付けを解除します。そうでない場合は、カメラではなくルートがそのポーズに到達するよう、リグのルートをトゥイーンしてください。子エンティティのトゥイーンは親のローカル空間で実行されるため、ワールドポーズを渡すと別の場所に配置されます。 カメラの切り替え/無効化:
MainCameraがすでに存在する場合は、MainCamera.getMutableOrNull(engine.CameraEntity)を使って変更します。virtualCameraEntityに別の VirtualCamera エンティティを設定すると、そこへカット/遷移します。undefinedを設定すると、プレイヤーの通常のカメラに戻ります(確認済み:2,22-virtual-cameras)。一度にアクティブにできる VirtualCamera は1つだけです(MainCamera.virtualCameraEntityが保持するエンティティは1つです)。
virtualCameraEntity に 0 や、VirtualCamera を持たないエンティティを書き込まないでください。 エンジンがプレイヤーカメラを解放するのは、このフィールドが存在しない場合のみです。undefined はメッセージから省略され、その後 MainCameraSystem がバインドを解除します。VirtualCamera を持たないエンティティ(0 は engine.RootEntity です)を値として設定すると、エンジンは毎フレーム再試行し、現在アクティブな VirtualCamera を使い続けます。リグがアクティブだった場合、入力を復元した後も視点がそのまま固定されます(アクティブなリグがなかった場合は、何も変わりません)。これは保存/復元コードで問題になります。フィールドは optional なので、新しい MainCamera を読み取ると undefined になります。保存値から 0 が得られるのは、通常は ?? 0 や || 0 のフォールバックなど、シーンコードが書き込んだ場合だけです。その 0 を後で書き戻すと、カメラが固定されます(2026-10 にキャプチャシーケンスで発生。この仕組みは unity-explorer main の MainCameraSystem.HandleVirtualCameraChange から読み取ったもので、単独では再現していません)。undefined と 0 の両方を「以前のカメラなし」として扱います。
function restoreCamera(previous: Entity | undefined) {
const cam = MainCamera.getMutableOrNull(engine.CameraEntity)
if (!cam) return
if (previous && VirtualCamera.has(previous)) cam.virtualCameraEntity = previous
else cam.virtualCameraEntity = undefined // or MainCamera.deleteFrom(engine.CameraEntity) if the scene never bound one
}
その後、ゲーム内でカメラが元に戻ることを確認します(get_player_state → camera.mode が通常のモードに戻っていること)。中断時とリスポーン時の経路も確認してください。ヘッドレステストでは、視点が固定されたままかどうかを確認できません。
プレイヤーの実際のカメラを直接動かすことはできません。シーン制御下でカメラを動かすには、アクティブな VirtualCamera エンティティの Transform を毎フレーム操作します。そのエンティティが MainCamera.virtualCameraEntity である間、プレイヤーはそのカメラ越しに見ます(確認済み:制御可能なカメラ 2,22-virtual-cameras)。InputModifier(advanced-input)と組み合わせてアバターの移動を無効にすると、WASD でカメラを操作できます。
カメラ位置の追跡
システム内で Transform.get(engine.CameraEntity).position を読み取り、毎フレームのカメラ位置をポーリングします。ゾーン追跡システムの実装例(cameraZoneSystem)については、{baseDir}/references/camera-patterns.md → 「カメラ位置の追跡(カメラゾーンシステム)」を参照してください。
## カメラとコライダー
プレイヤーのカメラが3人称モードで移動するとき、エンティティに割り当てられたコリジョンレイヤーによって、コライダーに遮られる場合と遮られない場合があります。カメラが壁を通り抜けないようにするには、カメラを遮りたいエンティティに ColliderLayer.CL_PHYSICS と ColliderLayer.CL_POINTER の両方のレイヤーを割り当てる必要があります。
// NO CAMERA GOING THROUGH THE WALL
// default (both pointer and physics use the invisible geometry)
GltfContainer.create(myEntity, {
src: '/models/myModel.gltf',
})
// NO CAMERA GOING THROUGH THE WALL
// Both use the same invisible geometry
GltfContainer.create(myEntity2, {
src: '/models/myModel.gltf',
invisibleMeshesCollisionMask:
ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER,
})
// NO CAMERA GOING THROUGH THE WALL
// Both use the same visible geometry
GltfContainer.create(myEntity2, {
src: '/models/myModel.gltf',
visibleMeshesCollisionMask:
ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER,
})
// YES CAMERA GOES THROUGH THE WALL
// physics and pointer are on different layers
GltfContainer.create(myEntity2, {
src: '/models/myModel.gltf',
invisibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS,
visibleMeshesCollisionMask: ColliderLayer.CL_POINTER,
})
// YES CAMERA GOES THROUGH THE WALL
// physics and pointer are on different layers
GltfContainer.create(myEntity2, {
src: '/models/myModel.gltf',
invisibleMeshesCollisionMask: ColliderLayer.CL_POINTER,
visibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS,
})
よく使われるパターン
実装例の全容については、{baseDir}/references/camera-patterns.md を参照してください。
- カメラ起動イベント — カメラの位置や近接を利用して、プレイヤーがエリアを見るとアクションを起動します。
- NPC の追跡(camera-follows-NPC) — 毎フレーム VirtualCamera の Transform を操作して NPC を追跡します(これが機能する理由についてのガードレールは、上記の VirtualCamera セクションにあります)。
- マウスルックカメラ(FPS スタイル) —
PrimaryPointerInfo.screenDelta(毎フレームのピクセル差分。カーソルがロックされている間も動作します)で VirtualCamera を操作します。yaw/pitch に値を累積し、pitch を [-85,+85] にクランプして、PointerLock とdisableAllを指定した InputModifier を組み合わせます。screenDeltaの原点は 左上で、yは下方向に増加するため、pitch にはdelta.yを加算します(pitch + delta.y * SENS)。減算すると垂直軸が反転します。デスクトップのみ対応(モバイルでは screenDelta は常に 0 です)。{baseDir}/references/camera-patterns.md→「Mouselook Camera」を参照してください。 - 観戦モード(観察者/演出者/リプレイカメラ) — プレイヤーの操作をアバターの移動から、自由移動カメラまたはプレイヤー追従カメラに切り替えます。2 エンティティの yaw/pitch リグ、WASD/E/F/1/2 コントロール、
onEnterScene/onLeaveSceneによるプレイヤー一覧、InputModifier による移動の固定、パーセル境界でのクランプを使用します(パーセル境界の外ではエンジンが VirtualCamera を無効にします)。{baseDir}/references/camera-patterns.md→「Spectate Mode」を参照してください。
カットシーン中にプレイヤーの動きを止めるには? VirtualCamera と、advanced-input スキルの
InputModifierを組み合わせると、シネマティックシーケンス中のプレイヤーの移動を防止できます。
シーンの例
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,22-virtual-cameras — 複数の VirtualCamera:固定カメラ、
Speed/Timeのトランジション、lookAtEntity: engine.PlayerEntity、Tween で動かすカメラ、WASD で操作するカメラ(毎フレーム VirtualCamera の Transform を操作)、さらにCameraModeAreaとAvatarModifierArea。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,5-primary-cursor-info — キー入力で
MainCameraを切り替えて VirtualCamera を有効化/無効化し、InputModifier と組み合わせます。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/32,20-virtual-camera-mouse-look — マウスルックカメラ:ポインターがロックされている間、
PrimaryPointerInfo.screenDeltaで VirtualCamera の yaw/pitch を操作し、InputModifierの disableAll と PointerLock コントロールを使用します。マウスルックパターンのリファレンス実装です。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/33,20-spectate-mode — 観戦モード:自由移動/プレイヤー追従カメラを、自己完結した単一モジュール(
src/spectate.ts)に実装しています。2 エンティティの yaw/pitch リグ、プレイヤー一覧、WASD/E/F/1/2 コントロール、パーセル境界でのクランプ、上記の遅延ティアダウンシーケンス、そしてTouchScreenControlsによるモバイルの完全な同等対応が含まれます(README に記載されているのはデスクトップの操作方法のみで、モバイル対応はソースに記載されています)。観戦パターンのリファレンス実装です。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/9,99-modifier-areas — 回転した 4x4x4 のボリューム内で
CameraMode.CT_FIRST_PERSONを強制するCameraModeAreaを、トリガーボリュームが見えるよう半透明のボックスとして描画します。クリックハンドラーで領域のTransformを移動させ、領域がエンティティに追従することを示します。コメントアウトされた分岐では、CameraModeArea.getMutable()を使ってmodeをその場で変更する方法を示しています。同じシーン内のAvatarModifierAreaと並んで配置されています。