"Adding features to our bouncing balls demo" assessment

Hi @samuel — thanks for sending this in, and congratulations, this looks like it works perfectly. Your code looks good, and you’ve used updated let keywords instead of var, which is great.

The only thing I’d say about that is that you could probably use const instead of let for declaring variables that are never going to change value, like canvas or ctx. But that’s a relatively minor point compared to everything else.

This is one of the hardest assessments we’ve got, so you should feel proud.

Hi everyone,
everything works fine with my code, except the evil.collisionDetect function.
When a ball touch the evil nothing happend.
Pease find my code below, any help or suggestion will be welcome!!
I spent the whole last night to try to find my error…unsuccessfull!!
(sorry for my english).
My 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 compteur = document.querySelector (‘p’);
var count = 0;

// function to generate random number

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

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

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 = Object.create(Shape.prototype);
Ball.prototype.constructor = Ball;

function EvilCircle(x, y, exists){
Shape.call(this, x, y, 20, 20, exists);
this.color = ‘white’;
this.size = 10;
}

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

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

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;

}

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

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

if ((this.y + this.size) >= height) {
this.y = (height - 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 === 37) {
_this.x -= _this.velX;
} else if (e.keyCode === 39) {
_this.x += _this.velX;
} else if (e.keyCode === 38) {
_this.y -= _this.velY;
} else if (e.keyCode === 40) {
_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.prototype.collisionDetect = function() {
for (var j = 0; j < balls.length; j++) {
if (balls[j].exists = true) {
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;
    count--;
  }
}

}
};

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

var balls = [];

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

function loop() {
ctx.fillStyle = ‘rgba(0, 0, 0, 0.5)’;
ctx.fillRect(0, 0, width, height);

while (balls.length < 15) {
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);
count++;
};

viseur.draw();
viseur.collisionDetect();
viseur.checkBounds();

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

requestAnimationFrame(loop);
}

loop();

compteur.textContent += (count);

Hi @tahar.bouhadida!

I have had a quick look, and I’m not immediately sure what si wrong. All the differnt components you need seem to be there.

Our version of the code is here:

Try looking at it and seeing how it differs from yours, and if you can trakc down the problem.

If not, let me know and I’ll have another look.

Hi Chrismills,
and thank you for you answer.
I compared my code with the solution you showed me and everything seems to be correct, except the order I declared the prototypes, but I think it doesn’t have any impact (??).
I have also some change to made on the balls count display, but it have nothing to do with collisionDetect.
I would be very gratefull if you can help me finding the wrong thing I did.
Best regards.

Awesome assessment! I really enjoyed it. Would have loved if the balls could bounce off each other, but I guess it’s really too complicated.

I found a small detail, that wasn’t described in the steps. The collissionDetect function of the balls currently still detects collisions with non-existing balls. This can be fixed by changing the condition in the if statement to if (!(this === balls[j]) && balls[j].exists) so the ball changes only color if it hits an existing ball.

And one question. I wanted to replace the window.onclick = function(e) {...}; event handler by window.addEventListener('onclick', function(e) {...}); to be able to later delete this event handler when the game finishes, so the user can’t move the evil circle anymore. But it won’t work with the addEventListener method. I also tried it on the document instead of on the window without any luck. My code is otherwise similar to the solution code.

@gerfolder cool, I’m glad you enjoyed the assessment. It wouldn’t be too hard to program balls that bounce off one another rather than change color; you could just do something similar to the code that makes them bounce off the edges of the screen. Making it work realistically in most situations would be the real challenge!

I decided to keep it simpler with the color change code, as I thought it was getting involved enough already, plus I thought the color change idea was kind cute.

About the collisionDetect() function of the balls — I suspect we solved this in a slightly different way. My function looks like this:

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].exists) {
        balls[j].color = this.color = 'rgb(' + random(0,255) + ',' + random(0,255) + ',' + random(0,255) +')';
      }
    }
  }
};

So I am detecting to make sure we are not trying to detect a collision between the ball and itself, then I am detecting the collision but also detecting whether the ball exists in a separate if statement. It might make more sense to move the exists bit up to the first if statement…?

As for the event listener code, I wonder if you are finding a problem with this because of function scope - the keydown handler is being set inside the setControls() method, so it won’t exist outside of that function? If you defined the keydown handler in the global scope, then referenced it from there, would that work?

@chrisdavidmills Thanks, I will try to implement the bouncing balls. I am just thinking how to figure out the direction to which the balls bounce of. Because with the stationary wall it was easy to just reverse the velocity of a ball, but with two balls bouncing in each other the direction is always different.

Regarding the collisionDetect() function: Ups, I didn’t spot the && balls[j].exists in the second if statement in the solution code. I put it in the first. I think it makes more sense to put it in the first, because if a ball doesn’t exists and is hidden it makes no sense to compute the distances anymore and one could save a bit on computation.

I tried putting the event handler outside, but it didn’t work. Also in my answer above I mistakenly wrote ‘onclick’ when it should be ‘onkeydown’. I can’t edit it anymore but I have been using ‘onkeydown’ in my code, so that is not the problem.

EDIT: Ok, I found the relevant math and it’s actually fairly easy. If you assume they all have the same mass they just exchange their velocities upon collision. I just replaced the line

balls[j].color = this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';

with

const tmpVelX = this.velX;
const tmpVelY = this.velY;
this.velX = balls[j].velX;
this.velY = balls[j].velY;
balls[j].velX = tmpVelX;
balls[j].velY = tmpVelY;

But it’s not perfect though. Sometimes they remain stuck together after a collision and also if they happen to be generated on top of each other they will be stuck together. And if they collide close to the wall they may push each other inside the wall and then get stuck there. I suppose the velocities don’t get updated fast enough because of some computation inefficiencies or slow animation rate.

Assessment wanted for Adding bouncing balls features.

Hi Chris,
I have just finished the assessment, could you evaluate or make some comments of my code.
Here is the link to my pen

@beck hi there!

I’ve looked over your code and functionality, and this is doing everything that it is meant to do. The code looks pretty good too. Well done on some great work!

@chrisdavidmills

Hi, Chris, can you have a look at my code.

https://codepen.io/vddroid/pen/ExVXBmG?editors=1010

Sorry for my English.
This challenge is quite hard for me, so I looked up most of the answers. However, now I can understand how to create this ball game. I have included a second player as well.
Is this the correct way of adding a second player into the game?
For player 1 I have to use window.onkeypress and window.onkeydown for player 2 to get the controls to work. Why doesn’t the controls work for both players if I use window.onkeydown for both players? What if I want to add a third or fourth player? Thank you.

Hi @vddroid!

Thanks for sending in your code! This looks great, and I love how you’ve added a second player — nice work!

The reason for the problem you’ve had with the event handlers is that if you write two window.onkeydown event handlers, the second one with overwrite the first one.

The best way to do this would be to use addEventListener() instead. Using this you can multiple event listeners of the same type of the same object. You could do something like

window.addEventListener('onkeydown', playerOneHandler);
window.addEventListener('onkeydown', playerTwoHandler);
window.addEventListener('onkeydown', playerThreeHandler);

See https://wiki.developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#addEventListener_and_removeEventListener for more info.

Another option would be to only have a single event handler function, and handle all the controls for all players inside it.

Hi @chrisdavidmills, thanks for your guide.

I added event listener to the code. It’s all working correctly. I have to change ‘onkeydown’ to ‘keydown’ for addEventListener in order for it to work.

https://codepen.io/vddroid/pen/eYpeKRY?editors=1010

Great work — thanks for sharing it with me!

Hello i just completed the " Adding features to our bouncing balls demo " assessment and would appreciate a marking/any feedback. Thanks.

//设置画布

const canvas = document.querySelector('canvas');

const ctx = canvas.getContext('2d');

const width = canvas.width = window.innerWidth;

const height = canvas.height = window.innerHeight;

const num = document.getElementById('num');

let ballsNum = 25;

// 生成随机数的函数

function random(min, max) {

 const num = Math.floor(Math.random() * (max - min)) + min;

 return num;

}

function randomColor() {

 return 'rgb(' +

   random(0, 255) + ', ' +

   random(0, 255) + ', ' +

   random(0, 255) + ')';

}

function Shape(x, y, velX, velY, exists) {

 this.x = x;

 this.y = y;

 this.velX = velX;

 this.velY = velY;

 this.exists = exists;

}

function Ball(x, y, vulX, vulY, exists, color, size) {

 Shape.call(this, x, y, vulX, vulY, exists);

 this.color = color;

 this.size = size;

}

Ball.prototype = Object.create(Shape.prototype);

Ball.prototype.constructo = Ball;

Ball.prototype.draw = function () {

 ctx.beginPath();//在画布上画一个图形

 ctx.fillStyle = this.color;//定义图形颜色,

 ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);//绘制圆弧,xy是圆弧的中心坐标(相对画布).圆弧的半径(size).0圆弧的起点位置,最后就是360度

 ctx.fill();//写入画布

}

/**

* 更新小球位置

* 

* 检查小球是否碰到画布边缘,碰到就反方向以移动.

*/

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 (let j = 0; j < balls.length; j++) {

   if (this !== balls[j]) {

     const dx = this.x - balls[j].x;

     const dy = this.y - balls[j].y;

     const distance = Math.sqrt(dx * dx + dy * dy);

     if (distance < this.size + balls[j].size) {

       balls[j].color = this.color = randomColor();

     }

   }

 }

}

let balls = [];

while (balls.length <= 25) {

 let size = random(10, 20);

 let ball = new Ball(

   // 为避免绘制错误,球至少离画布边缘球本身一倍宽度的距离

   random(0 + size, width - size),

   random(0 + size, height - size),

   random(-7, 7),

   random(-7, 7),

   true,

   randomColor(),

   size,

 );

 balls.push(ball);

}

function loop() {

 ctx.fillStyle = 'rgba(0,0,0,.25)';

 ctx.fillRect(0, 0, width, height);

 Evil1.draw();

 Evil1.checkBounds();

 Evil1.collisionDetect();

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

   if (balls[i].exists == true) {

     balls[i].draw();

     balls[i].update();

     balls[i].collisionDetect();

   }

   

 }

 requestAnimationFrame(loop);

}

function EvilCircle(x, y, velX = 20, velY = 20, exists, color = 'white', size = 10) {

 Shape.call(this, x, y, velX, velY, exists);

 this.color = color;

 this.size = size;

}

EvilCircle.prototype = Object(Shape.prototype);

EvilCircle.prototype.constructo = EvilCircle;

EvilCircle.prototype.draw = function () {

 ctx.beginPath();//画一个图形

 ctx.lineWidth = 3;

 ctx.strokeStyle = this.color;

 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.x - this.size*2;

 }

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

   this.x = this.x + this.size*2;

 }

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

   this.y = this.y - this.size*2;

 }

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

   this.y = this.y + this.size*2;

 }

}

EvilCircle.prototype.setControls = function () {

 window.onkeydown = e => {

   switch (e.key) {

     case 'a':

       this.x -= this.velX;

       break;

     case 'd':

       this.x += this.velX;

       break;

     case 'w':

       this.y -= this.velY;

       break;

     case 's':

       this.y += this.velY;

       break;

   }

 }

}

EvilCircle.prototype.collisionDetect = function () {

 for (let j = 0; j < balls.length; j++) {

   if (balls[j].exists) { 

     const dx = this.x - balls[j].x;

     const dy = this.y - balls[j].y;

     const distance = Math.sqrt(dx * dx + dy * dy);

     if (distance < this.size + balls[j].size) {

       balls[j].exists = false;

       setNum(ballsNum--);

     }

   }

 }

}

let Evil1 = new EvilCircle(

 random(10, 200),

 random(10, 200),

 20,

 20,

 true

);

Evil1.setControls();

function setNum(ballsNum) {

 if (ballsNum <= 0) {

   num.innerHTML = 'Game Over';

 } else {

   num.innerHTML = ballsNum;

 }

}

loop();

Hi, is there a need to add:
EvilCircle.prototype = Object.create(Shape.prototype);
Object.defineProperty(EvilCircle.prototype, ‘constructor’, {
value: EvilCircle
});

or
Ball.prototype = Object.create(Shape.prototype);

Object.defineProperty(Ball.prototype, ‘constructor’, {

value: Ball

});

yet we are not adding any methods in the prototype of the parent class Shape?