关于 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。 在调用 CameraModeArea.create() 后缩放区域实体或其任一父级,都不会调整区域大小:区域仍严格为 area 指定的尺寸(米)。某个轴缩放至 0 时仍会将其压缩。因此,不能通过补间缩放或将区域置于会缩放的控制架下方来扩大摄像机区域——应改为写入 CameraModeArea.getMutable(entity).area(并将相同数值同步到 scale,确保 Creator Hub 操控器显示正确)。将可见的调试框设为单独的实体;只要任一数值发生变化,区域实体自身的 MeshRenderer 就会错误报告体积。完整规则,以及 TriggerArea 的相反行为(没有尺寸字段——Transform 本身就是体积,之后的缩放变化也会影响体积),请参见 player-avatar → AvatarModifierArea。回归测试见 9,99-modifier-areas → src/areaScaleDrift.ts。
整段体验都需要限制时,覆盖范围也必须涵盖整段体验。 仅覆盖出生路线的区域,在场景传送、重生、将玩家跳转到不同高度,或电影式演出结束后将摄像机控制权交还时,都无法保证限制仍然有效。将区域设置为覆盖整个水平和垂直活动空间(包括调试跳跃和死亡/重启位置),然后在游戏内测试每种状态切换:出生、传送、死亡/重启、不同高度、解锁/重新锁定,以及电影式演出后交还控制权。交还控制权时,应恢复场景预期的模式(第一人称场景应恢复第一人称),而非统一切换为第三人称。
Creator Hub/Inspector 支持: Creator Hub 现已为 CameraModeArea 提供专用 Inspector 面板,其中 mode 下拉菜单可选择 First Person/Third Person(有意省略 Cinematic——该模式请使用 VirtualCamera)。资源目录的 utils 分类中提供了名为“Camera Modifier Area”的智能项(半透明占位立方体)。编辑器会将 area 字段与实体的 Transform.scale 隐式保持同步(运行时根据非零的 area 设置区域大小,并抵消 scale;若 area 为 0,则回退到 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实体(及其 rig 父级)存活约 1 秒,让渲染器能够平滑切回玩家摄像机。- 然后对 rig 调用
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)的姿态,而不是在镜头开始时读取:如果镜头期间头像移动了,提前读取的姿态就会过时。- 渐变驱动摄像机世界姿态所在的实体。对于观战模式使用的父子 rig,先取消摄像机的父级绑定。否则,应渐变 rig 根节点,让摄像机而非根节点最终到达该姿态。对子实体执行的渐变会在其父级的局部空间中运行,因此传入世界姿态会使它落在其他位置。 **在摄像机之间切换/停用:**如果
MainCamera已存在,通过MainCamera.getMutableOrNull(engine.CameraEntity)修改它;将virtualCameraEntity设为另一个 VirtualCamera 实体即可切换或过渡到该摄像机,设为undefined则返回玩家的普通摄像机(已验证:2,22-virtual-cameras)。同一时间只能有一个 VirtualCamera 处于活动状态(MainCamera.virtualCameraEntity只保存一个实体)。
切勿将 0 或任何不带 VirtualCamera 的实体写入 virtualCameraEntity。 只有当该字段不存在时,引擎才会释放玩家摄像机:undefined 会从消息中省略,随后 MainCameraSystem 会清除绑定。如果该字段存在,但其实体不带 VirtualCamera(0 是 engine.RootEntity),引擎就会在每一帧重试并保留当前活动的虚拟摄像机:如果某个 rig 处于活动状态,即使恢复输入,视角也会继续卡在该 rig 上(如果没有 rig 处于活动状态,则不会有变化)。这会影响保存/恢复代码:该字段是 optional,因此新的 MainCamera 读取到的是 undefined——只有当场景代码写入的保存值中包含 0 时,你才会得到 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)可禁用 avatar 移动,让 WASD 改为控制摄像机。
跟踪摄像机位置
在系统中每帧读取 Transform.get(engine.CameraEntity).position,以轮询摄像机位置。完整的区域跟踪系统示例(cameraZoneSystem)请参阅 {baseDir}/references/camera-patterns.md →“跟踪摄像机位置(摄像机区域系统)”。
摄像机与碰撞体
玩家的摄像机在第三人称模式下移动时,是否会被碰撞体挡住取决于分配给实体的碰撞层。为避免摄像机穿墙,必须将 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 和 InputModifierdisableAll使用。screenDelta的原点在左上角,y轴向下递增,因此 pitch 应加上delta.y(pitch + delta.y * SENS)——减去它会反转垂直轴。仅限桌面端(移动设备上的 screenDelta 始终为 0)。请参阅{baseDir}/references/camera-patterns.md→“鼠标视角摄像机”。 - 观察模式(观察者/导演/回放摄像机) — 将玩家从头像移动模式切换为自由漫游或跟随玩家的摄像机:双实体 yaw/pitch 装置、WASD/E/F/1/2 控制、
onEnterScene/onLeaveScene玩家列表、InputModifier 冻结,以及地块边界限制(引擎会在 VirtualCameras 超出地块边界时将其禁用)。请参阅{baseDir}/references/camera-patterns.md→“观察模式”。
过场动画期间冻结玩家? 将 VirtualCamera 与 advanced-input 技能中的
InputModifier结合使用,防止玩家在电影化序列期间移动。
示例场景
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,22-virtual-cameras — 多个 VirtualCameras:静态摄像机、
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 — 通过按键输入启用/停用 VirtualCameras,并切换
MainCamera,同时结合 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)中实现自由漫游/跟随玩家摄像机,包含双实体 yaw/pitch 装置、玩家列表、WASD/E/F/1/2 控制、地块边界限制、上述延迟拆除序列,以及完整的TouchScreenControls移动端对等支持(其 README 仅记录桌面端按键绑定——移动端处理逻辑见源码)。观察模式的参考实现。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/9,99-modifier-areas —
CameraModeArea在旋转的 4x4x4 体积内强制使用CameraType.CT_FIRST_PERSON,并以半透明方框渲染,使触发体积清晰可见。点击处理程序会移动该区域的Transform,以展示该区域会跟随实体;一段注释掉的分支展示了通过CameraModeArea.getMutable()原地修改mode。同一场景中还包含一个AvatarModifierArea。