Eboc started as a quick weekend project back in 2012. I was considering using Raphael.js for a project and figured building a game would be a good way to test the framework. I’ve refactored it since then using es6 promises to handle the asynchronous view.
The game is a color matching, falling block game divided into a model and view. The model contains the column structures and adjacency data. The view asynchronously animates game state transitions. Animations that block game state transitions used chained promises to ensure that game states do not transition until the animation is complete.
Promises compose nicely and work naturally whether the view chooses to kick off multiple aync animations serially or in parallel.
The end-of-level screen animates several, serial lines of text onto the screen, then animates a counting score. The game model does not transition to the next level until after all these animations, or if the user interrupts by clicking a “continue” button.
A helper method called _createLineExecutors()
in Eboc.SvgView
accepts lines of screen text as an array of strings. It returns an array of “executors” – functions that perform an asynchronous action which are passed to Promise
constructors.
Each executor calls resolve()
when its animation finishes. A simplified version of the Eboc function for generating executors looks like this:
function _createLineExecutors(lineTexts) {
const lineHeight = 10;
return lineTexts.map((text, lineNumber)=>{
return (resolve)=>{
r.paper.text(0,lineY += lineHeight, text).animate({fontSize:20}, 500, resolve);
};
}
}
Other executors can be appended to this array, and the final array chained together using Array.reduce
:
function showEndLevel(score) {
let executors = _createLineExecutors([
`The Score is: ${score}`,
"Well Done!",
this._animateScoreCounting
]);
return = executors.reduce((chain, executor)=>{
return chain.then(()=> {
return new Promise(executor);
});
}, Promise.resolve());
}
When showEndLevel()
is called, it returns the promise which is the head of the chain. The model can then tell the view to show the ending lines and only increment the model state after the view is done.
async function endLevel() {
await view.showEndLevel()
this.currentLevel++;
this.resetLevel();
}
The end level view animation is now decoupled and synchronized with the endLevel()
. The only requirement from the model is that the view’s method showEndLevel()
return a promise.
Eboc runs parallel promises using the Promise.all()
method on an array of promises. For example, when animating all blocks dropping:
Eboc.SvgView.prototype.showBoxesDropping = async function(boxesToDrop) {
return Promise.all(boxesToDrop.map(function(box){
return new Promise((resolve)=> {
let originalY = box.y;
box.y = originalY + box.fallingBy;
box.fallingBy = 0;
box.view.animate({
y: box.y
}, 500 * Math.random(), "bounce", resolve);
});
}));
}
When showBoxesDropping()
is called now, all box animations will kick off. Even though box animations may run for different amounts of time, the promise returned will not resolve until all are completed.