2012-03-10

Introducing Bacon.js


In my previous posting I ranted a bit about the WTFs related to RxJs hot and cold Observables and didn't come up with a very good workaround.

This time I have one. Use Bacon.js. That would be my homemade reactive library for Javascript. It solves the hot/cold problem by replacing Observable with two distinct abstractions:
  • EventStream: an observable stream of events. Never emits the same event twice. Always emits the same event to all subscribers.
  • Property: like a stream with current value. Any new subscriber will immediately be called back with the current value if there is any.
I'll use my earlier game development examples again, converted into bacon.js, to show how the new library works. I assume some RxJs knowhow.
So if you have some events (say keyboard events) and you want to map them into some domain events (like game character movement), you can start with the source events and use mapfilter and merge.
The following functions allow you to create a keyUp or keyDown stream for a given keyCode.
var allKeyUps = $(document).asEventStream("keyup")
var allKeyDowns = $(document).asEventStream("keydown")

function always(value) { return function(_) { return value } }
function keyCodeIs(keyCode) { return function(event) { return event.keyCode == keyCode} }
function keyUps(keyCode) { return allKeyUps.filter(keyCodeIs(keyCode)) }
function keyDowns(keyCode) { return allKeyDowns.filter(keyCodeIs(keyCode)) }
When you want to accumulate some state (like player position), you can use toProperty or scan to get a stateful Property. These things have a meaningful "current value", unlike streams like "keyup events".
The following creates keyState property from keyups and keydowns of each arrow key, then combines them into a single "direction" property usin toProperty:
function concat(a1, a2) {
  return a1.concat(a2)
}
function keyState(keyCode, value) {
  return keyDowns(keyCode).map(always([value])).
    merge(keyUps(keyCode).map(always([]))).toProperty([])
}
var direction = keyState(38, new Vector2D(0, -1))
  .combine(keyState(40, new Vector2D(0, 1)), concat)
  .combine(keyState(37, new Vector2D(-1, 0)), concat)
  .combine(keyState(39, new Vector2D(1, 0)), concat)
  .map(head)
The direction property will start with the empty array []. When you press an arrow key, the value will change to an array containing the corresponding direction vector. The combine method of Property is used to combine multiple properties into one.
The Property.sample method can be used to sample the current value of a Property each 50 milliseconds. The result will be an EventStream, because these are now distincts events (no current value, see?). So if you sample the direction each 50 milliseconds, you'll get a "movements" stream:
var startPos = new Vector2D(50, 50)

function head(array) { return array[0] }
function id(x) { return x }

var movements = direction.sample(50).filter(id)
Now we can further accumulate the movements into the current position:
var position = movements
  .scan(startPos, function(pos, move) { return pos.add(move) })
And voila, we are able to control our game character with the keyboard.
For a live demo of the same, see my slideshow, especially the last slide.
Bacon.js has quite good test coverage, it's open-source, it has at least some level of documentation and I'm a nice guy, so please try it out and let me know how the Bacon suits you.
All help is also appreciated also, so please contribute.

1 comment:

  1. sample(50).filter(id) does not work but filter(id).sample(50) works fine

    ReplyDelete