Cinematics
Camera shots for a story scene, written as data in local frames (the room, a person, the space between two people), played while the script waits.
A cutscene is a list of shots. Each shot puts the camera somewhere, points it at something, and holds or moves for a few seconds. Dialogue keeps running through the textbox over it, so a scene can be shot and written at the same time.
There is no level to author against: every room is generated when the player walks in, and stands wherever the city put the building. So a shot is never a world position. It is a position in a frame the shot names:
| frame | origin | axes |
|---|---|---|
room |
the floor centre of the room the player is in | X and Y are a fraction of the room's half-size (-1 and 1 are the walls, 0 the middle); Z is metres above the floor |
person:<role> |
that person's feet | X forward, Y right, Z up, in metres |
between:<a>,<b> |
the floor point between two people | X runs from a towards b, in metres |
piece:<match> |
the floor centre of a piece of furniture in the room whose kind, lead type or id contains <match> (piece:TV, piece:Sofa) |
X points out of the piece's front, Z up, in metres. Game:Piece("<match>") gives the same piece in the stage frame. No such piece: the shot plays framed on the room instead and the log says so |
piece:window |
the shell's window nearest the player (windows are not furniture): its centre on the floor, a wall tile wide | X points into the room from the glass, Z up, in metres |
The same shot frames a studio flat and a warehouse; a close-up follows whoever plays the role.
A cutscene file
Cutscenes/<id>.ulcutscene in your mod:
json{
"letterbox": true,
"fade_in": 0.6,
"fade_out": 0.8,
"shots": [
{ "frame": "room", "from": [0.8, -0.8, 1.7], "to": [0.4, -0.8, 1.6], "look_at": [0, 0, 1.1], "fov": 38, "duration": 4 },
{ "frame": "person:friend", "from": [1.4, 0.5, 1.6], "look_at": "face", "fov": 32, "duration": 3, "blend": 0.8 },
{ "frame": "between:player,friend", "from": [0, 2.2, 1.5], "look_at": [0, 0, 1.3], "duration": 3 }
]
}
One shot:
| key | default | |
|---|---|---|
frame |
room |
See the table above. |
from |
[0, -0.9, 1.6] |
Where the camera starts, in the frame. |
to |
Where it ends after duration; left out, the camera holds. |
|
look_at |
[0, 0, 1.2] |
A point in the frame, or "face" (the frame person's head) or "person:<role>". |
look_at_end |
The point it looks at by the end; left out, the look holds. | |
fov |
50 | Degrees. 30 to 40 is a portrait lens, 60 and up is wide. |
duration |
3 | Seconds. |
blend |
0 | Seconds the camera takes to glide from where it was. 0 is a cut. |
ease |
smooth |
smooth eases the move in and out; linear does not. |
While a cutscene plays, every HUD widget is hidden except the dialogue box and the choice menu; Game:HideHud(true) does the same on its own, Game:HideHud(false) brings the HUD back. The room frame is the room the player walked into; a scene launched from the map uses the room under any role, else the nearest room.
letterbox draws the black bars, fade_in and fade_out are seconds from and to black. Roles are the actors the script hands in; player is always there. A shot whose role is missing is skipped with a log line, the rest still plays.
Words on the picture
Footage is captured in the game, so the game draws the words. None of these open the dialogue box.
luaGame:Title("UNDERLIVES", "Life Has No Rules", 3) -- a card in the middle, gone after 3 s
Game:Subtitle("Nobody told her the rent was due.", 4) -- a line along the bottom
Game:Subtitle("") -- clears it now
Game:Hold(2.5) -- the script waits, picture and text stay
A duration of 0 keeps the text until the next call.
Marks and props
A beat should not depend on where a walk left everyone. A scene can place its cast and a few practicals itself, in metres in the stage frame: the origin is the player's feet, forward is the room's facing, the same frame a shot gets with frame = "stage".
luaGame:StageActor(landlord, {1.2, 0.4, 0}, 180) -- 1.2 m ahead, a little right, facing back at the player
Game:StageProp("cylinder", { person = cop, socket = "rHand",
position = {0, 0, 0.1}, size = {0.03, 0.03, 0.4}, color = {0.05, 0.05, 0.05} }) -- a baton in the hand
Game:StageProp("cube", { position = {0, 1.5, 0.4}, size = {1.2, 0.6, 0.8} }) -- a crate to sit on
Game:StageLight({ position = {0.6, -0.8, 2.2}, intensity = 2500, color = {1, 0.85, 0.7} })
Game:Shot({ frame = "stage", from = {2, 0.4, 1.6}, look_at = "face", fov = 40 }, false)
Game:PlayCue("door_slam")
A staged person stands still with collision and gravity off until the scene ends or Game:ClearStage() runs; props and lights are destroyed with them. StageActor and StageProp return false when the handle is not a person or no player exists yet. Shapes are cube, cylinder and sphere; sizes are scales where 1 is one metre; a rotation of {pitch, yaw, roll} in degrees is applied in the stage frame, or relative to the socket for a carried prop.
Shooting outside the game
A scene launched from the city plays in the dialogue layer with no room under it, so the camera has only the map to look at. To film a beat, run it in the cinematic harness: one venue, the player's body in it, your script, nothing else on screen.
UnrealEditor.exe <uproject> "/Game/Maps/RoomTest_Generic?game=/Script/Underlives.CinematicGameMode" -game -Cinematic=my_beat -RoomTag=residential -LookHour=19 -windowed -ResX=1920 -ResY=1080
-Cinematic names a Scenes/<name>.ulscene of your mod. -RoomTag is the venue (the same tags as rooms: residential, cafe, office, …), -RoomSeed replays one generated layout, -LookHour sets the hour, -Extras fills the venue with people. -ShotEvery=2 writes a frame every two seconds to Saved/Screenshots/Cinematic/<name>/, HUD included, to check a shot without watching it. The script spawns and dresses whoever else it needs, places them (moveToPersonStand, animations), and shoots. Game:Say has no dialogue box here: use Subtitle.
A shot is written against one room, so a beat should not roll its furniture: -RoomLayout=<preset> furnishes the venue with a layout preset instead (a Rooms/Layouts/<file>.ulroomlayout exported from the room editor, named by its name field or its file name). The venue tag, the room and the grammar come from the preset; -RoomTag is then ignored. Corners away from the door wall (y near 1) usually hold the wardrobes, so a wide shot starts near the door wall (y near -1), like the room's own viewpoint.
Framing live
The harness stays up between tries. Save your scene, a .ulcutscene or a Lua/*.lua library and the beat replays from the top: the player back on the starting mark, your files read again. Console commands drive the camera without touching a file:
| Command | Does |
|---|---|
cine.reload / cine.stop |
replay now / drop the scene, everyone stays put |
cine.cam {"frame":"person:player","from":[1.6,0.4,1.6],"look_at":"face","fov":32} |
hold that camera (the keys of a shot) |
cine.cutscene <id> [shot] [seconds] |
play one cutscene file alone, from a shot (0-based) |
cine.seek <shot> [seconds], cine.pause |
freeze on a moment of a shot |
cine.shot [name] [noui] |
a frame to Saved/Screenshots/Cinematic/Live/ |
cine.hour <h>, cine.room <venue> [seed], cine.room layout=<preset> |
the hour, the furniture |
The same lines written to Saved/Cinematic/Live/commands.txt run without typing, and Saved/Cinematic/Live/status.json answers: the shot playing, the camera in each frame (from_in_frame, ready to paste into from), the lenses the game had to move (shot_warnings) and your script's errors. On the city map, add -CineLive to get the same.
Playing it
lua-- Waits until the last shot ends.
Game:PlayCutscene("uls1_funeral_arrival", { friend = Cast.get("friend") })
-- One shot inside dialogue: the camera stays on them while they speak.
Game:Shot({ frame = "person:friend", from = {1.2, 0.3, 1.6}, look_at = "face", fov = 35, roles = { friend = friend } }, false)
Game:Say(friend, "Sad", "They did not suffer. That is what they tell you.")
Game:EndCutscene()
Game:Fade(1.0, true) -- to black
Game:Fade(0.5, false) -- and back
Game:Letterbox(true)
PlayCutscene and Shot return false, without pausing, when there was nothing to play: a missing file, a bad frame. The reason is in the log. A scene that ends or errors under a shot hands the camera back by itself.
The lens
A shot can carry a lens as well as a position. All keys are optional and work in Lua shots and in
.ulcutscene files alike.
luaGame:Shot({ frame = "person:friend", from = {1.2, 0.3, 1.6}, look_at = "face",
focus = "face", aperture = 1.8, -- depth of field: what is sharp, how shallow (f-stop)
fov = 35, fov_end = 30, -- a slow zoom over the shot
shake = 0.2, -- handheld, 0..1
roll = 4, -- the horizon tilted, degrees
speed = 0.5, -- the world at half speed while the shot plays (the shot clock stays real)
orbit = 40, -- the lens turns round the look-at point by 40 degrees over the shot
duration = 2.5, roles = { friend = friend } }, true)
focus is "face", "person:<role>" or a point in the shot's frame; focus_distance (metres from
the lens) does instead. A focus with no aperture gets f/2.
The picture
luaGame:Grade("club_red", 0.8) -- Grades/club_red.ulgrade from any mod, reached in 0.8 s
Game:Grade({ saturation = 0.4, vignette = 0.9, blur = 0.6, chroma = 2 }, 3) -- composed in place
Game:Grade(nil, 1) -- back to the plain picture
Game:VenueLights({ scale = 0.15, color = {1, 0.4, 0.5}, flicker = 0 }) -- the room's own lights; practicals untouched
Game:Flash({1, 1, 1}, 0.3) -- a hit of white that clears
Game:Strobe(8, 2.0) -- 8 Hz for two seconds
Game:Freeze(1.5) -- the world stands still; camera, words and music go on
Game:Fade(0.5, true, {1, 1, 1}) -- to white
Game:TitleStyle({ size = 28, color = {1, 1, 1}, y = 0.86 })
Game:Caption("Rent: $410 due Friday", { x = 0.08, y = 0.9, size = 22 }, 3)
A grade file holds the keys of the table: tint [r,g,b], saturation, contrast, gamma,
vignette, grain, bloom, chroma (chromatic aberration), blur (the whole frame out of
focus, 0..1), exposure (stops), temperature (Kelvin). Everything the grade, the lights, a
speed or a Freeze changed is given back when the scene ends.
Sound in a scene
luaGame:Music("Music.Venue.stripclub") -- crossfade to that cue; "" fades the music out
Game:Sfx("Trailer.Gunshot") -- a cue, or a file: Game:Sfx("Effects/door_slam.ogg", { at = landlord, volume = 0.8 })
Game:Duck(0.35, 0.3) -- the music down to 35 % under a line
Game:Say(p, "Angry", "Friday.")
Game:Duck(1, 0.8)
A file is looked for as given, then under every mod's Audio/ folder.
Markers and the live director
Game:Marker("hands") names a point in a scene. It costs nothing when the scene plays; the
live director uses it to jump: cine.run my_scene@hands runs the scene with no waits (holds,
shots, lines; a choice takes its first available option) until that marker, then plays it. Every
run also writes Saved/Cinematic/Live/timeline.json: each shot, line, voice clip, subtitle,
sound, grade and marker with its time and, during cine.record, its frame.
Game:Extras(6, "dance") fills the room with six people of the venue's kind doing that;
Game:ClearExtras() sends them off; Game:Seed(n) fixes the random draws that follow.