Builtin Library
README.md
cutscene
Author a cutscene as data, then play it. The global cutscene is injected by
the prelude, so no require is needed.
A cutscene takes the camera along a shot path, letterboxes and fades the image at its boundaries, shows subtitle lines, fires cues at the moments you name, holds the player still while it runs — and gives every bit of that back when it ends, however it ends.
cutscene.play({
duration = 6,
letterbox = 0.12,
shots = {
{ dur = 6, pos = { { -8, 3, 12 }, { 4, 2, 5 } }, look = "Hero", fov = { 55, 38 } },
},
subtitles = {
{ t = 0.5, dur = 4, text = "They came at first light.", speaker = "Mira" },
},
cues = {
{ t = 3, run = function() entity.find("gate"):despawn() end },
},
})
Playing
cutscene.play(source, opts?) -> Playback—sourceis a definition table or the path of a module returning one.optsoverrides definition keys for this play. The cutscene advances on its own; nothing has to be ticked. Starting one while another plays stops that one first.cutscene.stop(reason?) -> boolean,cutscene.skip() -> booleancutscene.current() -> Playback?,cutscene.isPlaying() -> booleancutscene.status()—{ name, time, duration, progress, paused, rate, playing, shot }cutscene.sweep()— remove a camera / subtitle / overlay left behind by a playback that never got to end.
Authoring aids that read a shot path without playing it: cutscene.evaluate(shots, t),
cutscene.shotDuration(shots), cutscene.easings().
The playback
play returns the handle driving the cutscene:
skip() · stop(reason?) · seek(t) · pause() · resume() · setRate(r) ·
onFinished(fn) · time() · getDuration() · progress() · isPlaying() ·
status()
onFinished(fn) is called with (playback, reason) where reason is
"completed", "skipped" or "stopped" — the place to hand control back to the
game.
The definition
| Key | Default | What it does |
|---|---|---|
duration | the shots' total | How long the cutscene runs, in seconds. |
shots | none | The camera path. See below. |
subtitles | none | { t, dur?, text, speaker? } per line. dur defaults to 3. |
cues | none | { t, run, name? } — run(playback) fires once as the clock crosses t. |
A skippable cutscene puts a Skip control on the player's surface for as long as
it is on screen (the keys in skipKeys, the pad buttons in skipButtons, and an
on-screen button), and takes it back off when the cutscene ends. It names at least
one of the three: emptying both lists with skipTouch = false is refused where the
playback is built, and skippable = false is how a cutscene plays out in full.
A skip fires every cue between the skip point and the end, so a cutscene that opens a door on its way out opens it whether or not the player watched. A seek does not: it re-derives the frame that time would have produced rather than running the clock through to it, so cues behind the new time count as already fired. Play through or skip to watch one happen; a backwards seek re-arms every cue ahead of it.
The letterbox and fade are a post pass drawn last, so they cover the UI as well
as the scene — keep a HUD a cutscene shows clear of the bands.
| letterbox | 0 | Band height per side, as a fraction of the screen height. |
| fadeIn / fadeOut | 0.6 | Seconds of fade at each boundary. |
| fadeColor / barColor | black | { r, g, b } in 0..1. |
| skippable | true | Whether the player can end it early. |
| skipKeys | Escape / Space / Enter | Keyboard codes that skip it. Empty declines the keyboard. |
| skipButtons | start / south / east | Gamepad buttons that skip it. Empty declines the pad. |
| skipTouch | true | Whether the on-screen skip button is drawn. |
| skipLabel | Skip | What the on-screen button and a rebind screen call it. |
| takeCamera | true when it has shots | False plays it over the viewer's own camera. |
| freezePlayer | true | Hold the player still for the duration. |
| hidePlayer | false | Hide the player's body. |
| renderLayers | scene default | Render layers the cutscene camera draws. |
| onStart / onFinish | none | onStart(pb), onFinish(pb, reason). |
Shots
A shot moves the camera between anchors over dur seconds.
{ dur = 4, pos = { { 0, 3, 10 }, { 0, 2, 4 } }, look = "Hero", fov = { 60, 40 }, ease = "inOutQuad" }
An anchor — pos or look — is a world position, the entity to read one from
(by name, id or proxy), or a list of either that the shot travels along as a
polyline. Reading the anchor from an entity is what lets a shot frame something
that moves: it is resolved on the frame it is drawn, so a shot aimed at a walking
character tracks the walk.
posOffset / lookOffset shift a resolved anchor — look = "Hero", lookOffset = { 0, 1.6, 0 }
frames the character's head rather than the ground under their feet.
ease takes any name cutscene.easings() lists, or a function.
Playing from a scene
A cutscene definition living in its own module is content: it can be played by
name, hot-reloads while you watch it, and the cutscene toolbox can drive it.
-- mygame/scenes/intro.module/init.luau
return { duration = 8, shots = { … }, subtitles = { … } }
-- the scene's entrypoint — the game opens on the cutscene
function onLoad()
cutscene.play("mygame.scenes.intro")
end
For a cutscene that fires when the player reaches somewhere, put a
CutsceneTrigger component on the entity instead of writing the wiring.
While authoring
The cutscene toolbox drives whatever is playing without waiting for it:
zero cutscene status, zero cutscene seek 6.5, zero cutscene pause,
zero cutscene rate 0.25, zero cutscene stop.
Submodules
path.module evaluates the shot list, stage.module owns what a cutscene takes
over and gives back, and runtime.module is one playback's clock. Each is
independently requirable; cutscene composes them.