Traluno▶ Back to the layout

Riding in the Cab

A camera at the front of a moving train turned out to be the most-used feature in the game, and generalising it to everything you can sit in was four lines of code and a rename.

The front-of-train camera started as a small thing: click a train, get a window showing what the driver sees. Model railway operators do this with real cameras, and it is the one view that makes a layout feel like a railway rather than a diorama.

It became the feature people spend the most time in, and it forced two decisions that were more interesting than the feature itself.

It is one renderer, not two

The obvious implementation is a second renderer drawing a second view into a second canvas.

Do not do this. A second renderer fails to acquire a context on some machines — older integrated graphics, browsers with a low context limit, anything already running another accelerated canvas. And when it fails inside a React effect, the exception escapes the effect and unmounts the entire component tree. The failure mode is not "no cab view". It is a blank page.

So the cab view is a scissored inset in the same renderer: set a viewport rectangle, set a scissor rectangle, render the scene again from the cab camera into that corner. One context, no extra allocation, and if it fails there is nothing to fail independently of the main view.

The consequence is that the cab window in the DOM is not a picture. It is a frame around a hole. The middle section has to stay fully transparent, because anything drawn there — a background colour, a border-radius fill, a loading state — covers the inset the renderer is painting underneath.

Recording has to happen inside the frame

Recording the cab view means copying the inset out of the drawing buffer.

That copy only works inside the same animation frame as the render. Read it later — in a timeout, in a promise continuation, in the next frame — and you get an empty buffer, because the browser is entitled to clear the drawing buffer after compositing and generally does.

So the recorder is a callback invoked at the end of the render function, in the same tick, before anything else can run. That is an unpleasant coupling and there is no way around it. It is written down in the code next to the copy, because it is exactly the kind of thing a later refactor moves somewhere tidier and breaks.

Anything you can sit in is a ride

The generalisation is the part worth stealing.

The cab camera needs a thing that moves and a place to put the eye. A train has both. So does a road vehicle. So does a pedestrian. So does a carousel horse, and a ferris wheel gondola.

There is now one flat list of ridable things, with trains first so that an index which used to mean "train number three" still means that. Everything else appends after them.

A part of an animated model becomes ridable by declaring a name and a camera — an eye position and a look-at point — in its own local frame. The engine puts both through that part's world matrix every frame.

That last detail is the whole trick. The carousel horse bobs up and down and rotates around the centre; the gondola counter-rotates so it stays level while the wheel turns. Neither of those behaviours had to be described to the camera system. They are already in the part's matrix, because that is what makes the horse bob, and running the eye through the same matrix means the camera inherits every bit of it for free.

Put the eye clear of the structure

One practical note that cost a rebuild.

The ferris wheel gondola's camera was first placed level with the seat, which is where a passenger's eye is. The view was entirely girder.

The reason is that the gondola hangs from the rim ring, and the rim ring is at the same radius as the gondola. An eye at seat height looks straight along the ring, so the ring fills the frame from edge to edge and you see nothing else — which is, to be fair, exactly what a passenger with their eyes closed sees.

The fix is to move the eye inward and up until the structure is behind you. The general rule: the camera position that is physically correct is often the one with the worst view, because real vehicles have interiors and these do not. Place the eye where the view is, not where the passenger would be.

What it changed about the game

The cab view is the reason the loading gauge exists, the reason track joints are checked at rail level, and the reason the tunnel bores got detailed interiors.

None of that was planned. It follows from giving the player a camera at rail height: every shortcut taken on the assumption that nobody would ever look at the layout from down there stopped being a shortcut. A feature that changes where the player can stand changes the standard everything else is built to.

  • camera
  • engine
  • rides

◀ All posts