The Station Manager game with Realtime Tube data
I’ve been learning a lot recently about using realtime data streams and how and why one might use them in an app. In order to better understand the differences between realtime streaming data and REST APIs (which I’ve had more experience with), I decided to build a game, the mechanic of which uses realtime train arrival data. As trains arrive into the station in real life, effects are triggered in the game which the user has to manage.
Make it your own!
All of the code for the game is on Glitch. This means that you can see the code, ‘remix’ it and make it your own. There is a thorough readme file in the repo and I’ll also go over some of the methods used in this blog post.
Getting Arrivals Data
Ably has a Hub of realtime data streams for developers to try out and build apps with. I used the London Tube Schedule stream, which provides a stream of various data from TFL; including arrivals at a given station. For the tamagotchi game, I wanted to find out the arrivals at a busy station, so that I would have lots of trains arriving in close succession. I chose King’s Cross station for this reason. The data stream uses the station NAPTAN code rather than it’s name to get the correct data, so I had to look up the correct code for King’s Cross (you can look up station codes here), which is 940GZZLUKSX
.
The stream I’ll therefore be using is:
[product:ably-tfl/tube]tube:940GZZLUKSX:arrivals
Every time the data from TFL is updated, this channel will publish a message with the new arrival times of trains into Kings Cross. The joy of the data being a realtime stream means that I don’t have to poll for the data, as I would with a REST API, instead, I establish a connection once and data is published to the channel as and when updates happen.
Connecting to a Data Stream
In order to connect to the data stream I used the Ably Javascript SDK. To do so, I need an Ably API key which comes with a free account. To keep my API key safe, I also used Token Authentication which makes a Token Request on the server side which is handed to the Ably realtime client to authenticate. There is a brilliant walkthrough of how to implement Token Authentication here:
TFL Data
The data published by the stream looks a little like this ↓
It is a large array of train objects each with a lot of information. For the sake of this game, the only information I’m really interested in is the TimeToStation
value. I can use these numbers to calculate when to cause a train to arrive into the station in the game.
I could have created all kinds of interesting extensions to the game, with multiple platforms and colour coded trains for their lines, even maybe an arrivals board with train destinations, but let’s not get too carried away…
Game mechanics
Congratulations! You’re the newest TamagoTrain Station Controller! Now you’ve got to keep your station in fine fettle.
Don’t let your station get too hot, don’t let your platform fill up with passengers, litter or mice!
Trains raise the temperature of your station, as do passengers
If it gets too hot, passengers will start to faint!
Unconscious passengers can’t leave the platform
Passengers sometimes drop litter
Too much litter attracts mice!
Trash and mice all take up space on the platform making it difficult for your passengers to exit
If your platform gets too full, too hot or too cold your station will have to shut and your game will end
How to play
Clean the platform to clear away litter
Vent cold air through the station to keep everyone cool (but don’t get carried away!)
Passengers departing through the exit will cool the platforms down a little
Departing trains also cool the platform slightly
You can charm mice with songs! They’ll find their way off the platform if musically enticed
Music will also wake up fainted passengers
Game Code
The game is an expressJS app. It is split into two parts — the simulation/game loop, which runs in ‘ticks’ and the ui/render loop which runs at 30 frames per second. This separation prevents us from tying the game logic to the frame rate, which will reduce the chance of the frame rate dropping if the game logic gets complicated. (If you’re interested, this is a great intro to game loops.)
Game.js
The Game.js file is the main control loop for the game - in it, we define a JavaScript class called Game
. When games are created, we create a new instance of this class to hold the game state. This is also where we set up the tick()
function, which is called once per second. This ‘tick’ steps the simulation forward by iterating the game loop. It ‘ticks’ the game entities (the platform, passengers and trains), applies any problems (adding litter and mice) and applies any buffs (cleaning, venting or music).
The only input the user can supply is applying a Buff
— either clean
, vent
or music
, which are triggered by the buttons in the UI. When pressed, these buttons add a Buff
object to an array in the Game
instance, that we use as a queue of actions. Buffs can only be added to the queue a maximum of 3 times, after that clicking the buttons in the UI will just return until the Buff
has been taken off the queue.
The Game
instance is responsible for three core things
Handling train arrival/departure messages, and routing them to the platform
Creating instances of
Buffs
Checking for game over
All the rest of the game logic happens in the tick()
functions found on the Entities
, Problems
and Buffs
.
GameUI.js
The GameUi.js file is where the rendering of the game happens. It uses an observer pattern to keep track of the game state.
30 times a second the GameUI.draw()
function is called and passed a snapshot of the game state. GameUI
instance keeps track of the last state it was called with so that it can avoid re-drawing things that have not changed.
The GameUi class has a collection called _renderingFunctions
— a list of functions it calls in order, each being passed the current game state. If any rendering function returns a value of -1, we use this as a signal to stop drawing to the screen and to display the** Game Over** screen. The rendering code places absolutely positioned divs on the page which are styled in the css. The divs contain animated gifs of the entities, buffs and problems. The appearance of the divs is changed by adding css classes and data-attributes dependant on the problems or buffs that have been applied in the game state.
Entities, Buffs and Problems
By default, when an instance of Game
is created, a Platform
entity is created. This platform has some basic state (an age measured in ticks
, a width
, a height
) along with the three core stats the game is ranked on - hygiene
, temperature
and capacity
. The game is won or lost based on the state of these variables, which the game evaluates each tick. As the game ticks, the Game
instance will process any objects in its queue first in first out, creating an instance of the requested Buff
and applying it to the Platform
.
When the Platform
ticks, the following things happen -
Any unprocessed messages are read, FIFO.
If a message for a train arrival or departure is found a train is created on the platform or removed from it.
All
tickables
aretick
ed.Any completed contents or buffs are removed — an item is deemed complete if a property
completed
is present, and set to true on the object.
The tickables
that the platform stores are:
Any present train
All of the contents of the platform
All of the buffs applied to the platform
On each tick, the item that is being ticked
gets handed the current instance of the platform, and based on the logic in that item’s class, it can mutate the properties of the platform. For instance - every tick, a Mouse
could reduce the hygiene
property of the platform.
The rest of the entities, Buffs and Problems are all JavaScript classes that can mutate the state of the Platform
instance in their tick
method.
Both
Entities
andProblems
havex
andy
coordinates that are used to move them around the user interface.Problems
all inherit from aBase Class
calledProblem
which creates these properties by default for them.
A problem looks like this:
Entities and problems hold state which will cause effects during the lifetime of a game. For example:
Travellers walk towards the exit by moving 10 pixels closer to the exit each tick
Travellers have a chance of dropping litter
Litter has a chance of adding mice to the platform
Trains add an extra Traveller to the platform every tick
All of this logic exists in the tick
function of each kind of entity or problem.
Starting the Game
The game uses webpack to bundle the client side JavaScript. The script.js file is the webpack entry point, webpack bundles together all of the JavaScript files before they are sent to the browser. This is great because it means we only need to reference script.js to start the game.
The script.js file is referenced in the index.html file and it takes care of starting new games. It contains a startGame()
function which does all the work:
This function:
Creates a
game
instanceCreates an instance of the
GameUI
class, passing it a reference to the newgame
instanceCalls
game.start()
passing a configuration object of two actions - one to execute on start, one on end.the onGameStart action listens for events on the dataSource
the onGameEnd action disconnects the dataSource to stop the game from using up messages that we don't need.
The
ui.startRendering()
function is called which will set up the render loopFinally the game is returned so that the UI buttons will work in the browser.
Game Over
Game failure states are managed in the Game.js file, in the function isGameOver()
. This contains a collection of objects with functions for different failure conditions. At the start of each tick, each of those functions are run and if any of them return true
then the game is over.
Have fun!
I hope you’ve enjoyed playing the game and will enjoy making your own versions, or even adding some extensions to mine. If you’ve any questions about the game or about realtime data streaming you can drop me a message in the comments or get me @thisisjofrank on twitter. I’d also love to see any remixes you make!
Top comments (2)
Great post! I learned a lot from this one. indianapolis paving
This is amazing :)