"Adding features to our bouncing balls demo" assessment

I have reviewed the code and correct should be corrected.
About the controls, i use the Keyboard arrows instead of letters.
So:

  • Arrow Right to move the evil circle Forward

  • Arrow left to move the evil circle Backward

  • Arrow Up to move the evil circle Upward

  • Arrow down to move the evil circle DownWard


This is my reviewed Code:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title> BOUNCING BALLS </title>
<style>
    body {
        margin: 0;
        padding: 0;
        overflow: hidden;
    }

    .counter-container {
        color: #fff;
        position: absolute;
        font-weight: bold;
        top: 25px;
        left: 25px;
    }

</style>
</head>

<body>
<div class="counter-container">Remain balls: <span id="counter"></span></div>
<canvas id="black-board"></canvas>

<script>
    // Get a reference to the canvas and store it into a variable
    var blackBoard = document.querySelector('#black-board');

    // Get a reference to the counter and store it into a variable
    var counter = document.querySelector('#counter');

    // Set a 2d context on the canvas
    var blackBoardContext = blackBoard.getContext('2d');

    // Set the scene width and height
    var blackBoardWidth = blackBoard.width = window.innerWidth;
    var blackBoardHeight = blackBoard.height = window.innerHeight;

    // FDraw the sene and make it fit the window of the browser
    blackBoardContext.fillStyle = 'rgba(0,0,0, .25)';
    blackBoardContext.fillRect(0, 0, blackBoardWidth, blackBoardHeight);

    // Define the random number generator function
    function random(min, max) {
        var num = Math.floor( Math.random() * (max - min + 1 ) ) + min;
        return num;
    }

    // Define a shape Object constructor
    function Shape(coordX, coordY, horizontalVelocity, verticalVelocity, size) {
        // Coordinates of the shape
        this.coordX = coordX;
        this.coordY = coordY;

        // Define a velocity for the shape movement
        this.horizontalVelocity = horizontalVelocity;
        this.verticalVelocity = verticalVelocity;

        //Define the size of the shape
        this.size = size;
    }

    /*
    * Define the ball Object
    * The Ball Object will inherit from the Shape Object
    */
    function Ball(coordX, coordY, horizontalVelocity, verticalVelocity, size, ballColor) {
        // Call the Shape constructor to inherit its properties from it
        Shape.call(this, coordX, coordY, horizontalVelocity, verticalVelocity, size);

        // Define a color for the ball
        this.ballColor = ballColor;

        /*
        * Define an existance for our ball
        * This will help us to make a ball disappear when it will be eaten by the evil circle
        * By default, a ball exists at it creation
        */
        this.exists = true;
    }

    // Define a method that will draw our balls on the game context
    Ball.prototype.drawBall = function () {
        // Create a new Path
        blackBoardContext.beginPath();

        // Set the Fill color of the ball with the ballColor of the current ball
        blackBoardContext.fillStyle = this.ballColor;

        // Design the abstract form of the circle with the current ball datas
        blackBoardContext.arc(this.coordX, this.coordY, this.size, 0, 2 * Math.PI);

        // Fill the cicrcle and make it solid ans visible with the current ball color
        blackBoardContext.fill();
    }

    /*
    * Define a function that will update ball position
    * Though make a ball moves
    */

    Ball.prototype.update = function () {
        if ((this.coordX + this.size) >= blackBoardWidth) {
            this.horizontalVelocity = - (this.horizontalVelocity);
        }

        if ((this.coordX + this.size) <= 0) {
            this.horizontalVelocity = - (this.horizontalVelocity);
        }

        if ((this.coordY + this.size) >= blackBoardHeight) {
            this.verticalVelocity = - (this.verticalVelocity);
        }

        if ((this.coordY + this.size) <= 0) {
            this.verticalVelocity = - (this.verticalVelocity);
        }
        this.coordX += this.horizontalVelocity;
        this.coordY += this.verticalVelocity;
    }

    /*
    * var oneBall = new Ball(250, 250, -7, 7, 50, 'red');
    * *oneBall.drawBall();
    */

    // An array that will store all the balls we are playing with on the game scene
    var balls = [];

    /*
    * Define a methodd to change color of the balls 
    * each time they collide of one another
    */
    Ball.prototype.ballCollisionDetect = function () {
        for (var i = 0; i < balls.length; i++) {

            // Check if the ball is different from the current ball
            if (!(this === balls[i])) {

                // Determine the distance between the two balls
                distanceX = this.coordX - balls[i].coordX;
                distanceY = this.coordY - balls[i].coordY;
                ballsDistance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);

                // Check if the two balls collides
                // And have different colors
                // Change the color of the second into the color of the current ball
                if ((ballsDistance - this.size) <= balls[i].size) {
                    if (this.ballColor !== balls[i].ballColor) {
                        this.ballColor = balls[i].ballColor;
                    } else {
                        this.ballColor = 'rgb('+random(0, 255)+', '+random(0, 255)+', '+random(0, 255)+')';
                    }
                }
            }
        }
    }


    /*
    * Define the EvilCircle Object constructor
    * This Object will inherit from Shape Object
    */
    function EvilCircle(coordX, coordY, horizontalVelocity, verticalVelocity, size, evilStrokeColor) {
        Shape.call(this, coordX, coordY, horizontalVelocity, verticalVelocity, size);

        // Set the Sroke Color of our Evil Circle
        this.evilStrokeColor = evilStrokeColor;
    }

    // Ddefine a method that draws the evil circle
    EvilCircle.prototype.drawEvil = function() {
        blackBoardContext.beginPath();
        blackBoardContext.lineWidth = 2;
        blackBoardContext.strokeStyle = "#fff";
        blackBoardContext.arc(this.coordX, this.coordY, this.size, 0, 2*Math.PI );
        blackBoardContext.stroke();
    }

    // Check the bounds to avoid the evil Circle to traverse the Window
    EvilCircle.prototype.checkBounds = function() {
        if( (this.coordX + this.size) >= blackBoardWidth ) {
            this.coordX = blackBoardWidth + 2*this.horizontalVelocity;
        }

        if( (this.coordX + this.size ) <= 0 ) {
            this.coordX = -2*this.horizontalVelocity;
        }

        if( (this.coordY + this.size ) >= blackBoardHeight ) {
            this.coordY = blackBoardHeight - 2*this.verticalVelocity;
        }

        if( (this.coordY + this.size)  <= 0) {
            this.coordY = 2*this.verticalVelocity;
        }
    }

    // Define a method that allows us control the movement of our evil circle
    EvilCircle.prototype.setControls = function() {
        console.log("I'am in");

        var myEvilCircle = this;
        // ArrowRight -> 39
        // ArrowLeft -> 37
        // ArrowUp -> 38
        // ArrowDown -> 40
        window.onkeydown = function(event) {

            event.preventDefault();
            // Move the Evil Cirlce Backward
            if(event.keyCode === 37 ) {
                myEvilCircle.checkBounds();
                myEvilCircle.coordX += myEvilCircle.horizontalVelocity;
            }

            // Move the Evil Circle forward
            if(event.keyCode === 39 ) {
                myEvilCircle.checkBounds();
                myEvilCircle.coordX -= myEvilCircle.horizontalVelocity;
            }

            // Move the Evil Circle UpWard
            if(event.keyCode === 38 ) {
                myEvilCircle.checkBounds();
                myEvilCircle.coordY -= myEvilCircle.verticalVelocity;
            }

            // Move the Evil Circle downWard
            if(event.keyCode === 40 ) {
                myEvilCircle.checkBounds();
                myEvilCircle.coordY += myEvilCircle.verticalVelocity;
            }
        }
    }


    // Make the  Evil circle eats balls when they collide with it
    EvilCircle.prototype.evilDetectCollision = function() {
        for( var k = 0 ; k < balls.length ; k++ ) {
            if( balls[k].exists ) {
                let distanceEvilBallX = this.coordX - balls[k].coordX;
                let distanceEvilBallY = this.coordY - balls[k].coordY; 
                let distanceEvilBall = Math.sqrt(distanceEvilBallX*distanceEvilBallX + distanceEvilBallY*distanceEvilBallY);

                if((distanceEvilBall - this.size) <= balls[k].size ) {
                    balls[k].exists = false;
                    defaultNumber--;
                }
            }
        }
    }

    // Create an evilCircle Object
    var evilCircle = new EvilCircle(250, 250, -20, 20, 10, '#fff');
    evilCircle.setControls();

    // A default number of balls to draw
    var defaultNumber = 25;

    // Define a loop to create our balls bouncing in the game scene
    function bouncingBallsLoop() {
        blackBoardContext.fillStyle = 'rgba(0,0,0, .25)';
        blackBoardContext.fillRect(0, 0, blackBoardWidth, blackBoardHeight);
        while( balls.length <= defaultNumber - 1 ) {
            // Create a New Ball with random parameters
            var ball = new Ball(
                random(0, blackBoardWidth),
                random(0, blackBoardHeight),
                random(-7, 7),
                random(-7, 7),
                random(5, 20),
                'rgb('+random(0, 255)+', '+random(0, 255)+', '+random(0, 255)+')'
            );
            balls.push(ball);
        }

        // Draw the ball, update it, and detect if collides with another balls
        for( var j = 0 ; j < balls.length ; j++ ) {
            evilCircle.drawEvil();
            evilCircle.evilDetectCollision();

            if(balls[j].exists) {
                balls[j].drawBall();
                balls[j].update();
                balls[j].ballCollisionDetect();
            }
        }

        
        counter.textContent = defaultNumber;
        // Recall the function by itself
        requestAnimationFrame(bouncingBallsLoop);
    }
    bouncingBallsLoop();
    
</script>
</body>

</html>

Cool, corrected code looks good. Also, I didn’t think to look at whether you had used different keys for the controls — looks like we are all good in that regard too.

Nice work!

m// JavaScript source code
// setup canvas

var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');

var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;

var para = document.querySelector('p');

// ballcount variable
var ballCount = 0;

// function to generate random number
function random(min, max) {
    var num = Math.floor(Math.random() * (max - min)) + min;
    return num;
}

// function to display ball count on screen
function displayBallCount() {
    para.textContent = 'Ball Count: ' + ballCount;
}

//Shape constructor function
function Shape(x, y, velX, velY, exists) {
    this.x = x;
    this.y = y;
    this.velX = velX;
    this.velY = velY;
    this.exists = exists;
}

//Ball constructor function
function Ball(x, y, velX, velY, exists, color, size) {
    Shape.call(this, x, y, velX, velY, exists);
    this.color = color;
    this.size = size;
}

// Ball prototype and constructor
Ball.prototype = Object.create(Shape.prototype);
Ball.prototype.constructor = Ball;

//Ball prototype methods
Ball.prototype.draw = function () {
    ctx.beginPath();
    ctx.fillStyle = this.color;
    ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
    ctx.fill();
    displayBallCount();
}

Ball.prototype.update = function () {
    if ((this.x + this.size) >= width) {
        this.velX = -(this.velX);
    }

    if ((this.x - this.size) <= 0) {
        this.velX = -(this.velX);
    }

    if ((this.y + this.size) >= height) {
        this.velY = -(this.velY);
    }

    if ((this.y - this.size) <= 0) {
        this.velY = -(this.velY);
    }

    this.x += this.velX;
    this.y += this.velY;
}

Ball.prototype.collisionDetect = function () {
    for (var j = 0; j < balls.length; j++) {
        if (!(this === balls[j])) {
            var dx = this.x - balls[j].x;
            var dy = this.y - balls[j].y;
            var distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < this.size + balls[j].size) {
                balls[j].color = this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';
            }
        }
    }
}

// EvilCircle constructor function
function EvilCircle(x, y, exists) {
    Shape.call(this, x, y, exists);
    this.color = 'white';
    this.size = 10;
    this.velX = 20;
    this.velY = 20;
}

//EvilCircle prototype and constructor
EvilCircle.prototype = Object.create(Shape.prototype);
EvilCircle.prototype.constructor = EvilCircle;

// EvilCircle prototype methods
EvilCircle.prototype.draw = function () {
    ctx.beginPath();
    ctx.stokeStyle = this.color;
    ctx.lineWidth = 3;
    ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
    ctx.stroke();
}

EvilCircle.prototype.checkBounds = function () {
    if ((this.x + this.size) >= width) {
        this.x -= this.size;
    }

    if ((this.x - this.size) <= 0) {
        this.x += this.size;
    }

    if ((this.y + this.size) >= height) {
        this.y -= this.size;
    }

    if ((this.y - this.size) <= 0) {
        this.y += this.size;
    }

}

EvilCircle.prototype.setControls = function () {
    var _this = this;
    window.onkeydown = function (e) {
        if (e.keyCode === 65) {
            _this.x -= _this.velX;
        } else if (e.keyCode === 68) {
            _this.x += _this.velX;
        } else if (e.keyCode === 87) {
            _this.y -= _this.velY;
        } else if (e.keyCode === 83) {
            _this.y += _this.velY;
        }
    }
}

EvilCircle.prototype.collisionDetect = function () {
    for (var j = 0; j < balls.length; j++) {
        if (balls[j].exists) {
            var dx = this.x - balls[j].x;
            var dy = this.y - balls[j].y;
            var distance = Math.sqrt(dx * dx + dy * dy);
            if (distance < this.size + balls[j].size) {
                balls[j].exists = false;
                ballCount--;
                displayBallCount();
            }
        }
    }
}


var balls = []; // Balls array

var evilCircle = new EvilCircle(random(0, width), random(0, height), true);
evilCircle.setControls();

// function to run program. name: loop
function loop() {
    ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
    ctx.fillRect(0, 0, width, height);


    while (balls.length < 25) {
        var ball = new Ball(
            random(0, width),
            random(0, height),
            random(-7, 7),
            random(-7, 7),
            true,
            'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')',
            random(10, 20)
        );
        balls.push(ball);
        ballCount++;
    }



    for (var i = 0; i < balls.length; i++) {


        if (balls[i].exists) {
            balls[i].draw();
            balls[i].update();
            balls[i].collisionDetect();
        }
    }

    evilCircle.draw();
    evilCircle.checkBounds();
    evilCircle.collisionDetect();

    requestAniationFrame(loop);
}

loop();

HI I completed the bounce tutorial and wanted to get the marking guide, I also wanted to share my code so that I could get it checked for problems and improvements.

cheers

Hi Laurent!

Congratulations on completing the assessment. These are the links you’ll find useful:

Let me know if you have any more questions. Have you got your source code up on a github repo or anything? That would probably be the easiest way to share it.

Hi.

I think there is a mistake in the code in line 97 when we are calling Shape() constructor:
Shape.call(this, x, y, exists);
This way we are essentially calling it like:
Shape.call(this, x, y, exists, undefined, undefined);
Which translates to:
this.x = x;
this.y = y;
this.velX = exists;
this.velY = undefined;
this.exists = undefined;
I think we should just call it like:
Shape.call(this, x, y, 20, 20, exists);
And remove lines 101, 102 in the code.
1 Like

Hi there!
I was finishing the assessment of this section, all exactly as the guidelines established and it was working perfectly, only thing remained was the score paragraph… then I felt like taking a challenge and decided to implement what @chrismills suggested and making the code suitable for 2 players; and I achieved it :rofl:
Here it’s the live preview of the game: Bouncing Balls Multiplayer ( A/W/S/D keys to control player 1; left/right/up/down keys to control player 2). Please try the game, before keep reading.

New Features

  • 2 players mode ( red and green)
  • keep score of both players
  • multi key press, which allows to move each evil circle at the same time pressing different keys and even more amazing, it allows you to move the evil circles in a diagonal path (for example, pressing up + left at the same time)

Code’s Adaptation

Here it’s my JS code.

  • guidelines asked us to " set the prototype and constructor properties correctly" i.e. to add Ball.prototype = Object.create(Shape.prototype) and
    Ball.prototype.constructor = Ball ; but I understood that that was made in order to inherit the methods from the parent constructor (in this case Shape ), but Shape doesn’t have any methods defined. So I found those lines of code without real purpose and didn’t add them.

  • because of the goal to make the game multiplayer, I was forced to solve the problem in a different way than the one that our assessment was based on. I replaced the onkeydown event inside EvilCircle by a couple of listeners to keep track of the keys kept pressed (stored in an array) and put those listeners just before the main loop function.

  • in EvilCircles, I replaced the “setControls” procedure by an “update” procedure, with a similar idea of the “update” in Balls but only fired when keys are kept pressed, inside the main “loop” function.

I added more properties in order to achieve the desired results, but I think it’s enough with what I explained before.

Wow, this is really cool — congratulations!

Even without the multiplayer, the keyboard controls implementation is a clear improvement on my original version. I think it’d be great to include those updates in the master code at some point.

Thank you, chris!!

And feel free to take any portion of the code I wrote, or implement it again or clean it :rofl:.

There’s just one thing I noticed minutes ago when I played it again, the speed of evil ball gets almost as twice as fast when the path is diagonal (square root of 2, to be exact), but that could get fixed with a conditional inside loop that will reduce the speed by that factor when the number of elements in the array is exactly two (number of keys pressed).

Hello!

so for the setControls method, I switched the var _this = this; inside the anonymous function in the method, and the controls wouldn’t work. I assume the reason for placing var _this = this; outside the anonymous function is becuase e is referencing _this, and therefore it has to be defined outside of it and before it is sent to the anonymous function.

is my reasoning correct?

everything else in my code worked fine.

I would like to add that this assessment was quite challenging and fun! Looking forward to more!

Yeah, basically this generally always refers to the scope of the block you are inside. Let’s explain. So we’ve got the following basic setup:

EvilCircle.prototype.setControls = function() {
  var _this = this;
  window.onkeydown = function(e) {
    if(e.keyCode === 65) { // a
      _this.x -= _this.velX;
    } else if(e.keyCode === 68) { // d
      _this.x += _this.velX;
    } else if(e.keyCode === 87) { // w
      _this.y -= _this.velY;
    } else if(e.keyCode === 83) { // s
      _this.y += _this.velY;
    }
  };
};

we want to check the key that is being pressed, and alter the x and y values of the current object to move the position on which it is drawn on the screen. By “the current object”, I mean the object instance that is been created whenever the EvilCircle constructor is used.

Here we are attaching a setControls() method to the constructor’s prototype, which means that every instance of EvilCircle will have a setControls() function available to it, as well as the x and y properties it already has from earlier on.

Inside setControls(), we want this to refer to the object instance as we said. But if we used this inside the window.onkeydown = function(e) {}, it would refer to the window.onkeydown event handler instead — again, this refers to the scope of the current block.

So to make sure we are referring to the right scope, we save a reference to the correct this inside a variable called _this, and use that inside the event handler block. This is a very common pattern that you’ll see when looking at JS object code (although instead of _this, the author may decide to use something else, like that).

Note that arrow functions are different — see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions. They do not have their own this scope, so an arrow function could be used to get around this problem in a different way.

I’ve not yet discussed arrow functions in the course, but hope to update it soon to include some material on them.

I hope this helps, anyhow!

1 Like

That’s exactly how I did it and was surprised to find the live sample to have this wrong call function.

Can anyone explain if we are missing something here?

Yeah, I think you are right. I’ve updated the code and the marking guide. Thanks!

This was an great example! I truly enjoyed it.

It is a good exercise for me to modularize it using requireJS when I dive into it.

Hi @chrismills, I have a small contribution to the code for fixing an “error” with the balls drawing. Sometimes, one ball remains bouncing itself around an edge (any edge) and it seams like shaking. That happens beacause the size property is bigger than the X or Y start position and the velocity X or Y is too small to get out the ball of the edge (Ball.size > Ball.x + Ball.velX or Ball.size > Ball.y + Ball.velY). So, the new code is like this:

function loop() {
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.fillRect(0,0,width,height);

  var size = 0; // Variable to store the random size
  while(balls.length < 25) {
    size = random(10,20);
    var ball = new Ball(
      random(0 + size,width - size),
      random(0 + size,height - size),
      random(-7,7),
      random(-7,7),
      true,
      'rgb(' + random(0,255) + ',' + random(0,255) + ',' + random(0,255) +')',
      **size**
    );
    balls.push(ball);
    count++;
    para.textContent = 'Ball count: ' + count;
  }

  for(var i = 0; i < balls.length; i++) {
    if(balls[i].exists) {
      balls[i].draw();
      balls[i].update();
      balls[i].collisionDetect();
    }
  }

  evil.draw();
  evil.checkBounds();
  evil.collisionDetect();

  requestAnimationFrame(loop);
}

With this fix the ball is always drawing inside the canvas area.

Hey, thanks a lot for this! I like the suggestion - it seems to work well. I’ve updated the articles and source code to include it!

Greetings!

I’ve completed this assessments and checked my codes against the official one, but I still have one problem.

After the evil circle ate the balls and they disappeared, the balls should be treated as non-exist, which means if another balls happen to collide with the already-non-exist balls, the still-exist balls shouldn’t have changed colors, do I get this right?
But mine still change colors, so from what I can see, it looks like the balls change color even though it doesn’t collide with anything.

If I understand this right, this code:

function loop() {
 ...

  for (var i = 0; i < balls.length; i++) {
      if (balls[i].exists) {
          balls[i].draw();
          balls[i].update();
          balls[i].collisionDetect();
      }
  }

  ...
}

should’ve taken care of it, right?

But since it doesn’t, I changed a little bit of my code to this

Ball.prototype.collisionDetect = function () {
    for (...) {
        if (...) {
            ...

            if (distance < this.size + balls[j].size && balls[j].exists) {
                ...
            }
        }
    }
};

and it works for me.

I’m really new to this, so please tell me if I understand this wrong or if I did something wrong!

This looks like it makes sense! I’ve added it to the code for the demo in the main repo.

Hi Chris, I was wondering if you could help me understand something. I just can’t seem to understand how I am to know that when creating a new EvilCircle instance, the properties have to be the ones that you have in your example code which are (random(0,width), random(0,height), true);. What is the logic behind such a choice? I am sure it is not difficult to understand but I just can’t seem to wrap my mind around it. I would appreciate your help. Thanks.

So, when we initially define the EvilCircle() constructor function, the parameters we define it as having are x, y, and exists:

function EvilCircle(x, y, exists) { ... }

So, it’s x and y positions inside the viewport, and whether it exists or not.

When we then come to create an instance of this object, we invoke the constructor function, wirth the following three parameters:

var evil = new EvilCircle(random(0,width), random(0,height), true);

We are defining x as a random number of between 0, and the width of the viewport, and y as a random number between 0 and viewport height. So it can appear literally anywhere on the screen, decided at random.

Exists is always true initially, so probably could have just been set automatically inside the constructor, but this at least gives us that flexibility later on, if we want to create an invisible evil circle later on.

Let me know if that helps, or if you’ve got more followup questions.