Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

camera-module

A shared virtual-camera system for Decentraland SDK7 scenes: a multi-camera manager, cinematic mode (with optional UI hooks), dolly/crane-style camera animations, and player lock/unlock while a virtual camera is active.

This module owns camera and input state only. It has no opinion on how a scene renders its own UI — see Cinematic-mode UI hooks for how a host plugs its own UI in.

Capabilities

A tour of what this module can actually do, before the API-level reference below.

Multiple cameras, one active at a time

addCamera(cameraData) creates as many virtual camera entities as a scene needs — one per interaction point, chess seat, theatre screen, dispenser, event board, whatever. Each is tracked in the shared MultiCameraManager component. Only one is ever "live" (assigned to MainCamera) at a time; switching which one is live is what enterCinematicMode() / useVirtualCamera() do. There's no built-in cycling/carousel between cameras — a host decides when to switch and to which one.

Cinematic mode: the main way scenes use this

enterCinematicMode(camera) / exitCinematicMode() is the one call most features need: it assigns the given camera to MainCamera, and exitCinematicMode() also unlocks player movement automatically. Optional UI (letterbox bars, prompts, whatever a scene wants) hooks in on top — see Cinematic-mode UI hooks for the full default / per-call-override / opt-out story.

Look-at targeting and lifelike idle motion

Set lookAtTarget: true on a CameraData and the camera continuously aims at a shared look-at point (cameraTarget) instead of holding a fixed rotation. While a camera is active and not marked avatarAttached, a background system (CameraFollowSystem) gives that look-at point a subtle, randomized "wobble" — picking a small offset every ~0.2–1.2s and smooth-interpolating toward it — so a static shot doesn't feel perfectly locked-off. Cameras with avatarAttached: true skip the wobble, since they're expected to already be tracking a moving target.

Re-targeting who/what the camera follows

setCameraFollowTarget(entity) redirects the follow/wobble system to track any entity — not just the player. guideNPC.ts uses this to have the tutorial guide camera follow the NPC; nekkoBarBartender.ts uses it to follow the bartender. centerCameraTarget() snaps the follow target back to the scene's own default center point (sceneCenterTarget, set once via setSceneParent/initCamera).

Player input locking, independent of cinematic mode

lockPlayer() / unlockPlayer() disable/enable player movement (jog, jump, run, walk) directly via InputModifier, without needing to go through enterCinematicMode/exitCinematicMode at all — useful for cases that want to freeze the player without switching the active camera.

Scripted dolly/crane camera moves

startCameraAnimation(animationType) plays a pre-configured Tween on whichever camera is currently active, computed from wherever the followed entity currently is. The available moves (each with its own duration and easing curve, defined in cameraConfig.ts's cameraAnimations table): DOLLY_IN, DOLLY_OUT, DOLLY_IN_FAST, DOLLY_OUT_FAST, CRANE_DOWN, MOVE_ALONG_RUNWAY_OPPOSITE, MOVE_WITH_AVATAR, and NONE (a very short settle-in move). These are hand-tuned for the scene this module was originally built for — treat the specific positions/durations in cameraConfig.ts as a starting point to retune, not universal constants.

Scene-relative positioning out of the box

Every CameraData.position passed to addCamera() is automatically offset relative to whatever entity was passed to setSceneParent(). That means camera coordinates can be authored as if the scene sat at the world origin, regardless of where the scene is actually parented — important for a multi-parcel layout like Genesis Plaza, where the same relative camera placements need to work no matter which parcel offset the scene ends up at.

Imported-model rotation correction

CameraData.importedRotation: true applies a corrective rotation (a -90°/180° axis fix-up) before placing the camera — for cameras whose transform data originates from an imported GLTF/3D tool with different axis conventions than the SDK expects, rather than needing every caller to hand-correct their rotations.

Tagging cameras by purpose

Every camera carries a types: CAMERA_TYPE[] array in its CamData component. Only CAMERA_TYPE.CENTER_TARGET exists today, but the enum is meant to be extended by consuming scenes for their own querying/filtering needs (e.g. "give me all cameras tagged for this minigame").

What's tracked but not currently acted on

disableCameraSwitch() / enableCameraSwitch() toggle a cameraSwitchEnabled flag on MultiCameraManager — but nothing inside this module currently reads that flag to gate behavior. It's exposed for a host to set and query its own logic against, not a built-in switch-blocking mechanism (an earlier "cycle to next camera of type X" feature that would have read it was removed as unused during extraction). Similarly, TutorialCameraData is a convenience type for hosts authoring their own scripted waypoint sequences — nothing in cameras.ts consumes it directly.

Installing into a scene

This module is distributed as a git subrepo (not an npm package) — it's meant to be vendored as real files directly inside a consuming scene's own source tree, sharing that scene's own @dcl/sdk install rather than bringing its own.

From the root of the scene's repo:

git subrepo clone <this-repo-url> src/modules/camera

This copies the module's files into src/modules/camera/ and adds a .gitrepo file there tracking where it came from. Import it the same way as any other local module, e.g. with this project's @modules/* path alias:

import { initCamera, addCamera } from "@modules/camera/cameras"
import { CAMERA_TYPE } from "@modules/camera/cameraConfig"

To pull in updates later, from the scene repo's root (with a clean working tree — git subrepo requires no unstaged changes anywhere in the repo):

git subrepo pull src/modules/camera

To contribute changes back upstream:

git subrepo push src/modules/camera

A note on this repo's package.json

This repo has its own package.json declaring @dcl/sdk as a dependency — but that version has no effect once vendored into a scene via subrepo. The vendored files have no node_modules of their own (it's gitignored, so subrepo never copies it), so import { engine } from "@dcl/sdk/ecs" in cameras.ts resolves via Node's normal upward directory search straight to the consuming scene's own node_modules/@dcl/sdk — whatever version that happens to be. Bumping the version in this repo's package.json doesn't change what any scene actually runs.

That package.json exists purely so this repo can be opened and developed standalone — running npm install here gives you @dcl/sdk types for IntelliSense and lets you run tsc --noEmit in isolation, without needing a host scene present at all.

One caveat: don't run npm install inside a scene's vendored copy (e.g. central-plaza/src/modules/camera/). That would create a node_modules/@dcl/sdk right there, which Node's resolver would find before walking up to the scene's own copy — silently reintroducing two separate engine instances in the same bundle (the exact bug this module's design otherwise avoids).

Setup

Call these once, during scene startup, before anything calls initCamera() or addCamera():

import { initCamera } from "@modules/camera/cameras"
import { setSceneParent } from "@modules/camera/sceneParent"

setSceneParent(mySceneRootEntity)
initCamera()
  • setSceneParent(entity) — the one piece of scene-specific data this module can't know on its own: which entity camera positions declared in world-space should be positioned relative to (mirrors however the rest of the scene parents its own content). If never called, cameras are positioned relative to world origin instead.
  • initCamera() — creates the camera manager and the default camera rig. Safe to call more than once; only does anything the first time. addCamera() also calls this internally if it hasn't run yet, so calling it explicitly is optional but recommended for predictable ordering.

Cinematic-mode UI hooks

enterCinematicMode() / exitCinematicMode() switch the active camera and optionally trigger UI (e.g. letterbox bars) — but this module doesn't implement any UI itself. Wire up a scene-wide default once:

import { setCinematicUIHooks } from "@modules/camera/cameras"

setCinematicUIHooks({
  onEnterCinematic: () => showMyLetterboxBars(),
  onExitCinematic: () => hideMyLetterboxBars(),
})

Every enterCinematicMode(camera) call with no second argument uses this default. Individual call sites can override it:

// Skip UI entirely for this one:
enterCinematicMode(chessCam, {})

// Use different UI for this one:
enterCinematicMode(theatreCam, {
  onEnterCinematic: () => showTheatreOverlay(),
  onExitCinematic: () => hideTheatreOverlay(),
})

Whichever hook set was passed to enterCinematicMode() (the override, or the default if none given) is what the next exitCinematicMode() call fires. Only one cinematic session is assumed active at a time — this isn't per-camera state, so don't rely on overlapping concurrent sessions.

API reference

cameras.ts

Export Signature Notes
initCamera () => void Creates the camera manager + default rig. Idempotent.
addCamera (cameraData: CameraData, transitionTime?: number) => Entity Creates a new virtual camera entity from a CameraData config.
enterCinematicMode (camera: Entity, uiHooks?: CinematicUIHooks) => void Activates camera as the main camera; fires enter UI hook.
exitCinematicMode () => void Returns to the player's own camera; fires exit UI hook; unlocks player input.
setCinematicUIHooks (hooks: CinematicUIHooks) => void Sets the scene-wide default UI hooks (see above).
useVirtualCamera (camera: Entity) => void Lower-level: just assigns MainCamera, no UI hook, no player lock.
freeCamera () => void Lower-level counterpart to useVirtualCamera.
lockPlayer / unlockPlayer () => void Disables/enables player movement input directly.
setCameraFollowTarget (entity: Entity) => void Re-targets the camera-follow system (wobble/tracking) to a different entity.
centerCameraTarget () => void Resets the follow target back to the scene's default center.
getDefaultCamera () => Entity Returns the currently active camera entity.
disableCameraSwitch / enableCameraSwitch () => void Toggles the cameraSwitchEnabled flag on the manager component.
startCameraAnimation (animationType: ANIMATION_TYPE) => void Plays a dolly/crane-style tween on the current camera (see cameraConfig.ts for the available types and their durations/easings).
MultiCameraManager, CamData, CameraTarget, ModelTracker components Exposed for direct reads (e.g. CamData.getMutable(camera)) where a host needs lower-level access.
sceneCenterTarget, modelTracker, cameraTarget, cameraWobbleTarget, cameraManager Entity Internal entities, exposed for advanced use.

cameraConfig.ts

Export Notes
CAMERA_TYPE Enum tagging what a camera is used for (currently just CENTER_TARGET).
ANIMATION_TYPE Enum of available startCameraAnimation() animations (DOLLY_IN, DOLLY_OUT, CRANE_DOWN, etc).
CameraData Config shape passed to addCamera() — position, fov, rotation, whether it should look at the camera target, etc.
TutorialCameraData Simpler camera-waypoint shape used for scripted tutorial/guide sequences.
cameras Default array of CameraData presets used by initCamera().
cameraAnimations Duration/easing table for each ANIMATION_TYPE.

sceneParent.ts

Export Signature Notes
setSceneParent (entity: Entity) => void Call once at scene startup (see Setup).
sceneParentEntity () => Entity | undefined Read the currently-set scene parent.

Example: a simple interactive camera

import { addCamera, enterCinematicMode, exitCinematicMode } from "@modules/camera/cameras"
import { CAMERA_TYPE } from "@modules/camera/cameraConfig"
import { Quaternion, Vector3 } from "@dcl/sdk/math"

const myCamera = addCamera({
  types: [CAMERA_TYPE.CENTER_TARGET],
  position: Vector3.create(4, 3, 4),
  fov: 50,
  targetVerticalOffset: 1.0,
  avatarAttached: false,
  rotation: Quaternion.fromEulerDegrees(0, 0, 0),
  importedRotation: false,
  lookAtTarget: true,
})

// somewhere in a pointer-event handler:
enterCinematicMode(myCamera)
// ...and to leave:
exitCinematicMode()

What's deliberately not in this module

Presentation/UI (letterbox bars, on-screen hints, buttons) lives in each consuming scene, not here — see Cinematic-mode UI hooks. Keeping this module to camera/input state only is intentional: it's the one thing every scene needs identically, while UI treatment is something different scenes reasonably want to differ on.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages