I've read about FRP and was very excited. It looks great, so you can write more high-level code, and everything is more composable, and etc.
Then I've tried to rewrite my own little game with a few hundreds sloc from plain js to Bacon.
And I found that instead of writing high-level logic-only code, I actually beating with Bacon.js and its adherence to principles.
I run into some headache that mostly interfere clean code
.take(1)
Instead of getting value, I should create ugly constructions.
Sometimes they should be by logic. But implementing it in FRP is scary
Even creator of bacon.js have troubles with it.
As example here is the peace of code to demonstrate the problem:
Task is to not allow two players stay at same place
Implemented with bacon.js
http://jsbin.com/zopiyarugu/2/edit?js,console
function add(a) {return function(b){return a + b}}
function nEq(a) {return function(b){return a !== b}}
function eq(a) {return function(b){return a === b}}
function always(val) {return function(){return val}}
function id(a){return a}
var Player = function(players, movement, initPos){
var me = {};
me.position = movement
.flatMap(function(val){
return me.position
.take(1)
.map(add(val))
})
.flatMap(function(posFuture){
var otherPlayerPositions = players
.filter(nEq(me))
.map(function(player){return player.position.take(1)})
return Bacon
.combineAsArray(otherPlayerPositions)
.map(function(positions){
return !positions.some(eq(posFuture));
})
.filter(id)
.map(always(posFuture))
})
.log('player:' + initPos)
.toProperty(initPos);
return me;
}
var moveA = new Bacon.Bus();
var moveB = new Bacon.Bus();
var players = [];
players.push(new Player(players, moveA, 0));
players.push(new Player(players, moveB, 10));
moveA.push(4);
moveB.push(-4);
moveA.push(1);
moveB.push(-1);
moveB.push(-1);
moveB.push(-1);
moveA.push(1);
moveA.push(-1);
moveB.push(-1);
What I want to demonstrate is:
me.positions
have dependency on its ownProbably I miss something fundamental. Maybe my implementation is not so in FRP style?
Maybe this code looks ok, and it just unaccustomed with new coding style?
Or this well-known problems, and I should choose best of all evil? So troubles with FRP like described, or troubles with OOP.
I've had similar experiences when trying to write games with Bacon and RxJs. Things that have a self-dependency (like player's position) are tough to handle in a "pure FRP" way.
For example, in my early Worzone game I included a mutable targets object that can be queried for positions of players and monsters.
Another approach is to do as the Elm guys do: model the full game state as a single Property (or Signal as it's called in Elm) and calculate the next state based on that full state.
So far my conclusion is that FRP is not so well-suited for game programming, at least in a "pure" way. After all, mutable state might be the more composable approach for some things. In some game projects, like the Hello World Open car race, I've used mutable state, like the DOM for storing state and EventStreams for passing events around.
So, Bacon.js is not a silver bullet. I suggest you find out yourself, where to apply FRP and where not to!
I have a similar filling sometimes. For me the experience of programming with FRP is mostly solving puzzles. Some of them are easy, some not. And those that I find easy might be harder form my colleagues and vice versa. And I don't like this about FRP.
Don't get me wrong, I like solving puzzles, this is a lot fun! But I think programming at payed work should be more... boring. More predictable. And code should be as straightforward as possible, even primitive.
But of course global mutable state also not the way we should go. I think we should find a way to make FRP more boring :)
Also a remark on your code, I think this will be more FRP'ish (a not tested draft):
var otherPlayerPositions = players
.filter(nEq(me))
.map(function(player){return player.position});
otherPlayerPositions = Bacon.combineAsArray(otherPlayerPositions);
me.position = otherPlayerPositions
.sampledBy(movement, function(otherPositions, move) {
return {otherPositions: otherPositions, move: move};
})
.scan(initPos, function(myCurPosition, restArgs) {
var myNextPosition = myCurPosition + restArgs.move;
if (!restArgs.otherPositions.some(eq(myNextPosition))) {
return myNextPosition;
} else {
return myCurPosition;
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With