Building a Pacman Game With Bacon.js

JavaScript embraces asynchronous programming. This can be a blessing and a curse that leads to the concept of “callback hell”. There are utility libraries that deal with organizing asynchronous code such as Async.js, but it’s still hard to follow the control flow and reason about asynchronous code effectively.

In this article, I’ll introduce you to the concept of reactive programming that helps dealing with the asynchronous nature of JavaScript, by using a library called Bacon.js.

Let’s Get Reactive

Reactive Programming is about asynchronous data streams. It replaces the Iterator Pattern with the Observable Pattern. This is different from imperative programming, where you actively iterate over data to handle stuff. In reactive programming, you subscribe to the data and react to events asynchronously.

Bart De Smet explains this shift in this talk. André Staltz covers Reactive Programming in depth in this article.

Once you become reactive, everything becomes an asynchronous data stream: database on the server, mouse events, promises, and server requests. This lets you avoid what’s known as “the callback hell”, and gives you better error handling. Another powerful feature of this approach is the ability to compose streams together, that gives you great control and flexibility. Jafar Husain explains these concepts in this talk.

Bacon.js is a reactive programming library and it’s an alternative to RxJS. In the next sections we’ll use Bacon.js to build a version of the well-known game “Pacman”.

Setup Project

To install Bacon.js, you can use Bower by running on the CLI the command:

$ bower install bacon

Once the library is installed, you are ready to get reactive.

PacmanGame API and UnicodeTiles.js

For the look and feel, I’ll use a text-based system, so that I don’t have to deal with assets and sprites. To avoid creating one myself, I’ll employ an awesome library called UnicodeTiles.js.

To start, I’ve built a class called PacmanGame, which handles the game logic. The followings are the methods it provides:

PacmanGame(parent): Creates a Pacman game object

start(): Starts the game

tick(): Updates the game logic, renders the game

spawnGhost(color): Spawns a new ghost

updateGhosts(): Updates every ghost in the game

movePacman(p1V): Moves the Pacman in the specified direction

In addition, it exposes the following callback:

onPacmanMove(moveV): Called if present, when user requests Pacman to move by pressing a key

So to use this API, we are going to start the game, call spawnGhost periodically to spawn ghosts, listen for the onPacmanMove callback, and whenever that happens, call movePacman to actually move Pacman. We also call updateGhosts periodically to update the ghost movements. Finally, we call tick periodically to update the changes. And importantly, we will use Bacon.js to help us with handling events.

Before we start, let’s create our game object:

var game = new PacmanGame(parentDiv);

We create a new PacmanGame passing a parent DOM object parentDiv where the game will be rendered into. Now we are ready to build our game.

EventStreams or Observables

An event stream is an observable, to which you can subscribe to observe events asynchronously. There are three types of events that you can observe for with these three methods:

observable.onEnd(f): Listen for an event that a stream has ended, and no move value will be available.

Creating Streams

Now that we’ve seen the basic usage of event streams, let’s see how to create one. Bacon.js provides several methods you can use to create an event stream from a jQuery event, an Ajax promise, a DOM EventTarget, a simple callback, or even an array.

Another useful concept about event streams is the notion of time. That is, events can come some time in the future. For example these methods create event streams that deliver events at some time interval:

We can call sink with a value that will send an event, and which the observers can listen for. The call to sink is within our onPacmanMove callback – that is whenever user presses a key to request a Pacman move. So we created an observable that emits events about Pacman move requests.

Note that we called sink with a plain value moveV. This will push move events with the value moveV. We can also push events like Bacon.Error, or Bacon.End.

Let’s create another event stream. This time we want to emit events that notify to spawn a ghost. We will create a spawnStream variable for that:

Bacon.sequentially() creates a stream that delivers the values with given interval. In our case, it’ll deliver a ghost color every 800 milliseconds. We also have a call to a delay() method. It delays the stream so the events will start to emit after a 2.5 second delay.

Methods on Event Streams and Marble Diagrams

In this section I’ll list a few more useful methods that can be used on event streams:

As you can see, throttle is throttling the events as usual, whereas debounce is emitting events only after the given “quiet period”.

These utility methods are simple yet very powerful, being able to conceptualize and control the streams thus the data within. I recommend watching this talk on how Netflix makes use of these simple methods to create an autocomplete box.

Observing an Event Stream

So far, we have created and manipulated the event stream, now we will observe the events, by subscribing to the stream.

Recall the moveStream and spawnStream we have created before. Now let’s subscribe to both of them:

When an event arrives on spawnStream (that happens every 800 ms), its value will be one of ghost colors, and we use the color to spawn a ghost. When an event arrives on moveStream, recall that this happens when a user presses a key to move Pacman. We call game.movePacman with the direction moveV: that comes with the event, so the Pacman moves.

Combining Event Streams and Bacon.Bus

You can combine event streams to create other streams. There are many ways to combine event streams, here are a few of them:

As you can see, we combine event streams – namely, password, username, firstname and lastname – into a combined event stream named loginInfo using a template. Whenever an event stream gets an event, loginInfo stream will emit an event, combining all the other templates into a single template object.

There is also another Bacon.js way of combining streams, that is Bacon.Bus(). Bacon.Bus() is an event stream that allows you to push values into the stream. It also allows plugging other streams into the Bus. We will use it to build our final part of the game:

Now we create another stream – the ghostStream, using Bacon.interval. This stream will emit 0 every 1 second. This time we subscribe to it and call game.updateGhosts to move the ghosts. This is to move the ghosts every 1 second. Notice the commented out game.tick, and remember the other game.tick from our moveStream? Both streams update the game, and finally call game.tick to render the changes, so instead of calling game.tick in each stream, we can produce a third stream – a combination of these two streams – and call game.tick within the combined stream.

To combine the streams, we can make use of Bacon.Bus. That’s the final event stream in our game, which we call combinedTickStream. Then we plug both moveStream and ghostStream into it, and finally subscribe to it and call game.tick within it.

And that’s it, we are done. The only thing left to do is to start the game with game.start();.

Bacon.Property and More Examples

Bacon.Property, is a reactive property. Think of a reactive property that is the sum of an array. When we add an element to the array, the reactive property will react and update itself. To use the Bacon.Property, you can either subscribe to it, and listen for changes, or use the property.assign(obj, method) method, which calls the method of the given object whenever the property changes. Here is an example of how you would make use of a Bacon.Property:

First, we create an event stream that produces the values of a given array – 1, 2, 3, and 4 – with a 1 second interval, then we create a reactive property that is the result of a scan. This will assign the 1, 3, 6, and 10 values for the reactiveValue.

Find Out More and Live Demo

In this article, we’ve introduced reactive programming with Bacon.js by building a Pacman game. It simplified our game design, and gave us more control and flexibility with the concept of event streams. The full source code is available at GitHub, and a live demo is available here.

Great post, thanks! I was wondering one thing. How would you implement the logic if you wanted the ghosts to start arriving or moving faster as the game evolves? In that case you couldn't make the Bacon interval streams static with 1000 and 800 millisecond intervals?

Ghosts have v properties that determine their speed. You could tweak that for dynamic speed, however, if you set it to 2, ghosts would start jumping by two squares. Another alternative is you set the Bacon interval to lowest value like 200 ms, and skip moving ghosts at some ticks. In any case you don't have to dynamically update the Bacon interval, because it's just a clock. Think about it like a cpu clock.