Cinematics
Usage guide of game.cinematic: authoring a sequence (shots, cast, timeline, cues, acting tracks, spoken lines), settings and defaults, playing from the game, events and hooks, engines.
Cinematics and cutscenes (game.cinematic) #
Real-time introductions and cutscenes made of authored camera shots, with titles, captions, letterbox, fades, staged characters and props, animated actors, timed audio and spoken lines. Readable offline: gripforge docs game-cinematic, or the MCP tool gripforge_docs (topic game-cinematic).
What it is for #
A cinematic is data: a sequence of shots that the player of your game plays over the live world. The kit pauses the simulation while the sequence advances on presentation time, releases the input, hides the HUD, then restores the gameplay camera under a short fade and resumes. Escape, the visible Skip button and the gamepad B button skip; the reduced-motion preference gives static framings. A sequence that is empty or invalid never locks the controls.
Turn it on #
gripforge_gamekit_install { project_id: "gkp_…", id: "game.cinematic" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "game.cinematic", config: { "once": false, "letterbox": 0.1 } }
gripforge_game_project { project_id: "gkp_…", action: "data", collection: "cinematics",
documents: [{ "id": "intro", "subtitle": "PROLOGUE", "shots": [
{ "id": "reveal", "duration": 6, "space": "ground", "from": [-20, 18, -30], "to": [-12, 12, -22],
"target": [0, 2, 0], "fov": 48, "endFov": 43, "title": "Your game", "caption": "Your story begins here." } ] }] }
Without an authored intro, the player plays a generated three-shot introduction after Play (or after the world loads when ui.menu has no title screen); it waits for model loading and an attached presentation. Authoring a sequence with the id intro replaces it. The kit requires game.engine. The full reference is gripforge kit api game.cinematic.
Settings and defaults #
| Key | Default | Meaning |
|---|---|---|
intro | intro | id of the sequence played automatically |
autoplay | true | play it after Play |
once | true | once per saved run; play(id, true) replays |
skippable | true | Escape, Skip button, gamepad B |
letterbox | 0.115 | bar height, as a fraction of the screen |
fadeIn, fadeOut | 0.7, 0.6 | seconds |
handoff | 0.35 | seconds of the fade back to the gameplay camera |
hideHud | true | hide the HUD while playing |
hidePlayer | false | hide the player character |
groundClearance | 1.2 | metres kept above the terrain by ground- and player-relative cameras |
A mod can change all of them and add, patch or remove documents of the cinematics collection.
The sequence #
Up to 16 shots, 60 seconds each, 600 seconds in all; shot ids are unique. Coordinates are in metres.
| Field of a shot | Meaning |
|---|---|
id, duration | |
space | world (absolute XYZ), ground (altitude above world.ground), player (offsets rotated by the player's yaw at the start), actor (with of: "<cast id>": offsets rotated by the actor's heading, for an over-the-shoulder shot) |
from, to, target, targetTo | camera start, end, look-at point, optional moving look-at |
fov, endFov | lens |
easing | smooth (default) or linear |
title, caption | text shown during the shot |
actors | what changes when the shot begins: { id, at?, facing?, anim? } |
World and actor coordinates are exact; only ground- and player-relative cameras are kept above the terrain. Keep shots inside the loaded region of the world: a camera does not stream distant places. An optional audio names an existing audio.core sound and stops only that voice when leaving.
Cast #
A sequence may carry up to 32 cast members, present only while it plays: { id, slot, variant?, at, facing?, space?, anim? } (a model of your game as bound, for example slot: "content", variant: "<archetype id>"), or { id, player: true, at } (a stand-in drawn with the player's model while the real player is hidden). kind: "prop" with size: [w, h, d] fits a model in a box of that size (at most 2000 m per axis); for a character, size fits the height with feet at at. A cast member can have parent: "earlier_cast_id": its poses then become local to that parent (an exhaust on a vehicle, a carried prop). Cast members are presentation entities without collision or gameplay, and are removed on every exit. A space exterior can sit in a separate staging area of the loaded scene and hard-cut to the real interior.
Animated timeline #
The optional timeline runs on presentation time, in seconds from the start of the sequence:
tracks:{ actor, interpolation: "linear" | "smooth" | "cubic", keys: [{ time, at?, rotation?, scale? }] }, one track per actor, 2 to 128 keys, first time zero, strictly increasing. Rotation is degrees[pitch, yaw, roll](shortest way per axis), scale from 0.001 to 100. The last key holds. Cubic positions can overshoot: check the clearance.cues:{ id, time, actor?, anim?, sound?, voice?, volume?, pitch?, stopAfter? }, up to 64, distinct ids. A clip cue restarts the actor's clip;soundnames a bank entry ofaudio.coreand the kit stops only its own voice (atstopAfter, on skip, end or cancel). During the pause, use a bank entry on themusicoruibuses (or onvoicewithvoiceDuringPauseenabled);sfxandambiencestay muted. Every crossed cue firescinematic.cueonce.shakes:[{ time, duration, amplitude: [x, y, z], frequency? }], deterministic decaying camera translation; suppressed by reduced motion.poses,expressions,reaches(acting tracks, 2 to 128 keys each):posesrotate a named bone (bone,rotationdegrees,weight,additivedefaulttrue);expressionsdrive a named shape weight from 0 to 1;reachesbring a chain of three bones (upper arm, forearm, end effector) to a world target with an elbowpole. Bone and shape names belong to your model; a name alone creates no geometry. These are cosmetic: no forces, collisions or gameplay commands.
A speech cue { id, time, actor, voice: "<clip id>" } calls the dialogue.voice service on that staged actor (install dialogue.voice and audio.core, set voiceDuringPause: true, put the recording in the sound bank); it cannot be combined with sound, volume, pitch or stopAfter. Skip, cancel, end, scene exit and load stop cast speech.
Timeline sequences are supported by the native player and the Rust kit host; the web runtime in JavaScript rejects them instead of showing static actors.
{ "id": "departure",
"cast": [{ "id": "vehicle", "slot": "content", "variant": "vehicle", "kind": "prop", "at": [0, 20, 0], "size": [8, 4, 12] }],
"shots": [{ "id": "wide", "duration": 4, "from": [-20, 28, -25], "to": [-15, 25, -22], "target": [0, 20, 0] }],
"timeline": { "tracks": [{ "actor": "vehicle", "interpolation": "cubic", "keys": [
{ "time": 0, "at": [-8, 20, 0], "rotation": [0, 0, -10] }, { "time": 2, "at": [0, 22, 6] , "rotation": [-5, 20, 18] },
{ "time": 4, "at": [12, 24, 18], "rotation": [0, 30, 0] }] }],
"cues": [{ "id": "engine", "time": 0, "sound": "departure_engine", "stopAfter": 4 }] } }
Playing from your game #
| How | What |
|---|---|
Service game.cinematic | attach(on?), play(id?, replay?), skip(), cancel(), active(), view(), camera(), sequences(), setReducedMotion(on) |
| Script verbs | { "play_cinematic": "arrival" } or { "type": "play_cinematic", "id": "arrival", "replay": true }; { "skip_cinematic": {} } |
| Variables | cinematic.active, cinematic.id |
Completed ids are saved; loading a save never replays the introduction. Active sequences are cancelled on load or scene exit so the player cannot stay stuck in a cutscene.
Events and hooks #
| What | Name | Detail |
|---|---|---|
| Events | cinematic.started { id }, cinematic.shot { id, shot, index }, cinematic.cue { id, cue, time, actor, entity, audio } | |
| Event | cinematic.finished { id, reason } | emitted exactly once, reason is completed or skipped. Put required story changes here, so skipping still gives the intended final state |
| Event | cinematic.cancelled { id } | load, scene exit or teardown; the sequence is not marked completed |
| Hook | cinematic.start { id, replay } | before a sequence starts; a tap may change id to another loaded sequence, or cancel (play returns false) |
| Hook | cinematic.skip { id, shot, elapsed } | before a skip; a cancel keeps it playing |
cancel() is the safety exit and cannot be intercepted.
{ "id": "no_skip_finale", "hook": "cinematic.skip", "match": { "id": "finale" }, "cancel": true }
Engines #
The web and native players play it; the timeline, acting tracks and voiced cues run in the Rust kit host and are presented by the native player. Other engine presenters are not certified for them.
When it does not do what you expect #
| Symptom | Cause and fix |
|---|---|
| The intro never plays | autoplay: false, once: true and it already played in this save, or the sequence id differs from intro |
| A story change is missing after Skip | it was attached to a shot or a cue: move it to cinematic.finished |
| An actor does not move | the sequence has no timeline track for it, or it runs in the JavaScript web runtime |
| A cue sound is silent | it is on an sfx or ambience bus (muted during the pause): use music, ui or voice |
| The camera dips under the ground | world space ignores the clearance: use ground space, or raise the points |
First-person awakening (1.7) #
Add wakeUp to a sequence to open curved eyelids over the live world, blink and gradually regain focus. Install the example below in cinematics, then call play("wake_up"). Set letterbox: 0 and fadeIn: 0. The player model is hidden automatically; camera positions do not move the gameplay character or switch its controller.
<!-- test: kits/game_engine/host/tests/game_cinematic.rs -->
{
"id": "wake_up",
"wakeUp": {
"closedDuration": 0.4,
"openDuration": 3.2,
"focusDuration": 5,
"blur": 0.018,
"blinkAt": 1.5,
"blinkDuration": 0.35
},
"shots": [
{
"id": "eyes_open",
"duration": 7,
"space": "player",
"from": [0, 1.65, 0],
"to": [0, 1.65, 0],
"target": [0, 1.9, 6],
"targetTo": [0, 1.65, 6],
"fov": 70
}
]
}
All durations are seconds. Opening and focus start after closedDuration; blur is a fraction of screen height (0–0.04, zero disables blur). Supply both blinkAt and blinkDuration, or neither. Opening, focus and blink must finish within the sequence. Adjust shots to your scene. A menu pause holds the effect; replay restarts it; skip, cancellation and handoff remove it. Reduced motion uses a simple fade without blur or blinking. reducedMotion() reads the preference; cinematic.motion { on } reports changes.
This effect is implemented in the shared Rust presenter and validated in Bevy WebGPU and WebGL2. Other engine presenters and the historical JavaScript exporter are not certified for it. The furnished infirmary is an independent example, not a dependency of the kit.