Making a Game with Rx.js and Web Speech at Active Dublin 2016 - Part I
Last week… or was it two weeks ago?… wo! Time flies!… Two weeks ago I was at Active Solution spring conference at Dublin. The topic of the conference was JavaScript (yippiii!) and we organized a hackathon for all employees to experiment with their JavaScript skills and build something cool over the weekend using music, sound or speech APIs.
It was a super fun experience and every team had something to show at the final reckoning in the demo sessions. My personal favorite (and the winner) was a virtual fly that sprang to life when you connected several devices together and started buzzing as it randomly flied through a crowded room to everyone’s delight (or almost everyone XD).
I, myself, worked as a coach during the Hackathon so I couldn’t fully participate with any team. I did sneak some hours though and attempted to build something cool with Rx.js and Web Speech.
This is the story of… Say What Maddafackaaaaaaa!! - The Game… Behold!!
That Light Bulb Moment
On the flight to Dublin I had some hours to brainstorm about what I could build during the hackathon. I had been experimenting a lot with Rx.js lately so I wanted to build something with it, just for experimenting and having fun. Of course, being a hackathon about music and sound it had to have some speech or audio web API in it.
I had also just finished reading Sergi’s Reactive Programming with Rx.js book, which is great by the by, and I think that it planted the seed for a game in my subconscious so that when I started thinking of options before the hackathon it sprung. I was going to build a game. But what kind of game?
Out of nowhere I remembered those typing games where words come from the top of the screen and slowly travel down. The gist is to type each word before it gets to the bottom or you die and disappear into oblivion. If you type the word right the word gets eliminated, you get some points and can continue fighting for your survival. As the game progresses words appear more often and they scroll down faster and faster and faster until they overwhelm you and bring your demise.
Yes! I was going to build something like that, but using speech instead of a keyboard. The idea was to start shouting words that started using a given letter, and then perhaps add more levels of complexity combining letters, phrases, speed, etc.
Awesome, I finally had an idea for the hackathon… rock on!
Take a Look at This Introduction to Rx.js
If you haven’t worked with Rx.js before then perhaps you may want to take a look at this introduction that explains some of the basics before you continue reading. If you like living on the edge, proceed my adventurous friend.
Building the Game Step by Step - Part I
The whole code for this article in this jsFiddle. Beware that this is weekend-conference-booze-hackathon level code. Expect duplication and inconsistencies :P
So! Given the super tight time schedule, my first priority was to get something onto the screen as soon as possible. I chose to use jsFiddle to write the game so it would be easy to demonstrate and share at the end of the hackathon.
I decided to start by having a black background and some letters falling down the screen at regular intervals. In order to achieve that I needed to create a teeny tiny game engine of sorts and undust my limited game dev knowledge (from this, this and that).
Let’s take a look at a simplified version of game development and an exemplar game engine for if you haven’t traveled these waters before.
And A Teeny Tiny Intro to Game Development
A game, in programming terms, consists of an infinite loop with the following steps:
- gather user input,
- update the state of the game based on that input
- draw everything
This is commonly known as… brace yourself… (ominous voice) the game loop.
In addition to implementing a game loop, you need a way to represent the different entities that are going to live within your game, like the player itself, backgrounds, enemies, scores, etc. You do that by using an abstraction usually called GameObject
to represent these entities. A GameObject
generally has an update
and draw
methods. The update
method updates the state of the game object based on the player input, previous interactions or other game logic and the draw
method paints it onto the screen.
Additionally, you often divide your game in a collection of Scenes. Each Scene let’s you group and manage collections of game objects that are displayed and interactive to the player at any given point in time.
The game loop for a simple game where you only have a scene could look some thing like:
-> every game loop interval
----- gather user input
----- FOR every game object in this Scene DO
--------- update game object
--------- draw game object
Doing a Game Loop in Rx.js
Now, let’s try to recreate this in Rx.js. An infinite loop, if you reflect about it, can be thought of as an infinite sequence of do-something events. Since we want this sequence to execute the game loop at certain intervals, we use Rx.Observable.interval
.
const INTERVAL_GAME = 16 // 60 fps => 16 ms
var gameLoop$ = Rx.Observable.interval(INTERVAL_GAME)
Now we want to setup my background in that infinite loop. So we create a GameObject
that represents a game object that can be drawn into the screen. This game object has a position in a 2-dimensional space and a speed when it moves:
class GameObject {
constructor(x, y, speed) {
this.x = x
this.y = y
this.speed = speed
}
update() {}
draw() {}
}
We then create a Background
object to represent the actual background of my game:
class Background extends GameObject {
constructor() {
super(0, 0, 0) // positioned at 0,0
}
draw() {
// to implement
}
}
In order to draw the Background
and the game itself we need to place a canvas
in my html markup:
<canvas id="game" width="500" height="500"></canvas>
Now that we have a canvas
we can use its primitives to paint a black rectangle when the background draw
method is called:
class Background extends GameObject {
constructor() {
super(0, 0, 0) // positioned at 0,0
}
draw() {
var canvas = document.getElementById('game')
var ctx = canvas.getContext('2d')
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, 500, 500)
}
}
Once we have a way to represent a background we can include it in our game by making it be a part of our game loop. In order to do that we create a background observable:
var background$ = Rx.Observable.of(new Background())
And combine it with our game loop:
var game$ = gameLoop$.combineLatest(background$, (_, b) => {
var gameObjects = [b]
return gameObjects
})
The combineLatest
operator combines multiple observables. It combines then in a way that every time one of these source observables emits a value, the resulting observable emits another value that contains all the latest values pushed by the original streams.
I understand, many words there, blablabla… It’s much easier to understand if we express it via a marble diagram:
gameLoop$ ---0---l---2---3---4---5---6--->
background$ b------------------------------>
combineLatest ---b0--b1--b2--b3--b4--b5--b6-->
So now that we have a game$
observable that contains our game objects being pushed through time every 16 milliseconds, we can subscribe to it to perform the actual updating and drawing:
game$.subscribe(gameObjects => {
gameObjects.forEach(g => {
g.update()
g.draw()
})
})
If we go back to our original definition for the game loop, we could see at a glance like this:
// every 16 ms
gameLoop$
// gather all my observables with game objects
.combineLatest(background$, (_, b) => {
return [b]
})
// and update and draw them
.subscribe(gameObjects => {
gameObjects.forEach(g => {
g.update()
g.draw()
})
})
After having included the background in our game loop and subscribed to it you should be able to see a black screen. Tadaaaa! :)
Adding Some Letters
The next step would be to add some letters falling from the top of the screen towards the bottom.
Just like with the background we need a way to represent a letter in our game universe. That is, we need to create a new Letter
game object:
const DEFAULT_SPEED = 1
class Letter extends GameObject {
constructor(letter, x, y, speed = DEFAULT_SPEED) {
super(x, y, speed)
this.letter = letter
}
update() {
this.y = this.y + this.speed
}
draw() {
//console.log('drawing letter');
var canvas = document.getElementById('game')
var ctx = canvas.getContext('2d')
ctx.fillStyle = '#fff'
ctx.fillText(this.letter, this.x, this.y)
}
}
And now we have to decide how we are going to introduce these letters into the game. Let’s think… We want a letter to fall down the screen every so often, so that sounds like some sort of interval. Let’s say that we want new letters every 2 seconds. We can represent through a new letters$
observable:
const INTERVAL_WORD = 2000
var letters$ = Rx.Observable.interval(INTERVAL_WORD)
But we need a way to transform these intervals into actual letters:
var letters$ = Rx.Observable.interval(INTERVAL_WORD).map(
_ => new Letter(getRandomLetter(), getRandomPosition(0, 500), 0)
)
The getRandomLetter
and getRandomPosition
are just helper method to get an arbitrary letter and place it in the screen at an arbitrary position:
function getRandomLetter() {
var letter = String.fromCharCode(97 + Math.floor(Math.random() * 26))
return letter
}
function getRandomPosition(min, max) {
return Math.floor(Math.random() * max - min)
}
We also we want to keep these letters in our game, accumulate them until the player says a word or the letters just fall off the screen.
In order to do that we are going to use the scan
operator which works like Array.prototype.reduce
and applies reduce on the existing items within a stream:
var letters$ = Rx.Observable.interval(INTERVAL_WORD)
.map(_ => new Letter(getRandomLetter(), getRandomPosition(0, 500), 0))
.scan((letters, newLetter) => {
letters.push(newLetter)
return letters
}, [])
If we represent it through a marble diagram it would look like this:
interval --------0--------1--------2-------->
map --------l--------l--------l-------->
scan --------[l]------[l,l]----[l,l,l]-->
Where we accumulate our letters inside an array every 2 seconds. It just remains to add them to our game by including them inside our game$
observable:
var game$ = gameLoop$.combineLatest(background$, letters$, (_, b, l) => {
var gameObjects = [b, ...l]
return gameObjects
})
And voilà! You should have letters appearing in your screen every 2 seconds and moving downwards until they leave the screen.
Concluding Part I
And again this grew up to be a much longer article that I had expected, so we’ll divide it into several articles.
In this first installment you learned how to implement the rudiments of a game engine in Rx.js with a basic game loop and a way to represent game objects, update their state and draw them in the screen. You also learnt about the combineLatest
and scan
operators along the way. We now have the beginnings of a game where we display a background and letters that travel downwards our screen.
Up next we will continue by gathering some user input using the Web Speech API that will affect our game and earn us some rewards.
At any rate, you can find the whole source code in this jsFiddle so feel free to experiment yourself if you’re curious.
Have a great weekend! Now I must run to work! :)
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.