Camera Control in Decentraland
Authoring split
CameraModeArea and VirtualCamera are supported in main-entities.ts — both static-by-nature components belong there. MainCamera is NOT supported because it lives on the reserved engine.CameraEntity; activate a virtual camera at runtime in src/index.ts.
VirtualCamera.lookAtEntity accepts an entity name in main-entities.ts (resolved to an Entity ID at build time, same as Transform.parent).
// main-entities.ts
cinematic_cam: {
components: {
Transform: {
position: { x: 12, y: 4, z: 8 },
rotation: { x: 0, y: 0.7071, z: 0, w: 0.7071 }
},
VirtualCamera: {
defaultTransition: { transitionMode: { $case: 'time', time: 2 } },
lookAtEntity: 'shopkeeper' // name of another entity in this file
}
}
},
first_person_zone: {
components: {
Transform: { position: { x: 8, y: 1, z: 8 } },
CameraModeArea: {
area: { x: 16, y: 2, z: 16 },
mode: 0 // CameraType.CT_FIRST_PERSON
}
}
}
Activate the cinematic camera at runtime:
// src/index.ts
import { engine, MainCamera } from '@dcl/sdk/ecs'
export function main() {
const cam = engine.getEntityOrNullByName('cinematic_cam')
if (cam) MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: cam })
}
The reserved engine.CameraEntity and engine.PlayerEntity are engine-managed and have no representation in main-entities.ts.
CameraType values for CameraModeArea.mode
| value | enum | meaning | |---|---|---| | 0 | CT_FIRST_PERSON | Force first-person inside the zone | | 1 | CT_THIRD_PERSON | Force third-person inside the zone | | 2 | CT_CINEMATIC | Force cinematic camera inside the zone |
Reading Camera State
Access the camera's current position and rotation via the reserved 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)
Camera Mode Detection
Check whether the player is in first-person or third-person:
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)
Camera Mode Values
CameraType.CT_FIRST_PERSON // First-person view
CameraType.CT_THIRD_PERSON // Third-person view (default)
CameraModeArea (Force Camera in a Region)
Force a specific camera mode when the player enters an area:
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
})
When the player leaves the area, the camera reverts to their preferred mode.
VirtualCamera (Cinematic Cameras)
Create scripted camera positions for cutscenes or special views:
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)
}
})
// Activate the virtual camera
MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = cinematicCam
// Return to normal camera
MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
Transition Modes
VirtualCamera.Transition.Speed(1.0) // Speed-based smooth transition
VirtualCamera.Transition.Time(2) // Time-based transition (2 seconds)
Look-At Target
Make the virtual camera track an entity:
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.getMutable(engine.CameraEntity).virtualCameraEntity = cinematicCam
Tracking Camera Position
Poll camera position each frame for camera-triggered events:
import { engine, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
let lastNotifiedZone = ''
function cameraZoneSystem() {
if (!Transform.has(engine.CameraEntity)) return
const camPos = Transform.get(engine.CameraEntity).position
let currentZone = ''
if (camPos.y > 10) {
currentZone = 'sky'
} else if (camPos.x < 4) {
currentZone = 'west'
} else {
currentZone = 'center'
}
if (currentZone !== lastNotifiedZone) {
lastNotifiedZone = currentZone
console.log('Camera entered zone:', currentZone)
}
}
engine.addSystem(cameraZoneSystem)
Common Patterns
Camera-Triggered Events
Use the camera position to trigger actions when the player looks at a specific area:
function cameraLookTrigger() {
const camTransform = Transform.get(engine.CameraEntity)
const targetPos = Vector3.create(8, 2, 8)
const distance = Vector3.distance(camTransform.position, targetPos)
if (distance < 5) {
// Player is close — check if camera is pointing at target
// Use raycasting for precise look detection (see add-interactivity skill)
}
}
engine.addSystem(cameraLookTrigger)
Following an NPC
Move camera to track an NPC by updating a VirtualCamera's Transform:
function followNpcCamera(dt: number) {
const npcPos = Transform.get(npcEntity).position
const camTransform = Transform.getMutable(cinematicCam)
// Position camera behind and above the NPC
camTransform.position = Vector3.create(
npcPos.x - 2,
npcPos.y + 3,
npcPos.z - 2
)
}
engine.addSystem(followNpcCamera)
Freezing player during cutscenes? Combine VirtualCamera with
InputModifierfrom the advanced-input skill to prevent player movement during cinematic sequences.
Camera vs Colliders (preventing camera-through-wall)
In third-person mode the player's camera can slide through walls if the wall's collider mask doesn't include CL_POINTER. The camera uses the pointer collider mask for occlusion checks — not CL_PHYSICS. Set both masks on walls and architecture that the camera should bounce off:
// main-entities.ts
wall: {
components: {
Transform: { position: { x: 8, y: 1.5, z: 16 } },
GltfContainer: {
src: 'models/wall.glb',
visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
}
}
}
For GLBs that ship with invisible collider meshes (Creator Hub asset packs), set invisibleMeshesCollisionMask: 3 instead. Default of CL_PHYSICS only would let the camera pass through.
Best Practices
- Only one VirtualCamera should be active at a time.
- Use
CameraModeAreato force first-person in tight indoor spaces. - Keep transition speeds between 0.5 and 3.0 for comfortable camera movement.
- Read camera state via
engine.CameraEntity— never try to write to it directly. - For look-at detection, combine camera position with raycasting (see
add-interactivityskill). - Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera.
微信扫一扫