Learning RxJS can be a steep curve. Concepts get easily lost if you just keep reading the docs. It easier if you have an end goal in mind. My end goal was to create a classic arcade game using RxJS.

Game screen

The game can be played at tanmaydharmaraj.github.io/space-blaster. Explaining the entire code would be difficult hence I have picked up the core parts of the game. You can read all the code for the game here.

Let’s get into understanding the core – index.js.

If you’ve been reading about RxJS then the concept of streams should be fairly familiar to you. Let’s move straight to the last line of our code.

var game = Rx.Observable.combineLatest(ticker$, object$).sample(Rx.Observable.interval(TICKER_INTERVAL)).subscribe(update);

This is the entry point of our game. Here we combine 2 streams the ticker$ and the object$ and sample them at a constant time interval specified by the TICKER_INTERVAL variable. What this means is after “TICKER_INTERVAL” number of milliseconds call update() and pass it the latest values of ticker$ and object$

Maintaining state

Our spaceship shoots bullets to blast asteroids. So we need a way to maintain the latest positions of asteroids and the bullets fired by the spaceship. We also need the positions of the spaceship to detect if an asteroid collides with the spaceship. All of this state is maintained in the object variable. Below is a representation of what the object variable looks like below. Here we are representing the x and y positions of the spaceship (initially center of the canvas) which will then be updated when the spaceship moves. Asteroids, bullets, explosions (more on this later), and the score are all managed by this object.

const INITIAL_OBJECTS = {
    spaceshipx: (canvas.width / 2) - (SPACESHIP_WIDTH / 2),
    spaceshipy: canvas.height - SPACESHIP_HEIGHT,
    asteroids: [],
    bullets: [],
    explosions: [],
    score: 0
}

Updating state

Based on the latest key inputs, that is move left or right or fire we update their respective streams. These updated streams are merged below and a state representation for the game is created. This state representation will then be passed over to the update function.

const object$ = ticker$.withLatestFrom(spaceship$, asteroid$, input_shoot$).scan((object, [ticker, spaceship, asteroid_object, shoot]) => {

    //calculate spaceship position
    object.spaceshipx = spaceship - (SPACESHIP_WIDTH / 2);
    object.spaceshipy = canvas.height - SPACESHIP_HEIGHT;

    //add a bullet to the bullet array if we have shot.
    if (shoot == 1) {
        object.bullets.push({
            xposition: spaceship - ((SPACESHIP_WIDTH / 2) - 35),
            yposition: canvas.height - (SPACESHIP_HEIGHT + 50)
        })
    }

    let newBullets = object.bullets.filter((data) => (data.yposition > 0)).map((data) => {
        data.yposition = data.yposition - 50
        return data;
    });
    return {
        spaceshipx: object.spaceshipx,
        spaceshipy: object.spaceshipy,
        asteroids: asteroid_object.asteroids,
        bullets: newBullets,
        explosions: object.explosions,
        score: object.score
    };
}, INITIAL_OBJECTS)

The heart of the game – the update function

Now that we know what the state will look like at a given point of time. Let’s now have a look at the update function. I have added comments that explain what is happening here.

var update = function([ticker, object]) {
    //After every time interval we clear the canvas and redraw everything.
    context.clearRect(0, 0, canvas.width, canvas.height);

    //This is the helper function that draws the background.
    drawHelper.drawBackground();

    //The object contains the spaceships x and y coordinates, we draw them on the canvas.
    drawHelper.drawSpaceship(object.spaceshipx, object.spaceshipy);

    //checking bullet and asteroid collisions
    object.bullets.forEach((bullet, bullet_index) => {
        object.asteroids.forEach((asteroid, asteroid_index) => {
            //Here we check if the bullet has collided with the asteroid. If so we remove the asteroid and the bullet from the object and draw an explosion at that place)
            if (bullet.xposition > asteroid.x - ASTEROID_WIDTH / 2 && bullet.xposition < asteroid.x + ASTEROID_WIDTH / 2 && bullet.yposition > asteroid.y - ASTEROID_HEIGHT / 2 && bullet.yposition < asteroid.y + ASTEROID_HEIGHT / 2) { object.asteroids.splice(asteroid_index, 1); object.bullets.splice(bullet_index, 1); //We successfully destroyed an asteroid. Add 1 to the score. object.score++; //There might be multiple explosions taking place. So we add an object into the explosions array with the position of the explosion. Later this will be used to animate an explosion. object.explosions.push({ currentFrame: 1, object: drawHelper.drawExplosion(asteroid.x, asteroid.y) }) } }) }); //We draw the latest score on top of the screen. drawHelper.drawScore(object.score); //animate asteroid rotation. object.asteroids.forEach((asteroid) => {
        asteroid.update();
        asteroid.render();
    });

    //animate explosions.
    var newExplosions = object.explosions.filter((explosion) => explosion.currentFrame <= 16).forEach((explosion) => {
        explosion.currentFrame++;
        explosion.object.update();
        explosion.object.render();
    });
    object.explosion = newExplosions;

    //draw the bullet.
    object.bullets.forEach((bullet) => drawHelper.drawBullet(bullet.xposition, bullet.yposition));

    //checking spaceship and asteroid collisions. This has to be the last one always to draw game over notification over everything else
    object.asteroids.forEach((asteroid, asteroid_index) => {
        if (object.spaceshipx > asteroid.x - ASTEROID_WIDTH / 2 && object.spaceshipx < asteroid.x + ASTEROID_WIDTH / 2 && object.spaceshipy > asteroid.y - ASTEROID_HEIGHT / 2 && object.spaceshipy < asteroid.y + ASTEROID_HEIGHT / 2) {
            drawHelper.drawGameOver(object.score);
            game.unsubscribe();
        }
    })
}