Wite a small game

June 26, 2017


Introduction to drawing/animating

HTML5 Canvas basic usage

HTML5 canvas は、絵を描いたり、アニメーションを作成するのに便利な透明な要素です。

A typical HTML code for adding a canvas to a Web page:
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>Draw a monster in a canvas</title>
 </head>
 <body>
  <canvas id="myCanvas" width="200" height="200"></canvas>
 </body>
</html>

8行目に canvas が宣言されています。 属性で幅と高さを指定しています。 しかし、CSSプロパティを追加しないと、画面に見えません、なぜならば透明だからです。

canvasが表示されるように、周囲に境界線を指定します 1px black border :

1
2
3
canvas {
   border: 1px solid black;
}

canvasを使った練習:

  • 1. ページが完全に読み込まれた(DOMが使用できるようになった)後に、呼び出される関数を使い、DOMの中の canvas を選択します。( init 関数ですね、 window.onload = init )
  • 2. 次に、この canvas ように 2D グラフィック コンテキストを取得します (このコンテキストはオブジェクトです、canvas に絵を描いたり、グローバルプロパティである、色、グラデーション、模様、線の幅などを設定するのに使用します
  • 3. 次は何かを描くだけです
  • 4. canvas と コンテキスト・オブジェクトのためにグローバル変数を使うことを忘れないように。また、canvas の幅と高さをどこかに保持しておくことをお勧めします。後々役立つでしょう。
  • 5. コンテキスト (color, line width, coordinate system, etc.)を変える関数のために、コンテキストを保存することから始めます、そして、元に戻して終了します

Extract from the JavaScript code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// useful to have them as global variables
var canvas, ctx;

window.onload = function init() {
  // called AFTER the page has been loaded
  canvas = document.querySelector("#myCanvas");
  // important, we will draw with this object
  ctx = canvas.getContext('2d');
  // ready to go! We can use the context for drawing
  // or changing colors, line widths, etc.
  // filled rectangle
  ctx.fillStyle = 'red';
  ctx.fillRect(10, 10, 30, 30);
  // wireframe rectangle
  ctx.strokeStyle = 'green';
  ctx.lineWidth = 4;
  ctx.strokeRect(100, 40, 40, 40);
  // fill circle, will use current ctx.fillStyle
  ctx.beginPath();
  ctx.arc(60, 60, 10, 0, 2*Math.PI);
  ctx.fill(); // or ctx.stroke() for a wireframe circle
  // some text
  ctx.fillStyle = "purple";
  ctx.font = "20px Arial";
  ctx.fillText("Hello!", 60, 20); // or ctx.strokeText for wireframe
}

Explanations:

  • 1. We use a function (line 4) called after the page is loaded (we say "after the DOM is ready"), so that the querySelector at line 6 will return the canvas. If the page was not completely loaded and if this code had been run before it had finished loading, the canvas value would have been "undefined".
  • 2. Once we have the canvas, we request a "graphic context" (line 8). This is a variable for 2D or 3D drawing on a canvas (in our case: 2D!) that we will use for drawing or setting colors, line widths, text fonts, etc.
  • 3. Then we can draw. Here we show only a few things you can do with the canvas API, but believe me, you can do much more (draw images, gradients, textures, etc.)! At line 15, we draw a filled rectangle. Parameters are the x and y coordinates of the top left corner (x goes to the right, y to the bottom of your screen), and the width and the height of the rectangle. At line 14, we used the fillStyle property of the context to set the color of filled shapes. This means: "now, all filled shapes you are going to draw will be in red!". It's like a global setting.
  • 4. Lines 17-20 draw a green wireframe rectangle, with a line width equal to 4 pixels. Notice the use of "stroke" instead of "fill" in the property name strokeStyle/fillStyle and in the context method for drawing a rectangle strokeRect/fillRect.
  • 5. Lines 23-25 draw a filled circle. The syntax is a bit different as circles are parts of a "path" (see the HTML5 fundamentals course, we explain the concept of "path" in the canvas API). Just keep in mind for now that before drawing a circle you need to call beginPath(). The call to arc(x, y, radius, start_angle, end_angle) does not draw the circle, it defines it. The next instruction ctx.fill() at line 25 will draw all shapes that have been defined since a new path began, as filled shapes. Calling ctx.stroke() here, instead of ctx.fill() would have drawn a wireframe circle instead of a filled one. Also note that the filled circle is red even if we did not specify the color. Remember that we set ctx.fillStyle = 'red' at line 14. Unless we change this, all filled shapes will be red.
  • 6. Lines 28-30 draw a filled text. The call to filltext(message, x, y) draws a filled text at the x,y position; this time in purple as we called ctx.fi
Example 2: functions that save and restore the context before drawing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// useful to have them as global variables
var canvas, ctx, w, h;

window.onload = function init() {
    // called AFTER the page has been loaded
    canvas = document.querySelector("#myCanvas");

    // often useful
    w = canvas.width;
    h = canvas.height;  

    // important, we will draw with this object
    ctx = canvas.getContext('2d');

    // ready to go !
    drawFilledRectangle(10, 10, 20, 20, "red");

    drawFilledCircle(100, 100, 15, "green");
};

function drawFilledRectangle(x, y, width, height, color) {
    // GOOD practice: save the context, use 2D trasnformations
    ctx.save();

    // translate the coordinate system, draw relative to it
    ctx.translate(x, y);

    ctx.fillStyle = color;
    // (0, 0) is the top left corner of the monster.
    ctx.fillRect(0, 0, width, height);

    // GOOD practice: restore the context
    ctx.restore();
}

function drawFilledCircle(x, y, radius, color) {
    // GOOD practice: save the context, use 2D trasnformations
    ctx.save();

    // translate the coordinate system, draw relative to it
    ctx.translate(x, y);

    ctx.fillStyle = color;
    // (0, 0) is the top left corner of the monster.
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, 2*Math.PI);
    ctx.fill();

    // GOOD practice: restore the context
    ctx.restore();
}
Explanations:

This time we’ve written two functions for a cleaner code: one function that draws a filled rectangle with a given color, and one function that draws a filled circle, with a given color.

The values for x, y, width, height, radius, color can be passed as parameters to these functions.

When a function changes anything to the “global context”: filled or stroke color, line width, or the position of the coordinate system (located by default in 0, 0, at the top left of the canvas), then it is good practice to save this context at the beginning of the function, with a call to ctx.save(), and to restore it at the end of the function, with a call to ctx.restore(). In this way, any change to the “global context” won’t have any effect outside of the function.

We used also ctx.translate(x, y) in order to move the rectangle and the circle (look, they have been drawn at x=0, y=0, but as we translate the origin of the coordinate system with ctx.translate, the shapes are located in x, y on in the canvas). This is also a good practice: indeed, if we add more shapes (like eyes in the rectangle, in order to draw a monster), using coordinates relative to 0, 0, the whole set of shapes will be translated by the call to ctx.translate(x, y). This will make it easier to draw characters, monsters, etc. as we will see in a third example.

Example 3: draw a monster instead of a simple rectangle or circle

This is where you reap the benefits of your good habits of saving/restoring the context and using ctx.translate(x, y)!

Here is JavaScript code that implements these best practices:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// useful to have them as global variables
var canvas, ctx, w, h;


window.onload = function init() {
  // Called AFTER the page has been loaded
  canvas = document.querySelector("#myCanvas");

  // Often useful
  w = canvas.width;
  h = canvas.height;

  // Important, we will draw with this object
  ctx = canvas.getContext('2d');

  // Ready to go!
  // Try to change the parameter values to move
    // the monster
  drawMyMonster(10, 10); // try to change that
};

function drawMyMonster(x, y) {
  // Draw a big monster!
  // Head

  // BEST practice: save the context, use 2D transformations
  ctx.save();

  // Translate the coordinate system, draw relative to it
  ctx.translate(x, y);

  // (0, 0) is the top left corner of the monster.
  ctx.strokeRect(0, 0, 100, 100);

  // Eyes
  ctx.fillRect(20, 20, 10, 10);
  ctx.fillRect(65, 20, 10, 10);

  // Nose
  ctx.strokeRect(45, 40, 10, 40);

  // Mouth
  ctx.strokeRect(35, 84, 30, 10);

  // Teeth
  ctx.fillRect(38, 84, 10, 10);
  ctx.fillRect(52, 84, 10, 10);

  // BEST practice: restore the context
  ctx.restore();
}

In this small example, we used the context object to draw a monster using the default color (black) and wireframe and filled modes:

  • ctx.fillRect(x, y, width, height): draws a rectangle whose top left corner is at (x, y) and whose size is specified by the width and height parameters; and both outlined by, and filled with, the default color.
  • ctx.strokeRect(x, y, width, height): same but in wireframe mode.
  • Note that we use (line 30) ctx.translate(x, y) to make it easier to move the monster around. So, all the drawing instructions are coded as if the monster was in (0, 0), at the top left corner of the canvas (look at line 33). We draw the body outline with a rectangle starting from (0, 0). Calling context.translate "changes the coordinate system" by moving the "old (0, 0)" to (x, y) and keeping other coordinates in the same position relative to the origin.
  • Line 19: we call the drawMonster function with (10, 10) as parameters, which will cause the original coordinate system to be translated by (10, 10).
  • And if we change the coordinate system (this is what the call to ctx.translate(...) does) in a function, it is good practice to always save the previous context at the beginning of the function and restore it at the end of the function (lines 27 and 50).

Animating

A typical animation loop will do the following at regular intervals:

  • 1. Clear the canvas
  • 2. Draw graphic objects / shapes
  • 3. Move graphic shapes / objects
  • 4. Go to step 1

Optional steps can be:

  • Look at the keyboard / mouse / gamepad if we need to do something according to their status (i.e. if the left arrow is pressed: move the player to the left)
  • Test collisions: the player collided with an enemy, remove one life
  • Test game states: if there are no more lives, then go to the "game over" state and display a "game over" menu.
  • Etc.
Example 1: let’s do some animation

There are different methods for coding an animation loop in JavaScript, that are described in the above video, and detailed both in the HTML5 Coding Essentials and Best Practices W3Cx course (week 4), and in the HTML5 Apps and Games W3Cx course (Week 2 is for learning how to code HTML5 games).

In this intro course, we’ll only use the recommended one, without going into advanced use.

The trick is to write a function, and at the end of this function, to ask the browser to call it again in 1/60th of a second if possible.

Here is an example:

Example 2: animating a ball that bounces on the sides of the canvas (walls)
Explanations:

This time we’ve used “simple objects” for the circle and the rectangles, and we’ve called them “player” and “ball”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ball = {
  x: 100,
  y:100,
  radius: 15,
  color:'green',
  speedX:2,
  speedY:1
}

var player = {
  x:10,
  y:10,
  width:20,
  height:20,
  color:'red'
}

With this syntax, it’s easier to manipulate “the x pos of the ball” - you just have to use ball.x. we added two properties to the ball object: speedX and speedY. Their value is the number of pixels that will be added to the current ball.x and ball.y position, at each frame of animation.

Let’s look at the animation loop:

1
2
3
4
5
6
7
8
9
10
11
12
function mainLoop() {
  // 1 - clear the canvas
  ctx.clearRect(0, 0, w, h);
  // draw the ball and the player
  drawFilledRectangle(player);
  drawFilledCircle(ball);

  // animate the ball that is bouncing all over the walls
  moveBall(ball);
  // ask for a new animation frame
  requestAnimationFrame(mainLoop);
}

Now, let’s decompose the animation loop in some external functions to make it more readable. At each frame of animation, we will clear the canvas, draw the player as a rectangle, draw the ball as a circle, and move the ball.

You can take a look at the new versions of drawFilledRectangle that now take only one parameter named r, instead of x, y, width, height and a color. We’ve only changed a few things in its code (changed x to r.x, y to r.y, color to r.color etc.)

Let’s look at the moveBall function:

1
2
3
4
5
6
function moveBall(b) {
    b.x += b.speedX;
    b.y += b.speedY;

    testCollisionBallWithWalls(b);
}

This function is called 60 times per second. So, 60 times per second we modify the b.x and b.y positions of the ball passed as parameter by adding to them the b.speedX and b.speedY property values.

Notice that we call moveBall(ball) from mainLoop. In the moveBall function, the ball passed as a parameter becomes the b parameter. So when we change the b.x value inside the function, we are in reality changing the x value of the global object ball!

Ok, and at line 5 we call testCollisionBallWithWalls(b), which will test if the ball b hits a vertical or horizontal wall. Let’s see an extract of this function now:

1
2
3
4
5
6
7
8
9
10
11
12
function testCollisionBallWithWalls(b) {
    // COLLISION WITH VERTICAL WALLS?
    if((b.x + b.radius) > w) {
        // the ball hit the right wall
        // change horizontal direction
        b.speedX = -b.speedX;

        // put the ball at the collision point
       b.x = w - b.radius;
    } ...
    ...
}

At line 3 you can see the test that checks if the ball b hits the right side of the canvas. The right wall is at w (the width of the canvas) on the X-axis. If we compare (b.x + b.radius) with w, we can check if a part of the ball extends beyond the right wall.

Remember that each 1/60th of a second, the ball moves a certain number of pixels to the right (the exact value is b.speedX). Imagine that the ball moves 10 pixels to the right at each frame of animation. At some point, it will “cross the right wall”. We cannot just change the sign of b.speedX to make it go to the other side. If we did this, it may stay stuck against the side with one half of the ball on either side of the wall.

If we now remove b.speedX to the ball.x position, we return the ball to the position it was in before it hit the wall. If we then reverse speedX, the ball will indeed start moving with a reverse horizontal speed. This will work but can give a strange visual effect if the balls moves, say, 20 pixels per frame or more. The ball will never be in a position where the eye can “see it against the wall”. This is why experienced game coders know that you just need to put the ball “at the contact position”, not to its previous position, before reversing the speed value. This is done at lines 8-9. Try changing speedX to say, 20, and you’ll see what we mean.


Animating multiple objects

createBalls(numberOfBalls), returns an array of balls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createBalls(n) {
  // empty array
  var ballArray = [];

  // create n balls
  for(var i=0; i < n; i++) {
      var b = {
          x:w/2,
          y:h/2,
          radius: 5 + 30 * Math.random(), // between 5 and 35
          speedX: -5 + 10 * Math.random(), // between -5 and + 5
          speedY: -5 + 10 * Math.random(), // between -5 and + 5
          color: getARandomColor(),
      }
      // add ball b to the array
      ballArray.push(b);
  }
  // returns the array full of randomly created balls
  return ballArray;
}
Explanations:
  • Line 3: we declare an empty array that will contain the balls,
  • Lines 7-14: we create a new ball object with random values. Note the use of Math.random(), a predefined JavaScript function that returns a decimal value between 0 and 1. We call another function named getARandomColor() that returns a color taken randomly.
  • Line 16: we add the newly created ball b to the array,
  • Line 19: we return the array to the caller.
The getARandomColor function
1
2
3
4
5
6
7
8
9
10
11
12
function getARandomColor() {
    var colors = ['red', 'blue', 'cyan', 'purple',
                  'pink', 'green', 'yellow'];
    // a value between 0 and color.length-1
    // Math.round = rounded value
    // Math.random() a value between 0 and 1
    var colorIndex = Math.round((colors.length-1)*Math.random());
    var c = colors[colorIndex];

    // return the random color
    return c;
}
Explanations:
  • Line 2: in this function, we use an array of random color names named colors (you can go on the codePen example and change these colors or add new ones).
  • Line 7: then we compute an index with a random value between 0 and colors.length-1. Remember that in an array of n elements, the index of the first is always 0 and the index of the last one is always equal to the length of the array -1. For example: var myArray = ['red', 'blue', 'green'], red is at index 0, green at index 2, while myArray.length = 3, the number of elements in the array.
  • Lines 8 and 11: once we get a random index in the correct range, we can return the corresponding color.
Functions drawAllBalls and moveAllBalls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function drawAllBalls(ballArray) {
    ballArray.forEach(function(b) {
        drawFilledCircle(b);
    });
}

function moveAllBalls(ballArray) {
    // iterate on all balls in array
    ballArray.forEach(function(b) {
        // b is the current ball in the array
        b.x += b.speedX;
        b.y += b.speedY;
        testCollisionBallWithWalls(b);
    });
}
Explanations:
  • These two functions use an iterator on the array of balls (using the forEach method that looked the best fit here). The code inside the iterator is the same as in the previous example. We did not have to modify the testCollisionBallWithWalls code, for example.

Mouse interactions, mouse events

Mouse interactions, mouse events in a canvas

INTRODUCTION

Detecting mouse events in a canvas is quite straightforward: you add an event listener to the canvas, and the browser invokes that listener when the event occurs.

The example below is about listening to mouseup and mousedown events (when a user presses or releases any mouse button):

1
2
3
4
5
6
7
canvas.addEventListener('mousedown', function (evt) {
    // do something with the mousedown event
});

canvas.addEventListener('mousedup', function (evt) {
    // do something with the mouseup event
});

The event received by the listener function will be used for getting the button number or the coordinates of the mouse cursor. Before looking at different examples, let’s look at the different event types we can listen to.

The different mouse events (reminder)

In the last example, we saw how to detect the mouseup and mousedown events.

There are other events related to the mouse:

  • mouseleave: similar to mouseout, fired when the mouse leaves the surface of the element. The difference between mouseleave and mouseout is that mouseleave does not fire when the cursor moves over descendant elements, and mouseout is fired when the element the cursor moves to is outside the bounds of the original element or is a child of the original element.
  • mouseover: the mouse cursor is moving over the element that listens to that event. A mouseover event occurs on an element when you are over it - coming from either its child OR parent element, but a mouseenter event only occurs when the mouse moves from the parent element to the child element.
  • mousedown: fired when a mouse button is pressed.
  • mouseup: fired when a mouse button is released.
  • mouseclick: fired after a mousedown and a mouseup have occured.
  • mousemove: fired while the mouse moves over the element. Each time the mouse moves, a new event is fired, unlike with mouseover or mouseenter, where only one event is fired.
The tricky part: getting the position of the mouse relative to the canvas

When you listen to any of the above events, the event object (we call it a “DOM event”), passed to the listener function, has properties that correspond to the mouse coordinates: clientX and clientY.

However, these are what we call “viewport coordinates”. Instead of being relative to the canvas itself, they are relative to the viewport (the visible part of the page).

Most of the time you need to work with the mouse position relative to the canvas, not to the viewport, so you must convert the coordinates between the viewport and the canvas. This will take into account the position of the canvas in the viewport, and the CSS properties that may affect the canvas position (margin, etc.).

Fortunately, there is a method for getting the position and size of any element in the viewport: getBoundingClientRect().

Here is an example that shows the problem:

Move the mouse cursor to the top left corner of the canvas:<p></p>

Instead of (0, 0), you will see coordinates relative to the window object (page)

WRONG code used in this example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
canvas.addEventListener('mousemove', function (evt) {
    mousePos = getMousePos(canvas, evt);
    var message = 'Mouse position: ' + mousePos.x + ',' + mousePos.y;
    writeMessage(canvas, message);
}, false);

...
function getMousePos(canvas, evt) {
   // WRONG!!!
   return {
      x: evt.clientX,
      y: evt.clientY
   };
}
A good version of the code:

Move the mouse cursor and click anywhere!

And here is the fixed version of the getMousePos function:

1
2
3
4
5
6
7
8
function getMousePos(canvas, evt) {
   // necessary to take into account CSS boundaries
   var rect = canvas.getBoundingClientRect();
   return {
      x: evt.clientX - rect.left,
      y: evt.clientY - rect.top
   };
}

Moving a player with the mouse

This time, we’ve added a mousemove event listener to the canvas in the init function, and reused the trick that you saw in the previous section to get the correct mouse position:

Working example:

Extract from the JavaScript source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var mousePos;

window.onload = function init() {
...
   // create 10 balls
   balls = createBalls(10);

   // add a mousemove event listener to the canvas
   canvas.addEventListener('mousemove', mouseMoved);

   // ready to go !
   mainLoop();
};

function mouseMoved(evt) {
   mousePos = getMousePos(canvas, evt);
}

function getMousePos(canvas, evt) {
   // from the previous section
   var rect = canvas.getBoundingClientRect();
   return {
      x: evt.clientX - rect.left,
      y: evt.clientY - rect.top
   };
}

Line 9 defines a mousemove event listener: the mouseMoved callback function will be called each time the user moves the mouse on the canvas.

The mouseMoved(evt) function uses the trick from the previous section and puts the correct mouse position in the mousePos variable.

With this code, as soon as we move the mouse on top of the canvas, we’ll have this mousePos global variable (line1) that will contain the mouse position (in the form of the mousePos.x and mousePos.y properties).

And here is the new mainLoop function. We added a call to the mousePlayerWithMouse function:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mainLoop() {
    // 1 - clear the canvas
    ctx.clearRect(0, 0, w, h);
    // draw the ball and the player
    drawFilledRectangle(player);
    drawAllBalls(balls);

    // animate the ball that is bouncing all over the walls
    moveAllBalls(balls);
    movePlayerWithMouse();
    // ask for a new animation frame
    requestAnimationFrame(mainLoop);
}

And here is the code of the movePlayerWithMouse function:

1
2
3
4
5
6
function movePlayerWithMouse() {
    if(mousePos !== undefined) {
        player.x = mousePos.x;
        player.y = mousePos.y;
    }
}

If the mouse position is defined, the player’s x and y position will equal to the positions of the mouse pointer.

The mouse position may be undefined if the animation loop started without the mouse cursor being on top of the canvas. Remember that the mainLoop starts as soon as the page is loaded.

Perhaps it’s occurred to you that it might be better to move the player “from its center” instead of from its top left corner. We leave this improvement to you! :-)


Making it a game! Adding collision detection

We have a player that is a rectangle and other objects that are circles. This is cool, as it allows us to find a short function that tests if a circle collides with a rectangle whose sides are aligned to the X-axis and Y-axis. (we implemented this after reading this Thread at StackOverflow):

1
2
3
4
5
6
7
8
9
10
// Collisions between rectangle and circle
function circRectsOverlap(x0, y0, w0, h0, cx, cy, r) {
   var testX=cx;
   var testY=cy;
   if (testX < x0) testX=x0;
   if (testX > (x0+w0)) testX=(x0+w0);
   if (testY < y0) testY=y0;
   if (testY > (y0+h0)) testY=(y0+h0);
   return (((cx-testX)*(cx-testX)+(cy-testY)*(cy-testY))< r*r);
}

Let’s look at our game! This time we’ve added into the loop a collision test between the player and the balls. If the player hits a ball, it’s removed from the ball array. We did this test in the moveBalls function, as we were already testing collisions with walls for each ball in the array. Let’s look at this new version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function moveAllBalls(ballArray) {
    // iterate on all balls in array
    ballArray.forEach(function(b, index) {
        // b is the current ball in the array
        b.x += b.speedX;
        b.y += b.speedY;

        testCollisionBallWithWalls(b);

        testCollisionWithPlayer(b, index);
    });
}

function testCollisionWithPlayer(b, index) {
    if(circRectsOverlap(player.x, player.y,
                        player.width, player.height,
                        b.x, b.y, b.radius)) {
     // we remove the element located at index
     // from the balls array
     // splice: first parameter = starting index
     // second parameter = number of elements to remove
    balls.splice(index, 1);
    }
}

Line 3: Look at the iterator; this time instead of just one parameter (the current element), we’ve added a second optional parameter that will be the index of the current element, starting from zero.

Line 10: for each ball in the array, we will call testCollisionWithPlayer(b, index); that will check if there is a collision between the ball b and the player. We also pass the index of the ball. If a collision occurs, we will have to remove the ball from the array, and for that, we will need its index in the array.

Line 15 is the collision test, and if it is true (collision with the player), then the ball dies and we remove it from the array using the splice method you can use on arrays.

Line 22: here it is, we remove the current ball in the array using balls.splice(position, numberOfElementsToRemove). The positon is given by index, and the number of balls to remove is one.

We’ve also added a function for displaying the number of balls in the array while we are playing. When this number reaches zero, we display “You Win!”:

1
2
3
4
5
6
7
8
9
10
function drawNumberOfBallsAlive(balls) {
    ctx.save();
    ctx.font="30px Arial";
    if(balls.length === 0) {
        ctx.fillText("YOU WIN!", 20, 30);
    } else {
        ctx.fillText(balls.length, 20, 30);
    }
    ctx.restore();
}

This function is called by the mainLoop:

1
2
3
4
5
6
7
8
9
function mainLoop() {
    // 1 - clear the canvas
    ctx.clearRect(0, 0, w, h);
    ...
    drawNumberOfBallsAlive(balls);
    ...
    // ask for a new animation frame
    requestAnimationFrame(mainLoop);
}