Cinematic
Description
The Cinematic API is a high-level timeline system built on top of Camera.
It lets you define shot sequences, interpolation behavior, look targets, and camera effects,
then control playback through a runtime handle.
When to Use
- Use
Cinematicfor intros, mission reveals, scripted moments, and camera tours. - Use
Cameradirectly when you only need one-shot manual camera control.
Main API
scene()
Creates a fluent SceneBuilder bound to this cinematic instance.
scene(id?: string): SceneBuilder
start()
Starts a cinematic immediately and returns a mutable CinematicHandle.
start(scene: SceneBuildable, options?: CinematicStartOptions): CinematicHandle
play()
Starts and awaits completion in one call.
play(scene: SceneBuildable, options?: CinematicStartOptions): Promise<CinematicResult>
isRunning() / cancel()
Checks if there is an active timeline and cancels it.
isRunning(): boolean
cancel(): void
registerEffect()
Registers a custom reusable effect in the effect registry.
registerEffect<TParams = Record<string, unknown>>(
effect: CameraEffectDefinition<TParams>,
): void
Runtime Handle (CinematicHandle)
start() returns a CinematicHandle with mutable runtime controls and events.
Playback control
pause(): void
resume(): void
cancel(): void
skip(): void
Live editing
edit(mutator: (definition: CinematicDefinition) => void): void
insertShot(index: number, shot: CinematicShot): void
replaceShot(shotId: string, shot: CinematicShot): void
setEffects(effects: CameraEffectReference[]): void
addEffect(effect: CameraEffectReference): void
removeEffect(effectId: string): void
Result and events
result: Promise<CinematicResult>
on<TEventName extends CinematicEventName>(
eventName: TEventName,
handler: (payload: CinematicEventPayloadMap[TEventName]) => void,
): () => void
Available events:
shotStartshotEndeffectAppliedpausedresumedcompletedcancelledinterrupted
Scene Builder Helpers
Pos
Position/node builders used in shots:
Pos.coords(x, y, z)Pos.entity(entity, offset?)Pos.entityBone(entity, bone, offset?)Pos.anchor(name, offset?)Pos.resolver(() => Vector3 | Promise<Vector3>)
Each returns a NodeBuilder with helpers like .offset(...), .rot(...), and .fov(...).
Fx
Effect builder helpers:
Fx.effect(id, params?)Fx.fadeIn(ms?)Fx.fadeOut(ms?)Fx.camShake(type?, amplitude?)Fx.timecycle(name?, strength?)Fx.letterbox(top?, bottom?, alpha?)Fx.subtitle(text, durationMs?)
Effects can be windowed using .from(ms), .to(ms), or .window(fromMs, toMs).
Core Types
type CinematicEase =
| 'linear'
| 'inSine'
| 'outSine'
| 'inOutSine'
| 'inCubic'
| 'outCubic'
| 'inOutCubic'
interface CinematicShot {
id?: string
durationMs?: number
waitMs?: number
from?: CameraNodeInput
to?: CameraNodeInput
path?: CameraNodeInput[]
lookAt?: LookAtInput | LookAtInput[]
ease?: CinematicEase
effects?: CameraEffectReference[]
}
interface CinematicDefinition {
id?: string
skippable?: boolean
freezePlayer?: boolean
invinciblePlayer?: boolean
hideHud?: boolean
hideRadar?: boolean
effects?: CameraEffectReference[]
effectPresets?: string[]
anchors?: Record<string, Vector3>
shots: CinematicShot[]
}
interface CinematicStartOptions {
skipControlId?: number
cameraName?: string
}
type CinematicResultStatus = 'completed' | 'cancelled' | 'interrupted'
interface CinematicResult {
status: CinematicResultStatus
definitionId?: string
}
Example
@Controller()
export class IntroCinematicController {
constructor(private readonly cinematic: Cinematic) {}
async playSpawnIntro() {
const playerPed = PlayerPedId()
const scene = this.cinematic
.scene('spawn_intro')
.flags({
skippable: true,
freezePlayer: true,
invinciblePlayer: true,
hideHud: true,
hideRadar: true,
})
.anchor('player_origin', { x: 0, y: 0, z: 0 })
.effects(Fx.fadeIn(700), Fx.letterbox(0.1, 0.1, 230))
.shot('opening', (shot) =>
shot
.duration(3200)
.ease('inOutSine')
.from(Pos.entity(playerPed).offset(-6, -2, 2.4).fov(55))
.to(Pos.entity(playerPed).offset(-2, -1, 1.4).fov(42))
.lookAt(Pos.entity(playerPed).offset(0, 0, 0.9))
.effect(Fx.subtitle('Welcome to OpenCore', 2000).window(800, 2800)),
)
.wait('hold', 600)
const handle = this.cinematic.start(scene, { skipControlId: 200 })
handle.on('shotStart', (payload) => {
console.log('Shot started:', payload.shotId ?? payload.shotIndex)
})
const result = await handle.result
console.log('Cinematic finished with status:', result.status)
}
}
Best Practices
- Use stable shot ids (
id) if you need runtime patching withreplaceShot. - Prefer
scene().play(...)for simple linear flows andstart(...)for advanced controls. - Keep shots focused; split long movements into multiple shots for better tuning.
- Use presets/effects sparingly to avoid visual overload and maintain readability.
- Validate anchors and effect ids early to avoid runtime validation errors.
Notes
- A new cinematic interrupts any currently running one.
- Invalid scene definitions throw
CinematicValidationErrorwith detailed issues. - Built-in effect presets include
dramatic_intro,action_chase, anddialog_scene. - On resource stop, active cinematics are cancelled and camera state is reset.
Visual Samples (Placeholder)
Add screenshots later under:
static/img/apis/client/cinematic/cinematic-timeline-overview.pngstatic/img/apis/client/cinematic/cinematic-shot-path.pngstatic/img/apis/client/cinematic/cinematic-effects-layer.png