week1.js | |
---|---|
Game code for week 1 of Writing Your First Game Using HTML5 and Canvas The game code uses underscore.js, and some utility methods defined in week1-util.js Currently, we have 3 objects: | |
| var Drawable = _.inherits(function() { }, { |
Store the position and velocity of the current object. | 'state': { x: 0, y: 0, vX: 0, vY: 0 }, |
The constructor takes an initial state of the object, and merges it in with the defaults | 'constructor': function(initialState) {
this.state = _.extend({}, this.state, initialState);
}, |
Draw is called in the game loop, receiving the elapsed time | 'draw': function(dT, game) { |
Update the x and y coordinates using the current velocities. (This is sometimes called "integrating".) | var centroid = {
x: this.state.vX * dT + this.state.x,
y: this.state.vY * dT + this.state.y,
}; |
Hand off to the actual drawing method. Descendant objects override this to implement custom drawing logic. | this.doDraw(centroid, dT, game); |
Finally update the state of the object. | this.state.x = centroid.x;
this.state.y = centroid.y;
}, |
By default | 'doDraw': function(centroid, dT, game) {
game.backbufferContext.beginPath(centroid.x, centroid.y);
game.backbufferContext.arc(centroid.x - 5, centroid.y - 5, 10, 0, 2 * Math.PI);
game.backbufferContext.stroke();
}
}); |
| var Background = _.inherits(Drawable, {
'image': null,
'ready': false, |
The constructor calls the constructor of | 'constructor': function(initialState) {
this.constructor.__super__.constructor.call(this, initialState);
this.image = new Image();
this.image.onload = _.bind(function() {
this.ready = true;
}, this);
this.image.src = '/posts/game/game-bg.jpg';
}, |
| 'doDraw': function(centroid, dT, game) { |
Don't begin drawing until the image has downloaded. | if(!this.ready)
return; |
Make the background wrap around the canvas. | if(centroid.y <= 0)
centroid.y = game.height; |
The tiling background is drawn in two slices.
We use this to draw a top slice and a bottom slice that move up in unison. | game.backbufferContext.drawImage(
this.image,
/* sx */ 0,
/* sy */ this.image.naturalHeight - centroid.y,
/* sw */ this.image.naturalWidth,
/* sh */ centroid.y,
/* dx */ 0,
/* dy */ 0,
/* dw */ game.width,
/* dh */ centroid.y);
game.backbufferContext.drawImage(
this.image,
/* sx */ 0, /* sy */ 0,
/* sw */ this.image.naturalWidth,
/* sh */ this.image.naturalHeight - centroid.y,
/* dx */ 0,
/* dy */ centroid.y,
/* dw */ game.width,
/* dh */ this.image.naturalHeight - centroid.y
);
}
}); |
Define our game object. It has methods for setting up the rendering environment, initializing objects, and running the game. | var Game = _.inherits(function() { }, { |
The speed that objects in the game move at. | 'gameSpeed': -.15,
'constructor': function() { |
Initialize the lastPaint time, which we use to calculate the elapsed time ( | this.lastPaint = Date.now(); |
Use two canvas elements to do double-buffering, which prevents the user from seeing flickering as we draw the scene.
When the scene is finished drawing, we swap the two. | this.canvii = document.querySelectorAll('#sample1 canvas');
this.canvas = this.canvii[0];
this.backbufferCanvas = this.canvii[1];
this.context = this.canvas.getContext('2d');
this.backbufferContext = this.backbufferCanvas.getContext('2d'); |
| this.canvIdx = 0; |
Convenience methods for accessing the canvas size | this.width = this.canvas.width;
this.height = this.canvas.height; |
We will put all the drawable objects in this array, later we loop over the array to draw the objects. | this.objects = [];
this.initBackground(); |
Draw a circle in the center as a placeholder for the player | var centerObj = new Drawable({
x: this.width/2,
y: this.height/2
});
this.objects.push(centerObj); |
| this.draw = _.bind(this._draw, this); |
These are used to track the framerate as a moving average. | this.frames = 0;
this.times = [];
this.start = Date.now();
},
'initBackground': function() { |
Initialize the background and add it to beginning of the objects array. It
extends | this.objects.unshift(new Background({
vY: this.gameSpeed,
y: this.height
}));
}, |
| '_draw': function() {
if(!this.stop) {
window.requestAnimationFrame(this.draw);
} |
Calculate the amount of time since the loop last ran ( | var dT = Date.now() - this.lastPaint;
var drawStart = Date.now(); |
Clear the backbuffer, then ask the objects to draw themselves. | this.backbufferContext.clearRect(0, 0, this.width, this.height);
this.objects.forEach(function(obj) {
obj.draw(dT, this);
}, this); |
Increment the frame counter. Every 12 frames (5 times per second, since we run at 60 fps), calculate a new moving average framerate. | this.frames++;
if(this.frames % 12 == 0) {
this.fr = this.frames*1000/(Date.now() - this.start);
this.frames = 0;
this.start = Date.now();
} |
Store the time drawing ended. | this.lastPaint = Date.now(); |
Draw the framerate. | this.backbufferContext.strokeText(this.fr, 900, 20); |
Finally, swap the backbuffer and the display buffer, which shows the new scene to the user. | this.swapsies();
}, |
Swaps the two canvas elements, and their contexts. Neat trick courtesy of Fedor van Eldijk via StackOverflow | 'swapsies': function() {
this.canvas = this.canvii[this.canvIdx];
this.backbufferCanvas = this.canvii[1 - this.canvIdx];
this.canvIdx = 1 - this.canvIdx;
this.context = this.canvas.getContext('2d');
this.backbufferContext = this.backbufferCanvas.getContext('2d');
this.canvas.style.visibility = 'visible';
this.backbufferCanvas.style.visibility = 'hidden';
}
}); |
A quick and dirty shim for | window.requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame; |
Now simply start the game by initializing it and calling the draw method. It can be stopped by setting | var game = new Game();
game.draw();
document.getElementByid('stop').onclick = function() {
game.stop = true;
}; |
That's all the code so far! | |