Cinematic
Overview
Ess.Cinematic is a declarative cutscene timeline runtime: hand it an ordered list of steps — camera cuts/dollies/orbits/chases, waits, spawns, actor facing, AI orders, helicopter fly-ins, narration (banner / subtitle / hint / VO), fades, shakes, music/sound cues, teleports, faction relations — and it plays them in order, pacing each with a per-step hold duration.
It is an orchestrator, not new engine spelunking — every step handler is built entirely on primitives documented elsewhere on this wiki that are already confirmed live: Ess.Camera’s cinematic take-over (beginCinematic/placeCamera/lookAtObject-auto-track/fade), Ess.Object.spawn, Ess.AIOrders.command (see Encounter Toolkit), Ess.Vehicle.flyTo, Ess.Hud, Ess.Sound/VO, and Ess.Player.teleport. See Camera, Bones & Spatial for the camera primitives Cinematic drives underneath every camera/orbit/chase step.
This is the engine a full “cinematic suite” for mission building sits on: MissionForge captures the spatial steps in-game (camera vantages, look-at points, action markers), a companion web tool authors the sequencing (order/durations/params), and both just emit a steps list this runtime plays. Ess.Contract plays one as a mission intro via def.cinematic (see Contract Engine).
Ess.Cinematic.play(steps, opts)
Ess.Cinematic.play(steps, opts) -> seq | nil
steps is a list of {type=, <params>, hold=} tables — see the step vocabulary table below for every type and its params. opts:
opts. | Default | Meaning |
|---|---|---|
camera | true | Take over the screen via Ess.Camera.beginCinematic/endCinematic. Set false to run the timeline (spawns, orders, HUD, etc.) without any camera takeover at all. |
blend | 0 | Camera blend passed to beginCinematic — 0 is instant cuts, matching the confirmed rule that a moving camera (dolly/orbit/chase) needs Blend 0 to stay smooth. |
skippable | true | Whether the skip key fast-forwards the timeline. |
skipKey | 0x1B (ESC) | The virtual-key code polled via Ess.Input.poll() each tick to trigger a skip. |
startFade | false | If true, opens on a black screen (Ess.Camera.fade(1)) — fade back in via a fade step. |
i | 0 | Player index (co-op seat) the cinematic plays for. |
onDone | — | fn(ctx) called after the sequence finishes (naturally, by skip, or by stop()) — fires after control/camera are already fully restored. |
Timing model: steps fire strictly in order. After firing a step, the timeline advances hold seconds before firing the next one — hold omitted or 0 means the next step fires the same tick, which is how several actions (e.g. a spawn + a subtitle) start together. A {type="wait"} step does nothing but occupy a hold — read its duration from time/seconds if hold itself is omitted.
Step context (ctx), shared across every step in one sequence:
ctx.named[name] = guid— aspawnstep withname=registers here;camera look=/order target=/fly target=and other refs resolve names against this table (plus the special refs"player"/"hero"and"partner"/"player2", and a raw guid passed through as-is, and finallyEss.Guid(ref)as a world-object-name fallback).ctx.groups[grp] = {guids}— aspawnstep withgroup=buckets into here and registers the group withEss.AIOrders.setGroup, so anorderstep (or any otherEss.AIOrderscall) can command it.ctx.track— anEss.Track. Aspawnmarkedephemeral=trueis cleaned up when the cutscene ends; by default aspawnpersists into the mission (mission actors are meant to outlive the cutscene that introduced them).
Guarantee: a cutscene can never strand the player. Camera and control are always restored on finish, skip, or stop() — this isn’t opt-in behavior, it’s unconditional in the finish() internals (see Guarantees below).
ESC-skippable: pressing the skip key fast-forwards — every remaining step still fires (so any mission actor the cutscene spawns or positions ends up in the right place), just with every remaining hold collapsed to zero.
Step type vocabulary
type | Params | What it does |
|---|---|---|
wait | time= / seconds= | Pure pause — no handler logic, the hold alone does the work. |
camera | at={x,y,z}, look=<ref>, lookAt={x,y,z}, bone=, to={x,y,z} | Cuts the camera to at, framing either a world point (lookAt) or an object it then auto-tracks as it moves (look, optionally a specific bone — e.g. look="heli", bone="Bone_Chest" tracks a pilot). Give to for a dolly: the camera lerps at→to across this step’s hold. Point-look (lookAt) is routed through a reusable TinyGeometry anchor object rather than the coordinate form of SetLookAt, working around a confirmed engine quirk: Camera.SetPosition no-ops until an active object-form SetLookAt binding exists, and the coordinate form never creates that binding — a fixed shot framing a bare point would otherwise leave the camera stuck on the player. See Camera, Bones & Spatial. |
orbit | target=<ref>, radius=, height=, speed=, startAngle=, look=<ref>, bone= | Camera smoothly orbits target, re-reading its live position every tick — the showcase shot for a spawned object. look defaults to target itself if omitted. |
chase | target=<ref>, angle=, dist=, height=, look=<ref>, bone= | Camera follows a moving target from a fixed world angle (a clean trailing shot; a fixed angle avoids the velocity-heading jitter of an auto-trail). Tracking a moving vehicle: point look= at its pilot with bone="Bone_Chest" — SetLookAt object-tracking works on character bones, not vehicle hardpoints. |
face | who=/target=<ref>, at={x,y,z} or toward=<ref> | A direct action (not a camera move): turns a spawned actor to face a world point, or another named actor. |
spawn | template=, at={x,y,z}, yaw=, name=, group=, ephemeral=, invincible= | Spawns one object (Ess.Object.spawn’s blank-template guard applies). name= registers it in ctx.named; group= buckets it in ctx.groups and registers the group with Ess.AIOrders. Persists by default — ephemeral=true marks it for cleanup on ctx.track when the cutscene ends. invincible=true calls Ess.Object.setInvincible. |
order | group=/target=, behavior=, at=, points=, radius=, speed=, loop=, attackTarget= | Commands a spawned group (or a single named/resolved target) with any Ess.AIOrders behavior — see Encounter Toolkit for the full behavior list (move/patrol/attack/defend/hold/face/follow/flee/enter/deploy/animate). Runs through Ess.AIOrders.command(guids, behavior, opts, ctx.track). |
fly | target=<heli ref>, at={x,y,z}, height= | Sends an AI helicopter to a point via Ess.Vehicle.flyTo (Ai.Deliver underneath). |
say / banner | text=/msg= | A clean centered narration banner (Ess.Hud.banner). banner is a plain alias for say. |
subtitle | text=/msg=, hold= | A lower-third radio-style caption (Ess.Hud.radio) — the better fit for cutscene dialogue than the big centered banner. Stays up for hold seconds (default 5), then clears itself. |
hint | text=/msg=, id= | The persistent tutorial-style HUD popup (Ess.Hud.hint) — stays until explicitly hidden or the cutscene ends (the running id, default "ess_cinematic", is auto-hidden in finish()). |
vo | lines=/text=, gap= | A voice-over line sequence via MrxVoSequence.Start, with gap seconds between lines (default 1). A no-op if VO isn’t loaded on this install (pcall-guarded, checked for presence first). |
music | cue= or stop=true | Starts (or stops) the special mission music via MrxMusic. cue="" or cue="stop" is equivalent to stop=true. Default cue if none given: "mu_pmc_panicloop_01". |
sound | cue=, on=<ref> | A one-shot sound effect (Ess.Sound.cue), distinct from music/VO. on= attaches it to an object (positional — an alarm, an impact); omit for a plain UI/HUD one-shot. |
fade | to=0\|1 or out=true | Full-screen fade (0 clear, 1 black) via Ess.Camera.fade. Pair two steps for a fade-out-then-in transition. |
shake | preset=, amplitude=, duration= | Camera shake for impacts, via Ess.Camera.shake. |
teleport | at={x,y,z}, yaw= | Warps the hero(es) via Ess.Player.teleport — co-op safe. |
relations | pairs={{a,b,set}, ...} | Sets a faction stance for the scene via Ess.Relations.apply. Not auto-restored — a mission’s relations are meant to persist past its intro; use Ess.Relations directly (holding the handle) if you need this scoped/reverted instead. |
func / custom | fn=function(ctx, seq) ... end | Arbitrary code — the escape hatch for anything not covered above (also the web tool’s own “custom” step type). |
Every step handler is pcall-wrapped individually (fireStep): a bad or erroring step is logged and the timeline carries on rather than aborting the whole cutscene.
Reusable cutscenes: define / playNamed
Ess.Cinematic.define(id, steps, opts) -- register a reusable cutscene under a name
Ess.Cinematic.playNamed(id, extraOpts) -> seq | nil
define registers a steps/opts pair under a string id (logged; re-defining under the same load is fine — the registry resets fresh every script load). playNamed plays a previously defined cutscene; extraOpts, if given, is shallow-merged over the defined opts (so the same cutscene can be replayed with, say, a different onDone callback each time) rather than replacing them outright.
Per the source’s own header comment, a defined cutscene is meant to be referenced either directly via playNamed, from a contract’s def.cinematic = "id", or from a trigger’s own cinematic support effect ({effect="cinematic", cinematic="id"}) — the contract-side wiring for the latter two lives in Ess.Contract’s own source, which is outside what this page verifies; treat that specific integration as documented-in-comment rather than independently confirmed here.
Control API
Ess.Cinematic.skip() -- fast-forward the active cutscene (remaining steps fire, holds collapse to 0)
Ess.Cinematic.stop(seq) -- end it now, restore control, run onDone (defaults to the active sequence)
Ess.Cinematic.isPlaying() -> bool
Ess.Cinematic.active() -> seq | nil
Only one cutscene plays at a time — calling play() while one is already active calls stop() on it first (the new one supersedes, it doesn’t queue or error).
Guarantees
- Camera/control are always restored.
finish()unconditionally callsEss.Camera.endCinematic(unlessopts.camera == false, since then it never took control) andEss.Camera.fade(0)— the screen can never be left stuck faded to black, even if the sequence ends mid-fade. - A persistent
hintstep is always cleared on finish, viaEss.Hud.hideHint. - Only
ephemeral=truespawns are cleaned up at the end (ctx.track:closeAll()) — everything else a cutscene spawns is left in the world by design, since mission actors are meant to outlive their intro.ephemeralspawns andfollow-behavior timers (viaAIOrders) both ride on the samectx.track.onDonefires after this cleanup and after the “done” log line, so a callback’s own logging reads in the correct order. - ESC-skippable by default, configurable via
opts.skippable/opts.skipKey; the key that launched the cutscene itself is discarded (Ess.Input.clear()) so triggering play doesn’t also instantly skip it. - A single bad step never aborts the timeline — each step handler is individually
pcall-wrapped and logged on error, then the sequence continues to the next step. - A reload resets
Ess.Cinematic._activetonilunconditionally — a sequence in flight when the world reloads is simply gone, never resumed as a stale handle.
Easy tier
Ess.Easy.Cinematic is the zero-opts entry point: play(steps, onDone) (skippable, camera takeover, no other options to think about) and shot(at, lookAt, seconds) -> step — sugar that builds one static {type="camera", ...} step, so a hand-authored list can read as a storyboard: { shot(a,b,4), shot(c,d,3), ... }. See Ess.Easy for the full one-liner catalog.
Example
From the shipped a_cutscene.lua recipe — a two-shot cutscene with a named ephemeral actor:
local steps = {
{ type = "spawn", template = "Veyron", at = { px + 14, py, pz }, name = "hero_car", ephemeral = true },
{ type = "camera", at = { px + 22, py + 4, pz + 6 }, look = "hero_car", hold = 1.2 },
{ type = "subtitle", text = "Recipe: a two-shot cutscene.", hold = 0 },
{ type = "orbit", target = "hero_car", radius = 9, height = 4, speed = 60, hold = 1.2 },
}
Ess.Cinematic.play(steps, {
skippable = true,
onDone = function()
-- fires after the last step AND after control is fully restored
end,
})
See also
- Camera, Bones & Spatial —
Ess.Camera’s cinematic take-over,SetLookAtobject-vs-point quirk, and the bone/hardpoint trackingcamera/orbit/chasesteps drive. - Encounter Toolkit —
Ess.AIOrders(theorderstep) andEss.Relations(therelationsstep), documented in full there. - Sound & HUD —
Ess.Hud.banner/.radio/.hintandEss.Sound.cue, the narration steps. - Contract Engine —
def.cinematicplays one of these as a mission intro. - Essentials (Ess) — the framework index this page belongs to.