Summary
- Introduction
- Prerequisites
- Setting up
the background
- Setting up
the game
- Conclusion
Introduction
To do
so, we will write together a brick breaker game (à la Arkanoïd or Blockout). It
will be composed of an animated background (using Canvas) and will use SVG for
bricks, pad and ball.
You
can try the final version here: http://www.catuhe.com/ms/en/index.htm
Prerequisites
Internet Explorer 9/10
or other hardware-accelerated
HTML5 modern browser
Visual Studio 2010 SP1
Web Standards Update : http://visualstudiogallery.msdn.microsoft.com/a15c3ce9-f58f-42b7-8668-53f6cdc2cd83
Setting up the Background
The
background is only an alibi to use a canvas. It will allow us to draw pixels in
a given area. So we will use it to draw a space wormhole (yes, I love Stargate!).
Users will have choice to display it or not using the mode button:
You can note that we will add a performance counter in the right
top corner (just to see the power of accelerated graphics )
Setting up HTML5 Page
Starting
from the index.htm file, we will add our canvas as child of the div “gameZone”:
1. <canvas id="backgroundCanvas">
2. Your browser doesn't support HTML5. Please install Internet Explorer 9 :
3. <br />
4. <a href="http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing">
5. http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing</a>
6. </canvas>
Adding JavaScript code
The
background is handled by background.js file (what a surprise!). So we
have to register it inside index.htm. So just before the body closing tag, we
will add the following code:
1. <script type="text/javascript" src="background.js"></script>
Setting up Constants
First of all, we need constants to drive
the rendering:
1. var circlesCount = 100; 2. var offsetX = 70; 3. var offsetY = 40; 4. var maxDepth = 1.5; 5. var circleDiameter = 10.0; 6. var depthSpeed = 0.001; 7. var angleSpeed = 0.05;
You
can of course modify these constants if you want different effects on your
wormhole.
Getting
elements
We
also need to keep reference to main elements of the html page:
1. var canvas = document.getElementById("backgroundCanvas");
2. var context = canvas.getContext("2d");
3. var stats = document.getElementById("stats");
How to display a circle?
The
wormhole is only a sequence of circles with different positions and sizes. So
to draw it, we will use a circle function which is build around a depth, an
angle and an intensity (the base color).
1. function Circle(initialDepth, initialAngle, intensity) {
2. }
The
angle and the intensity are private but the depth is public to allow the
wormhole to change it.
1. function Circle(initialDepth, initialAngle, intensity) {
2.
3. var angle = initialAngle;
4. this.depth = initialDepth;
5. var color = intensity;
6. }
We
also need a public draw function to draw the circle and update depth, angle. So
we need to define where to display the circle. To do so, two variables (x and
y) are defined:
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
As x
and y are in space coordinates, we need to project them into the screen:
1. function perspective(fov, aspectRatio, x, y) {
2. var yScale = Math.pow(Math.tan(fov / 2.0), -1);
3. var xScale = yScale / aspectRatio;
4.
5. var M11 = xScale;
6. var M22 = yScale;
7.
8. var outx = x * M11 + canvas.width / 2.0;
9. var outy = y * M22 + canvas.height / 2.0;
10.
11.return { x: outx, y: outy };
12.}
So
final position of the circle is computed by the following code:
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
3.
4. var project = perspective(0.9, canvas.width / canvas.height, x, y);
5. var diameter = circleDiameter / this.depth;
6.
7. var ploX = project.x - diameter / 2.0;
8. var ploY = project.y - diameter / 2.0;
And
using this position, we can simply draw our circle:
1. context.beginPath();
2. context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
3. context.closePath();
4.
5. var opacity = 1.0 - this.depth / maxDepth;
6. context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
7. context.lineWidth = 4;
8.
9. context.stroke();
You
can note that the circle is more opaque when it is closer.
So finally:
1. function Circle(initialDepth, initialAngle, intensity) {
2. var angle = initialAngle;
3. this.depth = initialDepth;
4. var color = intensity;
5.
6. this.draw = function () {
7. var x = offsetX * Math.cos(angle);
8. var y = offsetY * Math.sin(angle);
9.
10.var project = perspective(0.9, canvas.width / canvas.height, x, y);
11.var diameter = circleDiameter / this.depth;
12.
13.var ploX = project.x - diameter / 2.0;
14.var ploY = project.y - diameter / 2.0;
15.
16.context.beginPath();
17.context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
18.context.closePath();
19.
20.var opacity = 1.0 - this.depth / maxDepth;
21.context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
22.context.lineWidth = 4;
23.
24.context.stroke();
25.
26.this.depth -= depthSpeed;
27.angle += angleSpeed;
28.
29.if (this.depth < 0) {
30.this.depth = maxDepth + this.depth;
31.}
32.};
33.};
Initialization
With
our circle function, we can have an array of circles that we will initiate more
and more close to us with a slight shift of the angle each time:
1. 2. var circles = [];
3.
4. var angle = Math.random() * Math.PI * 2.0;
5.
6. var depth = maxDepth;
7. var depthStep = maxDepth / circlesCount;
8. var angleStep = (Math.PI * 2.0) / circlesCount;
9. for (var index = 0; index < circlesCount; index++) {
10.circles[index] = new Circle(depth, angle, index % 5 == 0 ? 200 : 255);
11.
12.depth -= depthStep;
13.angle -= angleStep;
14.}
Computing FPS
We can
compute FPS by measuring the amount of time between two calls to a given
function. In our case, the function will be computeFPS
. It will save the
last 60 measures and will compute an average to produce the desired result:
1. 2. var previous = [];
3. function computeFPS() {
4. if (previous.length > 60) {
5. previous.splice(0, 1);
6. }
7. var start = (new Date).getTime();
8. previous.push(start);
9. var sum = 0;
10.
11.for (var id = 0; id < previous.length - 1; id++) {
12.sum += previous[id + 1] - previous[id];
13.}
14.
15.var diff = 1000.0 / (sum / previous.length);
16.
17.stats.innerHTML = diff.toFixed() + " fps";
18.}
Drawing and Animations
The
canvas is a direct mode tool. This means that we have to reproduce all
the content of the canvas every time we need to change something.
And
first of all, we need to clear the content before each frame. The better
solution to do that is to use clearRect
:
1. 2. function clearCanvas() {
3. context.clearRect(0, 0, canvas.width, canvas.height);
4. }
So the
full wormhole drawing code will look like:
1. function wormHole() {
2. computeFPS();
3. canvas.width = window.innerWidth;
4. canvas.height = window.innerHeight - 130 - 40;
5. clearCanvas();
6. for (var index = 0; index < circlesCount; index++) {
7. circles[index].draw();
8. }
9.
10.circles.sort(function (a, b) {
11.if (a.depth > b.depth)
12.return -1;
13.if (a.depth < b.depth)
14.return 1;
15.return 0;
16.});
17.}
The
sort code is used to prevent circles from overlapping.
Setting Up Mode Button
To
finalize our background, we just need to hook up the mode button to display or
hide the background:
1.
2. var wormHoleIntervalID = -1;
3.
4. function startWormHole() {
5. if (wormHoleIntervalID > -1)
6. clearInterval(wormHoleIntervalID);
7.
8. wormHoleIntervalID = setInterval(wormHole, 16);
9.
10.document.getElementById("wormHole").onclick = stopWormHole;
11.document.getElementById("wormHole").innerHTML = "Standard Mode";
12.}
13.
14.function stopWormHole() {
15.if (wormHoleIntervalID > -1)
16.clearInterval(wormHoleIntervalID);
17.
18.clearCanvas();
19.document.getElementById("wormHole").onclick = startWormHole;
20.document.getElementById("wormHole").innerHTML = "Wormhole Mode";
21.}
22.
23.stopWormHole();
Setting up the Game
In
order to simplify a bit the tutorial, the mouse handling code is already done.
You can find all you need in the mouse.js file.
Adding the Game JavaScript File
The
background is handled by game.js file. So we have to register it inside index.htm.
So right before the body closing tag, we will add the following code:
1. <script type="text/javascript" src="game.js"></script>
Updating HTML5 Page
The
game will use SVG (Scalable Vector Graphics) to display the bricks, pad
and ball. The SVG is a retained mode tool. So you don’t need to redraw all
every time you want to move or change an item.
To add
a SVG tag in our page, we just have to insert the following code (just after
the canvas):
1. <svg id="svgRoot">
2. <circle cx="100" cy="100" r="10" id="ball" />
3. <rect id="pad" height="15px" width="150px" x="200" y="200" rx="10" ry="20"/>
4. </svg>
As you
can note, the SVG starts with two already defined objects : a circle for the
ball and a rectangle for the pad.
Defining Constants and Variables
In
game.js file, we will start by adding some variables:
1. 2. var pad = document.getElementById("pad");
3. var ball = document.getElementById("ball");
4. var svg = document.getElementById("svgRoot");
5. var message = document.getElementById("message");
The
ball will be defined by:
- A position
- A radius
- A speed
- A direction
- Its previous position
1. 2. var ballRadius = ball.r.baseVal.value;
3. var ballX;
4. var ballY;
5. var previousBallPosition = { x: 0, y: 0 };
6. var ballDirectionX;
7. var ballDirectionY;
8. var ballSpeed = 10;
The
pad will be defined by:
- Width
- Height
- Position
- Speed
- Inertia value (just to make things smoother)
1. 2. var padWidth = pad.width.baseVal.value;
3. var padHeight = pad.height.baseVal.value;
4. var padX;
5. var padY;
6. var padSpeed = 0;
7. var inertia = 0.80;
Bricks
will be saved in an array and will be defined by:
- Width
- Height
- Margin between them
- Lines count
- Columns count
We also
need an offset and a variable for counting destroyed bricks.
1. 2. var bricks = [];
3. var destroyedBricksCount;
4. var brickWidth = 50;
5. var brickHeight = 20;
6. var bricksRows = 5;
7. var bricksCols = 20;
8. var bricksMargin = 15;
9. var bricksTop = 20;
And
finally we also need the limits of the playground and a start date to compute
session duration.
1. 2. var minX = ballRadius;
3. var minY = ballRadius;
4. var maxX;
5. var maxY;
6. var startDate;
Handling a Brick
To
create a brick, we will need a function that will add a new element to the svg
root. It will also configure each brick with required information:
1. var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2. svg.appendChild(rect);
3.
4. rect.setAttribute("width", brickWidth);
5. rect.setAttribute("height", brickHeight);
6.
7. 8. var chars = "456789abcdef";
9. var color = "";
10.for (var i = 0; i < 2; i++) {
11.var rnd = Math.floor(chars.length * Math.random());
12.color += chars.charAt(rnd);
13.}
14.rect.setAttribute("fill", "#00" + color + "00");
The brick function will also provide a drawAndCollide
function to display a brick and to check if there is a collision with the ball:
1. this.drawAndCollide= function () {
2. if(isDead)
3. return;
4. 5. rect.setAttribute("x",position.x);
6. rect.setAttribute("y",position.y);
7.
8. 9. if (ballX+ ballRadius < position.x || ballX - ballRadius > position.x +brickWidth)
10.return;
11.
12.if (ballY + ballRadius <position.y || ballY - ballRadius > position.y + brickHeight)
13.return;
14.
15.16.this.remove();
17.isDead = true;
18.destroyedBricksCount++;
19.
20.21.ballX = previousBallPosition.x;
22.ballY = previousBallPosition.y;
23.
24.ballDirectionY *= -1.0;
25.};
Finally the full brick function will look like:
1. 2. functionBrick(x, y) {
3. var isDead= false;
4. varposition = { x: x, y: y };
5.
6. var rect =document.createElementNS("http://www.w3.org/2000/svg", "rect");
7. svg.appendChild(rect);
8.
9. rect.setAttribute("width",brickWidth);
10.rect.setAttribute("height",brickHeight);
11.
12.13.var chars = "456789abcdef";
14.var color = "";
15.for (var i = 0;i < 2; i++) {
16.var rnd = Math.floor(chars.length *Math.random());
17.color += chars.charAt(rnd);
18.}
19.rect.setAttribute("fill", "#00" + color+ "00");
20.
21.this.drawAndCollide= function () {
22.if (isDead)
23.return;
24.25.rect.setAttribute("x",position.x);
26.rect.setAttribute("y",position.y);
27.
28.29.if (ballX + ballRadius <position.x || ballX - ballRadius > position.x + brickWidth)
30.return;
31.
32.if (ballY + ballRadius <position.y || ballY - ballRadius > position.y + brickHeight)
33.return;
34.
35.36.this.remove();
37.isDead = true;
38.destroyedBricksCount++;
39.
40.41.ballX = previousBallPosition.x;
42.ballY = previousBallPosition.y;
43.
44.ballDirectionY *= -1.0;
45.};
46.
47.48.this.remove = function () {
49.if (isDead)
50.return;
51.svg.removeChild(rect);
52.};
53.}
Collisions with the Pad and the Playground
The ball will also have collision functions that will handle collisions with the pad and the playground. These functions will have to update the ball direction when a collision will be detected.
1. 2. functioncollideWithWindow() {
3. if (ballX< minX) {
4. ballX =minX;
5. ballDirectionX*= -1.0;
6. }
7. else if (ballX> maxX) {
8. ballX =maxX;
9. ballDirectionX*= -1.0;
10.}
11.
12.if (ballY < minY) {
13.ballY = minY;
14.ballDirectionY *= -1.0;
15.}
16.else if (ballY> maxY) {
17.ballY = maxY;
18.ballDirectionY *= -1.0;
19.lost();
20.}
21.}
22.
23.functioncollideWithPad() {
24.if (ballX + ballRadius < padX ||ballX - ballRadius > padX + padWidth)
25.return;
26.
27.if (ballY + ballRadius < padY)
28.return;
29.
30.ballX = previousBallPosition.x;
31.ballY = previousBallPosition.y;
32.ballDirectionY *= -1.0;
33.
34.var dist = ballX - (padX + padWidth/ 2);
35.
36.ballDirectionX = 2.0 * dist / padWidth;
37.
38.var square =Math.sqrt(ballDirectionX * ballDirectionX + ballDirectionY * ballDirectionY);
39.ballDirectionX /= square;
40.ballDirectionY /= square;
41.}
collideWithWindow
checks the limits of the playground and collideWithPad
checks the limitsof the pad (We add a subtle change here: the horizontal speed of the ball will be computed using the distance with the center of the pad).
Moving the Pad
Youcan control the pad with the mouse or with the left and right arrows. The movePad
function is responsible for handling pad movement. It will also handle the inertia:
1. 2. functionmovePad() {
3. padX +=padSpeed;
4.
5. padSpeed*= inertia;
6.
7. if (padX< minX)
8. padX =minX;
9.
10.if (padX + padWidth > maxX)
11.padX = maxX - padWidth;
12.}
The code responsible for handling inputs is pretty simple:
1. registerMouseMove(document.getElementById("gameZone"), function (posx,posy, previousX, previousY) {
2. padSpeed+= (posx - previousX) * 0.2;
3. });
4.
5. window.addEventListener('keydown', function (evt) {
6. switch(evt.keyCode) {
7. 8. case 37:
9. padSpeed-= 10;
10.break;
11.12.case 39:
13.padSpeed += 10;
14.break;
15.}
16.}, true);
Game Loop
Before setting up the game loop we need a function to define the playground size. Thisfunction will be called when window is resized.
1. functioncheckWindow() {
2. maxX =window.innerWidth - minX;
3. maxY =window.innerHeight - 130 - 40 - minY;
4. padY =maxY - 30;
5. }
By the way, the game loop is the orchestrator here:
1. functiongameLoop() {
2. movePad();
3.
4. 5. previousBallPosition.x= ballX;
6. previousBallPosition.y= ballY;
7. ballX +=ballDirectionX * ballSpeed;
8. ballY +=ballDirectionY * ballSpeed;
9.
10.11.collideWithWindow();
12.collideWithPad();
13.
14.15.for (var index =0; index < bricks.length; index++) {
16.bricks[index].drawAndCollide();
17.}
18.
19.20.ball.setAttribute("cx",ballX);
21.ball.setAttribute("cy",ballY);
22.
23.24.pad.setAttribute("x", padX);
25.pad.setAttribute("y", padY);
26.
27.28.if (destroyedBricksCount ==bricks.length) {
29.win();
30.}
31.}
Initialization and Victory
The first step of initialization is creating bricks:
1. functiongenerateBricks() {
2. 3. for (var index =0; index < bricks.length; index++) {
4. bricks[index].remove();
5. }
6.
7. 8. var brickID= 0;
9.
10.var offset = (window.innerWidth -bricksCols * (brickWidth + bricksMargin)) / 2.0;
11.
12.for (var x = 0;x < bricksCols; x++) {
13.for (var y = 0;y < bricksRows; y++) {
14.bricks[brickID++] = newBrick(offset + x * (brickWidth + bricksMargin), y * (brickHeight + bricksMargin)+ bricksTop);
15.}
16.}
17.}
The next step is about setting variables used by the game:
1. functioninitGame() {
2. message.style.visibility= "hidden";
3.
4. checkWindow();
5.
6. padX =(window.innerWidth - padWidth) / 2.0;
7.
8. ballX =window.innerWidth / 2.0;
9. ballY = maxY- 60;
10.
11.previousBallPosition.x = ballX;
12.previousBallPosition.y = ballY;
13.
14.padSpeed = 0;
15.
16.ballDirectionX = Math.random();
17.ballDirectionY = -1.0;
18.
19.generateBricks();
20.gameLoop();
21.}
Every time the user will change the window size, we will have to reset the game:
1. window.onresize= initGame;
Then we have to attach an event handler to the new game button:
1. vargameIntervalID = -1;
2. functionstartGame() {
3. initGame();
4.
5. destroyedBricksCount= 0;
6.
7. if(gameIntervalID > -1)
8. clearInterval(gameIntervalID);
9.
10.startDate = (newDate()).getTime(); ;
11.gameIntervalID = setInterval(gameLoop,16);
12.}
13.
14.document.getElementById("newGame").onclick= startGame;
Finally, we will add two functions for handling start and end of the game:
1. vargameIntervalID = -1;
2. function lost(){
3. clearInterval(gameIntervalID);
4. gameIntervalID= -1;
5.
6. message.innerHTML= "Game over !";
7. message.style.visibility= "visible";
8. }
9.
10.function win() {
11.clearInterval(gameIntervalID);
12.gameIntervalID = -1;
13.
14.var end = (newDate).getTime();
15.
16.message.innerHTML = "Victory! (" + Math.round((end - startDate) / 1000) + "s)";
17.message.style.visibility = "visible";
18.}
Conclusion
You are now a game developer! Using the power of accelerated graphics, we have developed a small game but with really interesting special effects!
It’s now up to you to update the game to make it the next blockbuster!
To go Further