Skip to content

Tutorials · Cameras and Framing · Chapter 13

A gallery of cameras

75 minutes Uses: Camera, PixelPerfect, CameraRig, Parallax, Viewport, ShaderMaterial, wait, set_flag, held, anneal_to, camera_view, position, clamp, move_to, pressed, make_current

By the end

  • Eight scenes beside the town and the castle, seven of them in one corridor painted on the castle’s tileset
  • A security monitor whose view is Never, snapped twice a second by a script and drawn through scanlines
  • A doorway onto a far side that draws only while it is seen, wound round its middle by a shader; and a magnifying glass at his hand, a view and its Sprite on one node
  • A title screen of three dusk planes, its clouds drifting for ever on Autoscroll and Repeat Y
  • A spyglass held on Down: a second framing that eases out to a far tower and back
  • An auto-scroller that a script glides while its rig is off, carrying him at its edge
  • Cinema bars made of the camera’s own Background, and a phone’s two cameras, which E swaps
On this page

Eight scenes and a corridor

The Lookout is built. This chapter puts a camera or a view to eight more uses, each a small case in a scene of its own beside the town and the castle: a security monitor, a doorway onto a far side, a magnifying glass, a title screen, a spyglass, an auto-scroller, cinema bars and a phone. To open one, open scenes in Assets and double-click it. Play plays the scene that is open; Run game still starts the town.

Seven of them stand in one corridor, a long room of the castle’s stone with him in it, so the corridor comes first. Its air runs from x 0 to 2000 and y -192 to 0, with two cells of stone all round and three more rows under the floor: the floor’s top is y 0, each end is a wall, and a camera that follows him sees stone to the bottom of its view.

  1. Choose File › New scene. Press + Add node, choose Node, and rename it gallery-monitor. Choose File › Save as…, open the project’s scenes folder, name the file gallery-monitor.ron, and save.
  2. Select gallery-monitor, press + Add node and choose Tilemap. Rename it room, and leave its Position at 0, 0.
  3. With room selected, open the Tilemap tab beside the Inspector. Drag art/old-dark-castle-interior-tileset.png from Assets onto New tilemap…. Before you paint, rename the new file gallery.tilemap in Assets.
  4. In the tab’s Tileset fold, set Margin y to 8: tiles 16 by 16 and 52 columns, as the castle’s. In its Layers fold, click ground, the solid layer, so it is the one you paint on. back stays empty.
Margin
The Tilemap tab’s Margin, in its Tileset fold: where the grid of tiles starts on the sheet, in pixels from its top-left corner. New tilemap… starts it at the corner, 0, 0. The castle’s tiles are 8 lower: with Margin y 8, every row of the stone pillar the corridor is made of is a whole tile you can pick.

The stone is the castle’s own, the pillar at x 320, y 152 of the sheet, two tiles across: its moss cap, the stone under a cap, and its body, two rows that alternate. Every cell of the corridor takes the pillar’s left tile in an even column and its right tile in an odd one. A cell in column c and row r covers x 16c to 16c + 16 and y 16r to 16r + 16, so row 0 is the first row under the floor’s top; the Viewport’s cursor line says where you are.

The stoneIn an even columnIn an odd column
The moss cap#489 (320, 152)#490 (336, 152)
Under a cap#541 (320, 168)#542 (336, 168)
The body, on an even row#593 (320, 184)#594 (336, 184)
The body, on an odd row#645 (320, 200)#646 (336, 200)
The corridor’s stoneRowsColumnsStone
The ceiling-14 and -13-2 to 126the body
The west wall-12 to 4-2 and -1the body
The east wall-12 to 4125 and 126the body
The floor’s top00 to 124the moss cap
Under it10 to 124under a cap
The floor’s body2 to 40 to 124the body
  1. Under Tools, click Brush. Choosing a tool turns on Paint mode at the top of the tab: a click in the Viewport now paints, and selects nothing.
  2. To pick a tile, hover the tiles in the picker at the tab’s foot until the tooltip reads its number, #593 for the body’s first, and click it: the Tile line over the picker shows it. If the tiles are too small to hit, raise Zoom beside it.
  3. Paint each piece’s first two columns by hand, one tile a cell, all its rows; the floor’s three pieces share their columns, so paint them as one block five rows deep. Then click Pick and drag over the block, click Stamp, and click the cell two columns along, and every second cell after it, to the piece’s end. Pick lifts any block you drag over, so once four columns are painted you can pick the four and stamp four at a time, then eight. Stop at the piece’s end: a stamp that runs past it paints over what is beyond.
  4. The floor’s 125 columns leave one over at its east end, column 124, which takes the left-hand tiles: paint its five rows on their own. The ceiling’s 129 columns leave one over too, column 126, which takes the left-hand tiles: paint it on its own. The east wall starts in an odd column, so its first column, 125, takes the right-hand tiles and its second the left.
  5. The walls run down: paint their top two rows by hand, and stamp that block down every second row; the row left over at the foot you paint on its own.
  6. Press the tab’s Save, which writes the tilemap. In the Inspector, set room’s Sort Layer to Ground, and leave Tile Size at 16, 16 and Collision ticked.
The editor with gallery-autoscroller.ron open and room selected and framed at 39.8%: a long, low room of dark green stone with a moss-capped floor, the watchman small at its west end inside a thin frame labelled follow; the Hierarchy lists room, watchman and camera; the Tilemap tab is open on the right: Paint mode and Save at its top, its Tools fold with Brush, Erase, Line, Rect, Fill, Pick, Stamp and Move, its Layers fold with back highlighted and ground under it, and its Tileset fold: Sheet old-dark-castle-interior-tileset.png, Tile w 16 h 16, Columns 52, Margin x 0 y 8, Spacing x 0 y 0, and the sheet drawn small
The corridor, 2064 by 304 with its stone, and its tilemap in the Tilemap tab.

Then him, and a camera.

  1. Press Ctrl+S. Open scenes/town.ron, right-click watchman in the Hierarchy and choose Copy. Open scenes/gallery-monitor.ron, right-click gallery-monitor and choose Paste as child. The map’s eye comes with him: select map-eye, under him, and press Delete. Set his Position to 96, 0.
  2. Select gallery-monitor, press + Add node and choose Camera. Rename it camera. Set Scaling to Letterbox, Extent to 180, Width to 320 and Background to 0.012, 0.020, 0.040, alpha 1, the town camera’s; leave Pixel Snap ticked.
  3. Press Ctrl+S.
  4. The other six corridor scenes start as this one does. Choose File › Save as…, name the file gallery-portal.ron, rename the root gallery-portal, and press Ctrl+S. Do the same for gallery-magnifier, gallery-spyglass, gallery-autoscroller, gallery-cinema and gallery-phone.

Each case below opens its own scene and adds to it. Where a case gives no other value, its camera keeps these.

A security monitor

A monitor in the corridor shows a vault far down it, through a view whose Update is Never: it draws only when a script says so. Its picture is drawn through a shader of scanlines, as an old screen’s is.

  1. In Assets, click Assets in the path bar, press Create, choose New shader, type scanlines and press Enter. Double-click it, replace everything in it with the first text below in the Code tab, and press Ctrl+S. Make snap.rgs the same way with New script, from the second.
  2. Open scenes/gallery-monitor.ron. Select camera and set its Position to 200, -60.
  3. Select gallery-monitor, press + Add node and choose Viewport. Rename it vault-eye and set its Position to 1400, -60: the vault.
  4. In its Viewport, choose New render target for Texture. Set Size to 64, 48, Zoom to 0.5 and Update to Never, and tick Drawn. Set Clear Colour to 0.10, 0.12, 0.14, alpha 1: a grey. Leave Subtree unticked and Layers empty, the whole scene.
  5. Press Add component and choose Snap. Leave its Every at 0.5.
  6. Select gallery-monitor, press + Add node and choose Shader Material. Rename it monitor and set its Position to 320, -64. In its Sprite, click Texture and choose vault-eye, and set Size to 64, 48 and Sort Layer to Fixtures. In its Shader Material, choose scanlines.wgsl for Shader, and leave lines unset.
  7. Select gallery-monitor, press + Add node and choose Animated Sprite. Rename it vault-demon, at 1400, 0, on the floor. In its Sprite, drag art/demon-idle.png onto Texture, and set Size to 160, 144, Anchor to 0.5, 1 and Sort Layer to Props. In its Animated Sprite, drag art/demon-idle.anim onto Animations, set Clip to idle and tick Frame Size.
  8. Press Ctrl+S.
wgsl scanlines.wgsl
// How many dark lines cut across the picture, from its top to its bottom. Each
// line is one row of the picture, the one under a row that is left as it is.
//! param lines: float = 24.0

// The security monitor's screen. It draws the monitor's picture, the vault's
// view, a row at a time: the first row of every pair as it is, and the second
// darkened, so the picture is cut by dark lines the way an old screen's is.
fn shade(frag: Fragment) -> vec4<f32> {
    // How far down the picture this pixel is, from 0 at its top to 1 at its
    // bottom.
    let down = region_uv(frag).y;
    // Where it is in its pair of rows, from 0 at the pair's top to 1 at its
    // bottom: past the half, it is in the second row, the dark one.
    let in_the_pair = fract(down * lines());
    let dim = select(1.0, 0.55, in_the_pair >= 0.5);
    // The picture's own colour at this pixel, times the sprite's Color; the
    // darkening takes the colour down and leaves the alpha alone.
    let picture = texel(frag.uv) * frag.color;
    return vec4<f32>(picture.rgb * dim, picture.a);
}
rgs snap.rgs
/// The security monitor's camera. Its view is Update Never and saved with Drawn
/// ticked, so it shows nothing but its Clear Colour until something unticks
/// Drawn: this does, every `every` seconds. The view is then not yet drawn, so
/// it draws the vault once more, as it is now, and keeps that picture until
/// the next snap.
component Snap {
    /// Seconds between one snap and the next.
    #[suffix("s")]
    every: f32 = 0.5,

    fn loop() {
        // Wait out the time between snaps, then untick Drawn on this view.
        await wait(every);
        set_flag("ViewportComponent", "drawn", false);
    }
}
//! param lines: float = 24.0
How many pairs of rows the picture is cut into, top to bottom: 24, the file’s own, while the monitor’s Values hold none. The shader reads it as lines().
region_uv(frag)
Where this pixel is in the sprite itself, from 0, 0 at its top-left corner to 1, 1 at its bottom-right. frag.uv is where it is in the whole texture.
await wait(seconds)
Holds the hook where it is for that many seconds; it goes on from the next line once they have passed.
The editor with gallery-monitor.ron open and vault-eye selected and framed at 631.1%: the vault’s demon, a horned blue skull, a dark cloak and red bat wings, inside a thin square frame with a cross at its middle; the Hierarchy lists room, watchman, camera, monitor, vault-eye and vault-demon; the Inspector shows Position x 1400.0 and y -60.0, the Snap with Every 0.50 s, and the ViewportComponent: Texture vault-eye, Size 64.0 and 48.0 px, Zoom 0.50, Update Never, Subtree unticked, Layers 0 items, a grey Clear Colour, Drawn ticked
The vault, and the frame of what vault-eye sees: 128 by 96.

The camera stands still at 200, -60, with no rig: it sees x 40 to 360, and the monitor, x 288 to 352, is whole in the picture. The vault is far down the corridor: vault-eye sees 128 by 96 round it, x 1336 to 1464, the demon standing there, and draws that into its 64 by 48 texture, which the monitor shows. The scanlines draw the texture a row at a time: the first row of each pair as it is, the second at 0.55 of its colour.

A Never view draws only when its Drawn is unticked, and then ticks it again. This one is saved with Drawn ticked before it has ever drawn, so it shows its Clear Colour: until the first snap, the monitor reads no signal.

The monitor close up before the first snap: a plain grey pane cut by thin dark lines, against the corridor’s night blue
No signal: the Clear Colour, cut by the scanlines.

Every 0.5 seconds Snap unticks Drawn, so the view draws the vault once more, as it is then, and keeps that picture until the next snap. It snaps every 31 steps, not 30: the wait is 30, and the loop takes a step to come round. In between, the demon goes on through its clip down in the vault, and the monitor holds still.

The game in the corridor: the watchman at the left on the moss-capped floor, and at the upper right the monitor, a grey pane cut by dark lines, on it the vault’s demon with its wings spread
The monitor, at a snap.
The monitor close up: the demon on its grey pane, cut by dark lines, takes a new pose twice a second, its wings up, then down, and holds each until the next
The monitor, a frame for each snap. Watch the demon on it jump to a new pose twice a second and hold it until the next.

The vault is a pass of its own twice a second, not in every frame: between snaps the view does not draw, and its texture keeps the last picture.

Node, under gallery-monitorComponentFieldValue
projectshader, scriptscanlines.wgsl, snap.rgs beside project.rengy
cameraNodePosition200, -60
vault-eyeNodePosition1400, -60
ViewportTexture, Size, ZoomNew render target; 64, 48; 0.5
Update, DrawnNever; ticked
Subtree, Layersunticked; none
Clear Colour0.10, 0.12, 0.14, 1
SnapEvery0.5
monitorNodePosition320, -64
SpriteTexture, Size, Sort Layervault-eye; 64, 48; Fixtures
Shader MaterialShader, Valuesscanlines.wgsl; none
vault-demonNodePosition1400, 0
SpriteTexture, Size, Anchor, Sort Layerart/demon-idle.png; 160, 144; 0.5, 1; Props
Animated SpriteAnimations, Clip, Frame Sizeart/demon-idle.anim, idle, ticked

A doorway onto a far side

A doorway standing on the corridor’s floor shows a far side, a stretch of the town far down the corridor, wound round its middle. Its view draws only while the doorway is on the screen.

  1. Make swirl.wgsl as you made scanlines.wgsl, with the shader below. It starts with chapter 12’s ripple.
  2. Open scenes/gallery-portal.ron. Select camera and set its Position to 96, -24, his framed point. Press Add component and choose CameraRigComponent. Next to Framings, press +, open the framing, set its Name to follow, choose watchman for Subject, and set Offset to 0, 24. Leave the rest as it comes.
  3. Select gallery-portal, add a Viewport and rename it far-eye, at 1700, -80. In its Viewport, choose New render target for Texture, and set Size to 32, 64, Zoom to 0.5 and Update to WhenVisible. Leave Clear Colour as it comes, clear, Subtree unticked and Layers empty.
  4. Select gallery-portal, add a Sprite, far-houses, at 1700, -16. Drag art/night-town-background-town.png onto Texture, and set Size to 512, 99, Anchor to 0.5, 1 and Sort Layer to Town.
  5. Add another Sprite, far-sky, at 1700, -80: Texture art/night-town-background-sky.png, Size 96, 224, Sort Layer Sky, and Anchor left at 0.5, 0.5.
  6. Select gallery-portal, press + Add node and choose Shader Material. Rename it doorway and set its Position to 480, -32: standing on the floor, x 464 to 496. In its Sprite, click Texture and choose far-eye, and set Size to 32, 64 and Sort Layer to Fixtures. In its Shader Material, choose swirl.wgsl, and leave turns unset.
  7. Press Ctrl+S.
wgsl swirl.wgsl
// The ripple, from ripple.wgsl, which the cistern's water and the beacon's hot
// air use too.
//! include ripple
// How many turns the picture is wound by, from its edge to its middle.
//! param turns: float = 1.5

// The doorway onto the far side. It draws the doorway's picture, the far
// side's view, wound round its middle: the nearer a pixel is to the middle,
// the further round the place it reads from is turned, and the winding turns
// on by itself as the time goes on. The ripple moves where it reads from a
// little more, so the doorway never settles.
fn shade(frag: Fragment) -> vec4<f32> {
    // Where this pixel is in the doorway, from -1 to 1 each way, from its
    // middle.
    let from_middle = region_uv(frag) * 2.0 - vec2<f32>(1.0, 1.0);
    // How near the middle it is: 1 at the middle, 0 from the edges' middles
    // out.
    let near_middle = clamp(1.0 - length(from_middle), 0.0, 1.0);
    // How far round to turn it, in radians: more the nearer the middle, and a
    // little more every second.
    let angle = near_middle * turns() * 6.2831853 + time();
    let turned = vec2<f32>(
        from_middle.x * cos(angle) - from_middle.y * sin(angle),
        from_middle.x * sin(angle) + from_middle.y * cos(angle),
    );
    // Back to a place in the picture, moved by the ripple: up to one pixel of
    // the picture itself.
    let here = frag.screen_uv * resolution();
    let place = frag.region.xy
        + (turned * 0.5 + vec2<f32>(0.5, 0.5)) * (frag.region.zw - frag.region.xy)
        + ripple(here, time()) * sheet_texel();
    return texel(region_clamp(frag, place)) * frag.color;
}
//! param turns: float = 1.5
How many whole turns the picture is wound at its middle. The shader reads it as turns().
frag.region
The sprite’s own rectangle in its texture, from the corner xy to the corner zw, each from 0 to 1. A place worked out from region_uv goes back into the texture through it.
region_clamp(frag, place)
Holds a place inside the sprite’s own rectangle, half a pixel clear of its edge, so a place turned past the edge reads the edge and nothing beyond it.
sheet_texel()
One pixel of the sprite’s own texture, as a share of it: the ripple, from -1 to 1, moves where the swirl reads by up to one pixel of the far side’s picture. Chapter 12’s texel_size() is a pixel of the screen.
The editor with gallery-portal.ron open and far-eye selected and framed at 473.3%: a row of grey houses with lit windows and a street lamp, a dark night sky above them, and a tall thin frame round the middle house, reaching up into the sky over it, with a cross at its centre; the Hierarchy lists room, watchman, camera, doorway, far-eye, far-houses and far-sky; the Inspector shows Position x 1700.0 and y -80.0 and the ViewportComponent: Texture far-eye, Size 32.0 and 64.0 px, Zoom 0.50, Update WhenVisible, Subtree unticked, Layers 0 items, the Clear Colour a checker of clear, Drawn unticked
The far side, and the frame of what far-eye sees: 64 by 128.

far-eye sees 64 by 128 round its place, x 1668 to 1732 and y -144 to -16: the town’s houses and the sky over them, standing far down the corridor. The camera follows him, so walk east to the doorway.

The game in the corridor: the watchman on the moss-capped floor, and just east of him the doorway, a tall dark pane standing on the floor, its picture wound into a spiral of grey-green rings
The doorway, beside him.
The doorway close up beside him: the spiral in it turns steadily round its middle, the rings near the middle wound tighter than those at its edges
The doorway, close up. Watch the far side turn round its middle, wound tighter at the middle than at the edges.

The swirl winds the picture round its middle: turns, 1.5 whole turns, at the middle, fewer further out, and none from the middles of its edges outward. On top of that the whole picture turns on by a radian every second, and the ripple moves where it reads from by up to a pixel. The far side itself stands still: it is the shader that turns.

At his start the doorway is far off the screen, and its view does not draw: no pass, and no texture made for it. Beside it, the view draws, one pass a frame, into a texture of 16,384 bytes, 32 by 64 pixels at 8 bytes each. Walk 400 units on and it stops drawing; the texture is kept.

Node, under gallery-portalComponentFieldValue
projectshaderswirl.wgsl beside project.rengy
cameraNodePosition96, -24
Camera RigFramingsfollow: Subject watchman, Offset 0, 24
far-eyeNodePosition1700, -80
ViewportTexture, Size, ZoomNew render target; 32, 64; 0.5
Update, Clear ColourWhenVisible; as it comes, clear
far-housesNodePosition1700, -16
SpriteTexture, Size, Anchor, Sort Layerart/night-town-background-town.png; 512, 99; 0.5, 1; Town
far-skyNodePosition1700, -80
SpriteTexture, Size, Sort Layerart/night-town-background-sky.png; 96, 224; Sky
doorwayNodePosition480, -32
SpriteTexture, Size, Sort Layerfar-eye; 32, 64; Fixtures
Shader MaterialShader, Valuesswirl.wgsl; none

A magnifying glass

A magnifying glass he holds up at his hand: a view that sees a little round itself, and shows it twice as big, in the same place.

  1. Open scenes/gallery-magnifier.ron. Give camera the doorway’s rig: Position 96, -24, and a CameraRigComponent with one framing, follow, Subject watchman, Offset 0, 24.
  2. Select gallery-magnifier, add a Sprite and rename it lever, at 132, -24. Drag art/lever.png onto Texture, and set Size to 20, 24, Anchor to 0.5, 1 and Sort Layer to Props: the starter’s lever, at the height of his hand, just east of where he starts.
  3. Select watchman, press + Add node and choose Viewport: it lands under him. Rename it glass and set its Position to 36, -40: at his hand.
  4. In its Viewport, choose New render target for Texture. Set Size to 32, 32 and Zoom to 2, and leave Update at Always and Subtree unticked. Add two Layers, Ground and Props. Set Clear Colour to 0.04, 0.05, 0.07, alpha 1, the map’s.
  5. Press Add component and choose SpriteComponent. Click Texture and choose glass, and set Size to 32, 32 and Sort Layer to Glass.
  6. Select glass, add a Sprite and rename it glass-rim, at 0, 0. Drag art/lens-bezel.png onto Texture, and set Size to 40, 40 and Sort Layer to Marks.
  7. Press Ctrl+S.
The editor with gallery-magnifier.ron open and glass selected and framed at 1893.2%: the brass ring of the bezel filling the Viewport, inside it the lever’s round brass knob and its stick, big, and a small square frame round the knob’s lower half and the stick’s top with a cross at its middle; the Hierarchy shows glass under watchman and glass-rim under glass, then camera and lever; the Inspector, narrowed with the search box, viewport, shows the ViewportComponent: Texture glass, Size 32.0 and 32.0 px, Zoom 2.00, Update Always, Subtree unticked, Layers 2 items, Ground and Props, a dark Clear Colour, Drawn unticked
The glass, and the frame of what it sees: 16 by 16.
The Viewport’s Zoom above 1
Closer than the picture: at 2 a pixel of the texture is half a unit, so the glass’s 32 pixels see 16 units, and its Sprite, 32 units across, shows them twice as big. Every view before this one was at 1 or under.

One node holds both the view and the Sprite that shows it, so what the glass sees and where it shows it are the same place, and both go where he carries them. It sees 16 by 16 units round itself, at his hand, 36 along and 40 up from his feet. Its Layers leave out Glass and Marks, where it and its rim draw, so it is not in its own picture: a view whose picture held its own texture would be a loop, and the game warns of it every frame. What it shows at his start is the lever’s knob and stick, twice as close as the picture draws them.

The watchman with the glass at his hand: it shows the lever’s knob and stick, big; he walks east and the glass goes with him, dark now; he turns and walks back west, the glass still on his east side, until it shows the lever again
Watch the glass at his hand: it leaves the lever as he walks east, stays on his east side when he turns back, and finds the lever again.

The glass does not flip with him. It is a child of his node, 36 along it, and turning him flips his Sprite and not his node, so it stays east of him whichever way he faces. It is Always: a pass of its own in every frame, a small one, 32 pixels square.

Node, under gallery-magnifier unless it saysComponentFieldValue
cameraNodePosition96, -24
Camera RigFramingsfollow: Subject watchman, Offset 0, 24
leverNodePosition132, -24
SpriteTexture, Size, Anchor, Sort Layerart/lever.png; 20, 24; 0.5, 1; Props
glass, under watchmanNodePosition36, -40
ViewportTexture, Size, ZoomNew render target; 32, 32; 2
Update, LayersAlways; Ground, Props
Clear Colour0.04, 0.05, 0.07, 1
SpriteTexture, Size, Sort Layerglass; 32, 32; Glass
glass-rim, under glassNodePosition0, 0
SpriteTexture, Size, Sort Layerart/lens-bezel.png; 40, 40; Marks

A title screen

A title screen: a camera that does not move, a dusk sky of three planes, and a Label in front. It has no corridor and no watchman.

  1. Choose File › New scene. Add a Node, rename it gallery-title, and choose File › Save as…: scenes/gallery-title.ron.
  2. Select gallery-title, add a Camera and rename it camera, at 0, -90. Set Scaling to Letterbox, Extent to 180 and Width to 320, and Background to 0.4, 0.1, 0.04, alpha 1: a dusk’s orange. It sees x -160 to 160 and y -180 to 0.
  3. Make three planes under gallery-title, each with + Add node → Parallax, as chapter 6’s were, from the table below. Every picture is in art, named night-town-background- and the layer. Each plane is at 0, 0, its Sprite’s Anchor at 0.5, 0.5, and a field the table does not name stays as it comes.
NodePictureSizeSort LayerFactorOther fields
dusk-sky…-sky.png96, 224Sky1, 1Repeat Y ticked; Color 1, 0.62, 0.5, 0.55
dusk-clouds…-clouds.png288, 224Clouds0.95, 1Autoscroll -8, 4; Repeat Y ticked; Color 1, 0.78, 0.66, 0.9
dusk-mountains…-mountains.png96, 224Mountains0.9, 1Offset 0, 40; Color 0.55, 0.4, 0.5, 1
  1. Select gallery-title, press + Add node and choose Canvas. Rename it title, and leave it as it comes: Reference Size 1280, 720, ScaleWithHeight.
  2. Select title, press + Add node and choose Label. Rename it title-label. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.3, Offset Min to -450, -80 and Offset Max to 450, 80: a box 900 by 160 round a point 30 percent of the way down the screen. Leave Pivot at 0.5, 0.5.
  3. In its Text, set Text to The Lookout, Scale to 6, Color to 1, 0.92, 0.8, alpha 1, and Align to Center.
  4. Press Ctrl+S.
The Label’s Text, Scale and Align
What it says; how big, 2 as it comes; and where across its box it sits, Center here, Left as it comes. Its Color is the colour its letters are drawn in.
The title screen: a dusky orange sky, dark grey-brown clouds across its upper half, The Lookout in pale cream letters a third of the way down, and a range of dark blue-black mountains along the bottom
The title screen.

A plane’s Color multiplies its picture, alpha and all, so it tints each plane, and it cannot add a hue the picture lacks: Night Town’s art has almost no red, so a warm Color on the sky makes it a muddy olive, not orange. The orange is what lies behind a see-through sky: the camera’s Background, showing through the sky at an alpha of 0.55, with the clouds and the mountains in front of it as dark shapes.

The title screen: the dark clouds drift slowly to the left and a little down across the orange sky, behind The Lookout, while the mountains stay put
Watch the clouds drift left and a little down, 8 units a second across and 4 down, behind the title.

With Repeat Y ticked the clouds never run out: after 20 seconds they have gone -160, 80, and the copies of the picture, repeated along and up, still cover the whole view. With Repeat Y unticked there is one row of them, and the drift carries it down out of the sky. A minute in, as made and on a copy with Repeat Y unticked:

The title screen a minute in, as made: dark clouds across the orange sky above the mountains, under and beside The Lookout
Repeat Y ticked: still clouds, the copies above drifted in.
The title screen a minute in, on the copy: a bare orange sky over the mountains, The Lookout in it, and no clouds
Repeat Y unticked: the one row has drifted down out of the picture.
Node, under gallery-title unless it saysComponentFieldValue
cameraNodePosition0, -90
CameraScaling, Extent, WidthLetterbox; 180; 320
Background0.4, 0.1, 0.04, 1
dusk-skySpriteTexture, Size, Sort Layerart/night-town-background-sky.png; 96, 224; Sky
ParallaxFactor, Repeat Y, Color1, 1; ticked; 1, 0.62, 0.5, 0.55
dusk-cloudsSpriteTexture, Size, Sort Layerart/night-town-background-clouds.png; 288, 224; Clouds
ParallaxFactor, Autoscroll0.95, 1; -8, 4
Repeat Y, Colorticked; 1, 0.78, 0.66, 0.9
dusk-mountainsSpriteTexture, Size, Sort Layerart/night-town-background-mountains.png; 96, 224; Mountains
ParallaxFactor, Offset, Color0.9, 1; 0, 40; 0.55, 0.4, 0.5, 1
titleCanvasas it comes
title-label, under titleRectAnchor Min, Anchor Max0.5, 0.3; 0.5, 0.3
Offset Min, Offset Max, Pivot-450, -80; 450, 80; 0.5, 0.5
TextText, Scale, Color, AlignThe Lookout; 6; 1, 0.92, 0.8, 1; Center

A spyglass

Hold Down and the camera glides out to a far tower and draws it close; let go and it glides back to him. A second framing, on the tower, that a script switches on while the key is held.

  1. Open scenes/gallery-spyglass.ron. Select gallery-spyglass, add a Node and rename it far-tower, at 1500, -80: the middle of its beacon, about 1400 units east of him.
  2. Select far-tower, add a Sprite, far-pillar, at 0, 80: the floor. Drag art/gothic-horror-tiles.png onto Texture, click Atlas Region, type pillar and press Enter. Set Size to 32, 48, Anchor to 0.5, 1 and Sort Layer to Fixtures.
  3. Select far-pillar, add a Sprite, far-beacon, at 0, -48: the same picture, Atlas Region beacon, Size 32, 64, Anchor 0.5, 1, Sort Layer Fixtures.
  4. Select gallery-spyglass, add a Sprite and rename it sky, at 1000, -8. Drag art/night-town-background-sky.png onto Texture, set Draw Mode to Tiled, Tile Size to 96, 224, Size to 2000, 224 and Sort Layer to Sky, and leave Anchor at 0.5, 0.5: x 0 to 2000 and y -120 to 104, the length of the corridor, its top just above the highest the camera looks and its foot behind the floor.
  5. Select camera and set its Position to 96, -24, his framed point. Press Add component and choose PixelPerfectComponent: Assets PPU 1, Reference Width 320, Reference Height 180, then tick Upscale Render Texture and Crop Frame, as in chapter 3.
  6. Press Add component and choose CameraRigComponent. Add a framing, look: Subject watchman, Offset 0, 24, the rest as it comes.
  7. Add a second framing, spyglass: Subject far-tower. Untick its Active, and set Priority to 10, Zoom to 3 and Duration to 0.6. Leave Ease at EaseInOut and the rest as it comes.
  8. Make spyglass.rgs with the script below. Select camera, press Add component and choose Spyglass. Press Ctrl+S.
The camera’s Inspector narrowed with the search box, camerarig, to its CameraRigComponent: Enabled ticked; Framings, 2 items, 1 look closed and 2 spyglass open: Name spyglass, Subject far-tower, Active unticked, Priority 10, Zoom 3, Rotation 0°, Duration 0.60 s, Ease EaseInOut, Dead Zone 0.0 and 0.0, Soft Zone 0.0 and 0.0, Offset 0.0 and 0.0, Damping 0.25 and 0.5, Time 0.00 s, Smoothing 0.20 s, Limit 0.00 u; and the Cut to live framing button
The second framing, spyglass, off until the script switches it on.
rgs spyglass.rgs
/// The spyglass. The camera's rig has a framing called `spyglass` that looks at
/// the far tower and is off. While Down is held this switches it on, and the
/// camera glides over and draws the tower close; let go and it switches it off,
/// and the camera glides back to him.
component Spyglass {
    fn loop() {
        // `down` is the Down arrow or S. Held: the spyglass framing is on. Let go:
        // it is off.
        set_flag("CameraRigComponent", "framings.spyglass.active", held("down"));
    }
}
held("down")
True on every step the action down is held: the Down arrow or S. Not Up: Up is also his jump.

While Down is held the spyglass is on, and with a Priority of 10 it outranks look: the camera blends to the tower over the spyglass’s 0.6 seconds and settles at a zoom of 12, Pixel Perfect’s 4 times the framing’s 3, on a view 106.7 by 60 with the beacon’s 32 across in it. Let go and it glides back to him in look’s 0.5 seconds: a blend takes the Duration of the framing it arrives at. He stands still all the while.

The watchman standing at the west end of the corridor under a dark night sky; the floor slides away to the left and down as the camera speeds east, the sky’s dithered streaks of cloud racing past, then the far tower’s lit beacon comes in and settles, big, filling the picture’s height; it holds, then the clouds race the other way and the floor and the watchman come back
Hold Down: watch the camera leave him slowly, speed across the sky, and settle on the tower’s beacon. Let go, and it glides back to him in half a second.

The sky is there for the glide’s sake. In the middle of the glide the view holds neither the floor nor the tower, only the corridor’s air, and with nothing drawn in it the air is the camera’s Background: a flat night blue, which does not show how fast the camera goes. Its clouds do. Its top, y -120, is just above the highest the camera looks, y -114 when it is on him, so none of the air it shows is the Background.

The Camera tab in Play, Down held: Camera camera; At rest, As drawn and Node at 1500.0, -80.0; Rotation 0.0°; View 106.7 × 60 units; Zoom 12.00 px a unit (Pixel Perfect 4 × Zoom 3.00); camera_view() left 1446.7, top -110.0, right 1553.3, bottom -50.0; Room: Held by No room, Clamping No, Blend None; Rig: Shot spyglass, Blend None
The Camera tab with Down held: the spyglass live, at a zoom of 12.

EaseInOut starts slowly and ends slowly. At a quarter of the 0.6 seconds the camera has come 0.156 of the way, not a quarter, as it would on Linear; at three quarters, 0.844. The glide back is eased the same way.

Node, under gallery-spyglass unless it saysComponentFieldValue
projectscriptspyglass.rgs beside project.rengy
far-towerNodePosition1500, -80
far-pillar, under far-towerNodePosition0, 80
SpriteTexture, Atlas Regionart/gothic-horror-tiles.png, pillar
Size, Anchor, Sort Layer32, 48; 0.5, 1; Fixtures
far-beacon, under far-pillarNodePosition0, -48
SpriteTexture, Atlas Regionart/gothic-horror-tiles.png, beacon
Size, Anchor, Sort Layer32, 64; 0.5, 1; Fixtures
skyNodePosition1000, -8
SpriteTexture, Draw Mode, Tile Sizeart/night-town-background-sky.png; Tiled; 96, 224
Size, Sort Layer2000, 224; Sky
cameraNodePosition96, -24
Pixel PerfectAssets PPU, Reference Width, Reference Height1; 320; 180
Upscale Render Texture, Crop Frameticked; ticked
Camera Rigframing lookSubject watchman, Offset 0, 24
framing spyglassSubject far-tower, Active unticked, Priority 10, Zoom 3, Duration 0.6, Ease EaseInOut
Spyglassthe script

An auto-scroller

A camera that scrolls by itself, at one pace, with him carried along at its edge. Its rig is off, and a script moves it.

  1. Make scroll.rgs and held.rgs with the two scripts below.
  2. Open scenes/gallery-autoscroller.ron. Select camera and set its Position to 160, -60. Press Add component and choose CameraRigComponent, and untick its Enabled. Add a framing, follow: Subject watchman, Offset 0, 24, the rest as it comes.
  3. Press Add component and choose Scroll. Select watchman, press Add component and choose Held, and leave its Margin at 16.
  4. Press Ctrl+S.
rgs scroll.rgs
/// The auto-scroller's camera. Its rig is off, so nothing follows him: this
/// sends the camera down the corridor by itself, at a steady 48 units a second
/// for half a minute, and then switches the rig on, which finds him wherever the
/// scroll has left him.
component Scroll {
    fn on_create() {
        // The first step: start the glide to the far end of the corridor, at one
        // pace all the way.
        anneal_to(vec2(1600.0, -60.0), 30.0, .Linear);
        // Wait the glide out.
        await wait(30.0);
        // Then the rig, off until now, takes the camera back to him.
        set_flag("CameraRigComponent", "enabled", true);
    }
}
rgs held.rgs
/// Keeps him inside what the camera shows. A camera that scrolls on its own
/// does not wait for him, so this puts him back inside the view, by a margin,
/// whenever the edge has caught up with him.
component Held {
    /// How far inside the edge of the view he is kept.
    #[suffix("u")]
    margin: f32 = 16.0,

    fn loop() {
        // What the camera shows now: (left, top, right, bottom).
        let view = camera_view();
        let here = position();
        // The nearest place to him that is inside the margin.
        let inside = vec2(
            clamp(here.x, view.x + margin, view.z - margin),
            clamp(here.y, view.y + margin, view.w - margin),
        );
        // Move him only when he is outside it, so that he walks as he does
        // when the view is wide of him.
        if inside.x != here.x || inside.y != here.y {
            move_to(inside);
        }
    }
}
on_create()
Runs once, as the scene starts. It may wait, as this one does: the rest of it runs 30 seconds later.
anneal_to(point, seconds, ease)
Moves this node to point over that many seconds, in the ease’s shape: .Linear at one pace. The camera goes 1440 units in 30 seconds, 48 a second, to the corridor’s far end.
position() and move_to(point)
Where this node is, and putting it somewhere else. Held moves him only when the view’s edge has caught up with him, so between times he walks as he always does.
clamp(value, low, high)
The value, held between the two: here, his place held inside the view less the margin, on both axes.
The corridor with the watchman standing on its floor; the view glides steadily east, the floor’s stones going by to the left, until its left edge reaches him, and then carries him along at the edge
Watch the floor’s stones go by as the view scrolls east, and him, standing still, reached by its left edge and carried along.

He stands still, and the view’s edge carries him along the corridor, 399 units in ten seconds, 16 inside its left edge. At the end, 30 seconds on, the camera is at 1600, -60, and the script switches the rig on.

The rig being off is not what lets the script scroll the camera. Scripts run after the rig in a step, so the script’s write to the camera wins whether the rig is on or off: on a copy with the rig on, the camera scrolls all the same, less than 2 units off the line, pulled a little toward him. What Enabled off does is keep the camera exactly on the line, and make the hand-back one flag.

Two pictures side by side. Left: the watchman at the view’s left edge on the corridor’s floor, the view’s upper part empty night blue. Right: the watchman in the middle of the view, the floor higher in it, with the stone under it showing
Steps 1802 and 1803, 30.03 and 30.05 s: the scroll’s end, and a step later, the camera on him.

The hand-back is a cut. The rig keeps its springs running while it is off, so on the step it is on again it puts the camera at his framed point, 144 units from where the scroll left it, and does not glide.

Node, under gallery-autoscrollerComponentFieldValue
projectscriptsscroll.rgs, held.rgs beside project.rengy
watchmanHeldMargin16
cameraNodePosition160, -60
Camera RigEnabledunticked
Framingsfollow: Subject watchman, Offset 0, 24
Scrollthe script

Cinema bars

Bars above and below the picture, as a film has: a Letterbox wider than the window, its bars made of the camera’s own Background.

  1. Open scenes/gallery-cinema.ron. Select camera, set its Position to 160, -60, its Extent to 134, and its Background to 0.10, 0.02, 0.03, alpha 1: a dark red. Its Width stays 320.
  2. Select gallery-cinema, add a Sprite and rename it sky, at 160, -60. Drag art/night-town-background-sky.png onto Texture, set Draw Mode to Tiled, Tile Size to 96, 224, Size to 384, 224 and Sort Layer to Sky, and leave Anchor at 0.5, 0.5: x -32 to 352, y -172 to 52, the whole picture and more.
  3. Press Ctrl+S.
The game at 1280 by 720: a dark red bar across the top and another across the bottom, and between them the night sky’s dark teal over the corridor’s floor, the watchman standing on it
At 1280 by 720: 92 rows of bar above, and 92 below.

A Letterbox of 320 by 134 fills what is not its picture with its Background. At 1280 by 720 the picture is 1280 by 536 at 4 pixels a unit, with 92 rows of bar above and below it; at 1920 by 1080, 1920 by 804 at 6; at a phone’s 2400 by 1080, 2400 by 1005 at 7.5, bars 37.5 rows deep. A script sees the same 320 by 134 round 160, -60 in each.

The game at a phone’s 2400 by 1080, shown smaller: the same sky, floor and watchman across the whole width, and thin dark red bars along the top and the bottom
At 2400 by 1080: the same 320 by 134, bars 37.5 rows deep.

The sky is there for the bars’ sake. The corridor’s air has nothing drawn in it, so it shows the Background too: without sky, the top bar runs into the air and cannot be told from the picture. With it, nothing between the bars is the Background, and each bar ends at the picture’s edge. On a copy with sky hidden:

The game on the copy: a single field of dark red from the top of the picture down to the corridor’s floor, the watchman standing on the floor, and dark red again under it; no edge where the top bar ends
Without the sky: the bars and the air are one dark red.
The bars
What a Letterbox draws where the window is not its picture: the camera’s Background, above and below when the picture’s shape is wider than the window’s, at the sides when it is narrower.
Node, under gallery-cinemaComponentFieldValue
cameraNodePosition160, -60
CameraScaling, Width, ExtentLetterbox; 320; 134
Background0.10, 0.02, 0.03, 1
skyNodePosition160, -60
SpriteTexture, Draw Mode, Tile Sizeart/night-town-background-sky.png; Tiled; 96, 224
Size, Sort Layer384, 224; Sky

A phone

A game for a phone held upright, and two ways to frame it: a tall Letterbox, and a camera fixed to its width. E swaps them.

  1. Make swap.rgs with the script below.
  2. Open scenes/gallery-phone.ron. Rename camera to tall and set its Position to 96, -96: the corridor’s 192 of height in the middle of its 320. Set its Width to 180 and Extent to 320, and leave Active ticked.
  3. Press Add component and choose Swap, and tick its On.
  4. Right-click tall in the Hierarchy and choose Copy; right-click gallery-phone and choose Paste as child. Rename the copy wide. Set its Scaling to FixedHorizontal and Extent to 180, untick its Camera’s Active, and untick its Swap’s On. Its Width stays the copied 180, which FixedHorizontal does not read.
  5. Select gallery-phone, add a Sprite and rename it sky, at 96, -96, the cameras’ place. Drag art/night-town-background-sky.png onto Texture, set Draw Mode to Tiled, Tile Size to 96, 224, Size to 192, 640 and Sort Layer to Sky, and leave Anchor at 0.5, 0.5: x 0 to 192 and y -416 to 224, more than either camera shows on a phone. Its tiles meet at y -192, just under the ceiling’s stone, and at 32, inside the floor’s, so no seam shows.
  6. Press Ctrl+S.
rgs swap.rgs
/// One of the phone's two cameras. Each carries this script, and E swaps them:
/// the camera that was the game's lets go of it, and the other takes it.
component Swap {
    /// Whether this camera is the one the game is seen through. The tall camera
    /// starts on and the wide one off.
    on: bool = false,

    fn loop() {
        // E or Enter is the action key.
        if pressed("action") {
            on = !on;
            if on {
                // This camera becomes the game's, and the other goes off.
                make_current();
            }
        }
    }
}
pressed("action")
True on the one step the action is pressed: E or Enter. Both cameras’ scripts see it on the same step, so one E swaps them: the one that was on turns off, and the other calls make_current(), chapter 9’s, which makes it the game’s and switches every other camera off.

FixedHorizontal’s width is its Extent, not its Width, as chapter 2 found. On a phone the shape of 720 by 1280 the two show the same: tall its 180 by 320 at 4 pixels a unit, filling the window, and wide 180 across and as tall as the window makes it, 320.

Two tall pictures side by side, the same: the sky’s plain teal above the corridor’s ceiling stone near the top, under the stone the sky darker, with dithered streaks of cloud, and the watchman standing on the moss-capped floor near the bottom
At 720 by 1280, shown at half size: through tall at step 30, and through wide at step 60, E pressed at step 40.

On a longer phone they part. At 1080 by 2400, tall still shows 180 by 320, at 6 pixels a unit: a picture 1080 by 1920, with 240 rows of bar above and below, the camera’s Background. wide shows 180 across and 400 tall, drawn to the whole window: no bar, and more of the room above and below.

Two tall pictures side by side. Left, through tall: a night-blue bar across the top and another across the bottom, and between them the sky’s teal over the ceiling stone, the clouded sky in the corridor and the watchman on the floor. Right, through wide: the same room from the top edge to the bottom one, more sky over the ceiling, the floor’s stone and a strip of dark sky under it, and no bar
At 1080 by 2400, shown at half size: through tall at step 30, its bars the Background, and through wide at step 60, the sky to its edges.

The sky is there for the bars’ sake, as the cinema’s is. Without it the air above the ceiling and in the corridor is the Background too, the same night blue as the bars, and the bars run into it. It reaches further than either camera shows, so through wide it goes on under the floor’s stone, to the foot of the window.

Leave wide’s Active ticked and the scene has two active cameras: the game looks through the first, tall, and Problems says so on wide, as in chapter 9. Try it, then untick it again.

The Problems tab with one warning on wide: "tall" already frames this room and only the first active camera is used — this node’s zoom, roll and position are never read, so switch one of the two off
Both left Active: one row, on the camera that loses.
Node, under gallery-phoneComponentFieldValue
projectscriptswap.rgs beside project.rengy
tallNodePosition96, -96
CameraActive, Scalingticked; Letterbox
Width, Extent180; 320
SwapOnticked
wideNodePosition96, -96
CameraActive, Scalingunticked; FixedHorizontal
Width, Extent180, not read; 180
SwapOnunticked
skyNodePosition96, -96
SpriteTexture, Draw Mode, Tile Sizeart/night-town-background-sky.png; Tiled; 96, 224
Size, Sort Layer192, 640; Sky

Who moves the camera

A different owner in each case. The monitor’s, the title’s and the cinema’s cameras are moved by nothing, and neither are the views: a view’s camera is its own node, and vault-eye and far-eye stand where they were put. The glass is a child of his node, so he carries it. The doorway’s and the magnifier’s cameras are the rig’s, following him. The spyglass’s is the rig’s too, and a script picks which of its framings is on. The auto-scroller’s is a script’s while its rig is off, and the rig’s after; with the rig on as well, the script’s write would still win, coming after the rig’s in each step. The phone has two cameras, and make_current() picks which one the game is seen through.

Check it works