Game characters and efficiency in RxJS

Một phần của tài liệu Manning functional reactive programming (Trang 180 - 186)

Now we’ll look at some of these efficiency considerations in an RxJS example that has the same basic structure as the zombie game. See figure 7.8: you’re the groundskeeper for a golf course in Austria run by your

uncle, a mad professor. To control the mole problem, he has issued you a large wooden mallet. You’re sure this contra- venes every animal welfare code since Charlemagne, but, having been caught evading import tax on Moroccan lanterns, you need to prove you’re capable of hon- est work, or you’ll lose your inheritance.

Your “inheritance” is a golf course full of

moles, but the penny hasn’t dropped yet. Figure 7.8 Video game “Whack That Mole!”

The first thing we’ll look at is the mole itself, in listing 7.9. It’s the same basic idea as the zombie and human characters in the previous example, except you don’t need a view of the world because moles are blind. Here are the inputs:

■ Unique ID for the mole.

■ Constant x-y position.

■ Animation clock. You’re treating this as both a stream and a cell. This isn’t in the spirit of FRP, but it allows you to represent the animation clock in a single variable, which is arguably an advantage.

■ sClick stream to indicate that the user clicked the mole.

And here are the outputs:

■ id—From the input

■ drawable—Function that draws the mole character

■ sDestroy—Stream allowing the mole to request its own destruction

■ dispose—Described shortly

The mole has three states: rising, up, and whacked. The user can only whack the mole in the rising state. If it rises all the way without being whacked, its state changes to up, meaning it won; it will then laugh at you for 10 seconds. This is the first time we’ve done a game in JavaScript, but the FRP concepts used here are all the same.

You return a dispose function for this reason: the Rx.BehaviorSubject/ subscribe idiom in RxJS is like any use of subscribe, in returning a subscription object. When the logic is used in flatMapLatest(), which is RxJS’s switch (as you’ll do shortly), there will be a memory leak if you don’t clean this up explicitly.

This is true at the time of writing, but the desirable situation is that this should be automatic, as it is in Sodium. If you used scan() instead of the Rx.BehaviorSubject/ subscribe idiom, this wouldn’t be an issue. You could do it this way, but there are two advantages of using Rx.BehaviorSubject:

■ You can rely on the cell-like semantics of the state value. The observable returned by scan() doesn’t give you a current value on subscription. This might be important if you use withLatestFrom() on it later.

■ You’re combining sUp and sWhack, which originate from two different inputs.

We think the Rx.BehaviorSubject/subscribe idiom expresses things a bit more naturally than scan() in this sort of case.

function mkMole(id, x, y, clock, sClick) {

var tRise = 100, tWhack = 15, tUp = 500;

function drawMole(ctx, x, y, up, fracVisible) { Listing 7.9 The mole’s logic

Time to rise from hole Time to descend

when whacked Time to hang around

in the up state

159 Game characters and efficiency in RxJS

...

}

var state = new Rx.BehaviorSubject({ phase : 'rising',

t0 : clock.getValue() }), sUp = clock.withLatestFrom(state, function (t, state) {

return state.phase == 'rising' &&

t - state.t0 >= tRise ? { phase : 'up', t0 : t } : null;

})

.filter(function (state) { return state !== null; }), sWhack = sClick.withLatestFrom(clock, state, function (_, t, state) {

var dt = t - state.t0;

return state.phase == 'rising' ? { phase : 'whacked',

t0 : t - (1 - dt / tRise) * tWhack } : null;

})

.filter(function (state) { return state !== null; }), subscr1 = sUp.merge(sWhack).subscribe(state), drawable = clock.withLatestFrom(state, function (t, state) { return state.phase == 'rising' ? function (ctx) {

var dt = t - state.t0;

drawMole(ctx, x, y, false, dt / tRise); } : state.phase == 'up' ? function (ctx) {

drawMole(ctx, x, y, true, 1); } : function (ctx) {

var dt = t - state.t0;

if (dt < tWhack)

drawMole(ctx, x, y, false, 1 - dt / tWhack); };

}),

sDestroy = clock .withLatestFrom(state,

function (t, st) { var dur = t - st.t0;

return (st.phase == 'up' && dur >= tUp)

|| (st.phase == 'whacked' && dur >= tWhack) ? id : null;

})

.filter(function (id) { return id != null; });

return { id : id,

drawable : drawable, sDestroy : sDestroy,

dispose : function () { subscr1.dispose(); } };

}

Listing 7.10 gives a main program with everything written without caring too much about performance. As with the zombies, you extract a single sDestroy and drawable from the game state’s current list of moles using RxJS’s switch operation: flat- MapLatest() is equivalent to a map, hold, and then switch in Sodium.

Graphics code omitted Changes to the up

state after tRise

Changes to the whacked state if clicked while rising

Merges the two state transitions into state Function to draw the mole, updated every clock tick

Request to be terminated when the whacked or up state expires

Note that when you destroy a mole, as mentioned earlier, you return a function to dispose it explicitly to avoid memory leaks. You call it using a setTimeout with a delay of 0 so you can guarantee it will be done safely after the current RxJS processing has completed.

As noted, the “restrictive” style of FRP that requires referential transparency does put a lot of work onto the garbage collector. The performance scalability of this code as presented is abysmal:

■ The mole outputs a new drawable every frame and all of them are combined each frame.

■ The combining of drawables has an algorithmic complexity of O(N) for each mole update. Because each mole updates each frame, that’s O(N2) per frame.

■ Appending arrays by copying is inefficient if the arrays are long.

Next you’ll see ways to improve these without going outside the FRP paradigm.

function init() {

var canvas = document.getElementById("myCanvas"), getXY = function(e) {

return { x : e.pageX - canvas.offsetLeft, y : e.pageY - canvas.offsetTop }; },

sMouseDown = Rx.Observable.fromEvent(canvas, 'mousedown') .map(getXY),

clock = new Rx.BehaviorSubject(0);

Rx.Observable.interval(20).subscribe(clock);

var state = new Rx.BehaviorSubject({ nextID : 0, moles : []}), sAddMole = clock

.filter(function (_) { return Math.random() < 0.02; }) .withLatestFrom(state, clock,

function (_, state, t0) { var x = 25+(canvas.width-50) * Math.random();

var y = 25+(canvas.height-50) * Math.random();

var sClick = sMouseDown.filter(function (pt) { return pt.x >= x - 20 && pt.x <= x + 20 &&

pt.y >= y - 20 && pt.y <= y + 30;

});

var newMoles = state.moles.slice();

newMoles.push(mkMole(state.nextID, x, y, clock, sClick));

state = { nextID : state.nextID+1, moles : newMoles };

console.log("add mole "+state.nextID+

" ("+state.moles.length+")");

return state;

}),

sDestroy = state.flatMapLatest(

function (state) {

var sDestroy = Rx.Observable.of();

for (var i = 0; i < state.moles.length; i++)

sDestroy = sDestroy.merge(state.moles[i].sDestroy);

Listing 7.10 Main program for Whack That Mole!

50 fps animation clock

At random times …

… creates a new mole Clicks in the mole’s area are passed to the mole.

Copies the list for referential transparency

Merges all moles’

sDestroys into one

161 Game characters and efficiency in RxJS

return sDestroy;

});

sRemoveMole = sDestroy.withLatestFrom(state, function (id, state) {

var newMoles = [];

for (var i = 0; i < state.moles.length; i++) if (state.moles[i].id != id) newMoles.push(state.moles[i]);

else

setTimeout(state.moles[i].dispose, 0);

console.log("remove mole "+id+" ("+newMoles.length+")");

return { nextID : state.nextID, moles : newMoles };

});

sAddMole.merge(sRemoveMole).subscribe(state);

var drawables = new Rx.BehaviorSubject([]);

state.flatMapLatest(

function (state) {

var drawables = new Rx.BehaviorSubject([]);

for (var i = 0; i < state.moles.length; i++) { var thiz = state.moles[i].drawable.map(

function(draw) { return [draw];

});

drawables = i == 0 ? thiz

: drawables.combineLatest(thiz,

function (d1, d2) { return d1.concat(d2); });

}

return drawables;

}).subscribe(drawables);

clock.subscribe(function(t) { var ctx = canvas.getContext("2d");

ctx.fillStyle = '#00af00';

ctx.fillRect(0, 0, canvas.width, canvas.height);

var ds = drawables.getValue();

for (var i = 0; i < ds.length; i++) ds[i](ctx);

});

}

To run this code, check it out if you haven’t done so already, and point your browser at sodium/book/web/whack1.html. The code is in whack1.js.

Let’s improve the performance. First, instead of updating the draw function every frame, you’ll turn it into a function of time and update it only when the mole changes state. This change is trivial. Replace this section

var drawable = clock.withLatestFrom(state, function (t, state) { ...

with the following so the updating of drawables happens only on changes to the mole’s state, not for every clock frame. t is now an argument of the returned function:

var drawable = state.map(function (state) {

return state.phase == 'rising' ? function (ctx, t) { Turns sDestroy into

a state change

Copies non-destroyed moles

Explicitly disposes of deleted moles

Merges state changes together

into state Combines drawables into

a single BehaviorSubject

Redraws the screen every frame

var dt = t - state.t0;

drawMole(ctx, x, y, false, dt / tRise); } : state.phase == 'up' ? function (ctx, _) { drawMole(ctx, x, y, true, 1); } : function (ctx, t) {

var dt = t - state.t0;

if (dt < tWhack)

drawMole(ctx, x, y, false, 1 - dt / tWhack); };

});

Next, modify the main loop like this to pass the current time:

var ds = drawables.getValue();

for (var i = 0; i < ds.length; i++) ds[i](ctx, t);

The second improvement is to construct drawables in a binary tree structure instead of a flat structure, as described in section 7.6:

var drawables = new Rx.BehaviorSubject([]);

state.flatMapLatest(

function (state) { var drawables = [];

for (var i = 0; i < state.moles.length; i++) drawables.push(state.moles[i].drawable);

return sequence(drawables);

}).subscribe(drawables);

It uses a generalized sequence() function like the Java one you used earlier, but tuned up a bit. It converts a list of cells of values (the Java type would be List<Cell<A>>) to a cell of a list of values: Cell<List<A>>.

You split the list and work recursively so the number of combineLatest operations between any input cell and the cell you output is no more than log(N). Thus the algo- rithmic complexity of a single update is O(log N):

function sequence(xs) {

if (xs.length == 0)

return new Rx.BehaviorSubject([]);

else

if (xs.length == 1)

return xs[0].map(function(x) { return [x]; });

else {

var mid = Math.floor(xs.length/2), left = xs.slice(0, mid), right = xs.slice(mid);

return sequence(left).combineLatest(sequence(right), function (x1, x2) { return x1.concat(x2); });

} }

Third, appending arrays by copying them is costly if the arrays are long. Recall that to ensure referential transparency, the values have to be immutable, so you must copy

163 Switch use case #3: removing invalid states

the arrays. It turns out you can get the best of both worlds: there’s a data structure called a 2-3 finger tree that is immutable, that behaves like an array, and for which appending has a complexity of O(log(min(n1,n2)). There are JavaScript implementa- tions of it.

We’ve done the first two modifications. To try the new version, point your browser at sodium/book/web/whack2.html. The code is in whack2.js.

Một phần của tài liệu Manning functional reactive programming (Trang 180 - 186)

Tải bản đầy đủ (PDF)

(362 trang)