mcpField
C
camera-control

camera-control

github.com/dcl-regenesislabsDevelopment1737 installsUpdated 21w ago
Repository

About camera-control

A camera-control skill for Decentraland scene developers. It explains camera mode areas, runtime activation of virtual cameras, camera-state reading, and collider settings that prevent cameras from passing through walls.

Key features

  1. 01Set camera modes by area
  2. 02Create and activate virtual cameras
  3. 03Read camera position and mode
  4. 04Configure camera occlusion colliders

Use cases

  1. 01Force first-person view indoors
  2. 02Set up cutscene cameras
  3. 03Follow an NPC or trigger camera events

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/dcl-regenesislabs/opendcl --skill camera-control

After installing, type/camera-controlin Claude Code to confirm it loaded.

Details

From the project README

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
valueenummeaning
0CT_FIRST_PERSONForce first-person inside the zone
1CT_THIRD_PERSONForce third-person inside the zone
2CT_CINEMATICForce 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 InputModifier from 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 CameraModeArea to 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-interactivity skill).
  • Camera control is read-only outside of VirtualCamera and CameraModeArea — you cannot directly move the player's camera.

Comments

Markdown supported