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 objectstart()
: Starts the gametick()
: Updates the game logic, renders the gamespawnGhost(color)
: Spawns a new ghostupdateGhosts()
: Updates every ghost in the gamemovePacman(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.onValue(f)
: Listen for value events, this is the simplest way to handle events.observable.onError(f)
: Listen for error events, useful for handling errors in the stream.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:
Bacon.interval(interval, value)
: Repeats thevalue
indefinitely with the given interval.Bacon.repeatedly(interval, values)
: Repeats thevalues
with given interval indefinitely.Bacon.later(delay, value)
: Producesvalue
after givendelay
.
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:
observable.map(f)
: Maps values and returns a new event stream.observable.filter(f)
: Filters values with the given predicate.observable.takeWhile(f)
: Takes while given predicate is true.observable.skip(n)
: Skips the firstn
elements from the stream.observable.throttle(delay)
: throttles the stream by somedelay
.observable.debounce(delay)
: Throttles the stream by somedelay
.observable.scan(seed, f)
Scans the stream with given seed value and accumulator function. This reduces the stream to a single value.
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:
Bacon.combineAsArray(streams)
: combines event streams so the result stream will have an array of values as it’s value.Bacon.zipAsArray(streams)
: zips the streams into a new stream. Events from each stream are combines pairwise.Bacon.combineTemplate(template)
: combines event streams using a template object.
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:
- Bacon.js API Reference
- A video introduction to Bacon.js
- RxJS Website
- Highland.js The high-level streams library
- Reactive Game Programming for the Discerning Hispter by Bodil Stokke
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.
Emre Guneyler is a CS student in Turkey. He is passionate about web and game development. Besides programming he enjoys chess, poker, and Call of Duty 4.