Building a Pacman Game With Bacon.js

    Emre Guneyler
    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:

    Frequently Asked Questions (FAQs) about Building Pacman with Bacon.js

    How can I start building my own Pacman game using Bacon.js?

    To start building your own Pacman game using Bacon.js, you first need to have a basic understanding of JavaScript and functional reactive programming (FRP). Once you have that, you can start by setting up your development environment. You will need to install Node.js and npm (Node Package Manager) on your computer. After that, you can install Bacon.js using npm. Once you have everything set up, you can start coding your game. You can follow the tutorial on our website to get a step-by-step guide on how to build a Pacman game using Bacon.js.

    What is the role of Bacon.js in building a Pacman game?

    Bacon.js is a functional reactive programming (FRP) library for JavaScript. It allows you to handle asynchronous events, such as user input, in a more manageable and readable way. In the context of building a Pacman game, Bacon.js can be used to handle user input (like keyboard events), game logic (like Pacman and ghost movements), and rendering the game state to the screen.

    Can I customize the Pacman game built with Bacon.js?

    Absolutely! Once you have built your basic Pacman game using Bacon.js, you can customize it to your liking. You can change the game’s visuals, add new features, or even modify the game’s rules. The possibilities are endless, and the best part is that you can do all of this while still benefiting from the power and simplicity of Bacon.js and functional reactive programming.

    How can I debug my Pacman game built with Bacon.js?

    Debugging a Pacman game built with Bacon.js is similar to debugging any other JavaScript application. You can use the browser’s developer tools to inspect the code, set breakpoints, and step through the code. Additionally, Bacon.js provides a method called ‘onError’ that you can use to handle errors in your event streams.

    How can I optimize the performance of my Pacman game built with Bacon.js?

    There are several ways to optimize the performance of your Pacman game built with Bacon.js. One way is to minimize the number of DOM updates. You can do this by using Bacon.js’s ‘combineTemplate’ function to combine multiple streams into a single stream that updates the DOM. Another way is to use the ‘flatMap’ function to avoid creating unnecessary streams.

    Can I use Bacon.js to build other types of games?

    Yes, you can use Bacon.js to build any type of game that requires handling of asynchronous events. This includes not only classic arcade games like Pacman, but also more complex games like real-time strategy games or multiplayer online games.

    How can I add multiplayer functionality to my Pacman game built with Bacon.js?

    Adding multiplayer functionality to your Pacman game built with Bacon.js would require a server to handle the communication between the players. You could use Node.js and WebSockets for this. On the client side, you would use Bacon.js to handle the incoming and outgoing WebSocket messages.

    Can I deploy my Pacman game built with Bacon.js on a website?

    Yes, you can deploy your Pacman game built with Bacon.js on a website. You would need to bundle your JavaScript code using a tool like Webpack or Browserify, and then you can host the bundled code and the game’s assets (like images and sounds) on a web server.

    Can I use Bacon.js with other JavaScript libraries or frameworks?

    Yes, you can use Bacon.js with other JavaScript libraries or frameworks. Bacon.js is a standalone library, so it doesn’t have any dependencies on other libraries or frameworks. However, it can be used in combination with other libraries or frameworks to build more complex applications.

    Where can I learn more about functional reactive programming (FRP) and Bacon.js?

    There are many resources available online to learn about functional reactive programming (FRP) and Bacon.js. You can start with the official Bacon.js documentation, which provides a comprehensive guide to the library’s features and API. There are also many tutorials, blog posts, and online courses available that cover FRP and Bacon.js in more detail.