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.
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.
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
The stream I’ll therefore be using is:
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.
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…
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
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
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. 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
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.
Game instance is responsible for three core things
Handling train arrival/departure messages, and routing them to the platform
Creating instances of
Checking for game over
All the rest of the game logic happens in the
tick() functions found on the
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.
By default, when an instance of
Game is created, a
Platform entity is created. This platform has some basic state (an age measured in
height) along with the three core stats the game is ranked on -
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 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.
Any completed contents or buffs are removed — an item is deemed complete if a property
completedis present, and set to true on the object.
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.
Platform instance in their
ycoordinates that are used to move them around the user interface.
Problemsall inherit from a
Problemwhich 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.
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:
Creates an instance of the
GameUIclass, passing it a reference to the new
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.
ui.startRendering()function is called which will set up the render loop
Finally the game is returned so that the UI buttons will work in the browser.
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.
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!