Building a Pacman Game With Bacon.js

    Emre Guneyler
    Share

    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:

    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:

    For more control, you can roll your own event stream using Bacon.fromBinder(). We will show this in our game by creating a moveStream variable, that produces events for our Pacman moves.

    var moveStream = Bacon.fromBinder(function(sink) {
       game.onPacmanMove = function(moveV) {
          sink(moveV);
       };
    });

    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:

    var spawnStream = Bacon.sequentially(800, [
       PacmanGame.GhostColors.ORANGE,
       PacmanGame.GhostColors.BLUE,
       PacmanGame.GhostColors.GREEN,
       PacmanGame.GhostColors.PURPLE,
       PacmanGame.GhostColors.WHITE,
    ]).delay(2500);

    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:

    For more methods on event streams see the official documentation page. The difference between throttle and debounce can be seen with marble diagrams:

    // `source` is an event stream.
    //
    var throttled = source.throttle(2);
    
    // source:    asdf----asdf----
    // throttled: --s--f----s--f--
    
    var debounced = source.debounce(2);
    
    // source:             asdf----asdf----
    // source.debounce(2): -----f-------f--

    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:

    moveStream.onValue(function(moveV) {
       game.movePacman(moveV);
    });
    
    spawnStream.onValue(function(ghost) {
       game.spawnGhost(ghost);
    });

    Despite you can use stream.subscribe(), to subscribe to a stream, you can also use stream.onValue(). The difference is that subscribe will emit both three types of events we have seen before, while onValue will only emit events that are of type Bacon.Next. That is it will omit the Bacon.Error and Bacon.End events.

    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:

    Let’s see an example of Bacon.combineTemplate:

    var password, username, firstname, lastname; // <- event streams
    var loginInfo = Bacon.combineTemplate({
       magicNumber: 3,
       userid: username,
       passwd: password,
       name: { first: firstname, last: lastname }
    });

    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:

    var ghostStream = Bacon.interval(1000, 0);
    
    ghostStream.subscribe(function() {
       game.updateGhosts();
    });
    
    var combinedTickStream = new Bacon.Bus();
    
    combinedTickStream.plug(moveStream);
    combinedTickStream.plug(ghostStream);
    
    combinedTickStream.subscribe(function() {
       game.tick();
    });

    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:

    var source = Bacon.sequentially(1000, [1, 2, 3, 4]);
    
    var reactiveValue = source.scan(0, function(a, b) {
       return a + b;
    });
    
    // 0 + 1 = 1
    // 1 + 2 = 3
    // 3 + 3 = 6
    // 6 + 4 = 10

    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.

    Here are some more useful links: