"Adding features to our bouncing balls demo" assessment

I didn’t understand much of this task, so I used the source code for the most part.

But one thing is really bothering me cause I can not seem to understand why from the sourcecode.

The counter, how does that work? If i create my own variable at the top of the script and try to increment it at the right places, it just start to count from 0 to infinity. Obviously you might say, but I really can not figure out why the counter in the finished example works, cause to me it looks like they just incrementet 0. I see no reference to the balls array or anything…

Hi @nidomusen, thanks for saying hi! I am sorry to hear that you’ve had difficulty with the bouncing balls demo additions. This is one of the hardest parts of our courses, and you are brave to attempt it. OOJS is difficult to get your head around, so don’t feel bad if you didn’t get it the first time round. Try it a couple more times and it’ll probably make sense eventually.

So, the counter.

We define the count variable here: https://github.com/mdn/learning-area/blob/master/javascript/oojs/assessment/main.js#L4

The count variable is then incremented in this while loop: https://github.com/mdn/learning-area/blob/master/javascript/oojs/assessment/main.js#L188. Every time a new ball object is created and added to the balls array, we also increment the count value by one (count++ is shorthand for count = count + 1). We also update the ball count paragraph to say how many balls are on the field — para.textContent = 'Ball count: ' + count.

Now let’s look at the EvilCircle class’s collisionDetect() method: https://github.com/mdn/learning-area/blob/master/javascript/oojs/assessment/main.js#L157. In here we do the exact opposite of what we did in the while loop. So when an evil circle object instance collides with one of the balls, we set that ball to not existing any more (exists = false) so it is no longer drawn and doesn’t have any more effect on the game, decrement the count variable by one (count--), and again update the text label to show the new count.

Let me know if that helps. More than happy to discuss further.

Hey Chrismills.

Thank alot man! Yeah it did work, turns out i just had increment count outside the loops closing bracket.

Yeah, I’m gonna need to reread alot of the things in this chapter, it is a really confusing subject.
Thanks for the help

You are very welcome. Feel free to ask if you have any more questions.

Hello. I’ve finished this assessment and need a mark. Be kind to check @chrisdavidmills… thank you!

const canvas = document.querySelector(".ballBounce canvas");
const ctx = canvas.getContext("2d");
const bCount = document.querySelector(".ballBounce p");

let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
let count = 0;

// function to generate random number

function random(min, max){
  let num = Math.floor(Math.random()*(max-min)) + min;
  return num;
}
//ADD OF SHAPE()
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;

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;
};

Ball.prototype.collisionDetect = function(){
	for(let j = 0; j < balls.length; j++){
		if(!(this === balls[j])){
			let dx = this.x - balls[j].x;
			let dy = this.y - balls[j].y;
			let 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) + ")";
			}
		}
	}
};

//let's add an EvilCircle() constructor heir of Shape() constructor
function EvilCircle(x, y, velX, velY, 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;

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.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.collisionDetect = function(){
	for(let k = 0; k < balls.length; k++){
		if(balls[k].exists === true){
			let dx = this.x - balls[k].x;
			let dy = this.y - balls[k].y;
			let distance = Math.sqrt(dx * dx + dy * dy);

			if(distance < this.size + balls[k].size){
        	balls[k].exists = false;
       	 	count--;
        	bCount.textContent = "Ball Count : " + count;
			}
		}
	}
};

EvilCircle.prototype.setControls = function(){
  let _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;
    }
  }
};

let evil = new EvilCircle(random(0, width), random(0, height), true);
evil.setControls();

const balls = [];

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

	while(balls.length < 50){
		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,
			"rgb(" + random(0,255) + "," + random(0,255) + "," + random(0,255) +")",
			size,
			);
		balls.push(ball);
    	count++;
    	bCount.textContent = "Ball Count : " + count;
	}

	for(let 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);
}
loop();

@nikhma99 — this looks really good!

I tried it out, and can’t really fault you on much. The example does exactly what it’s supposed to do. Well done!

Hi Chris,

In the assessment, you wrote in the brief some lines which I couldn’t get a grasp of.

  1. "You also need to add a new parameter to the new Ball() ( ... ) constructor call — the exists parameter should be the 5th parameter, and should be given a value of true . " ---- What did you mean by this ??

  2. " Remember to set the Ball() constructor’s prototype and constructor appropriately. " ---- By this line I get it what should be done, but you wrote this
    " Ball.prototype.constructor = Ball; "

Why didn’t you use the " Object.defineProperty " ?
This was clearly mentioned in your articles but " Ball.prototype.constructor = Ball " property was not.

Hi Chris,

I’ll try to respond your questions from how I understood this assesment.

  1. With the introduction of the EvilCircle , we somehow need to keep track of the fact whether a ball is “existant” or not. – that is why we defined the exists property. And we also want to make sure that this property is assigned the value true by default. So at every moment you create a new Ball instance, you pass the value of true as the constructor argument , and the constructor will set this.exists = true of the Ball.
  2. using the assignment operator (=) and Object.defineProperty is pretty much the same. So you can use either interchangeably I suppose. If you agree, you can put a +1 on my pull request https://github.com/mdn/learning-area/pull/131

I get it that ‘exists’ property is needed and it’s value should be ‘true’. That was done when we created a new Object instance of ‘Ball’ constructor and that argument was given the value ‘true’ as a third parameter. So my question is, while defining ‘Ball’ constructor, can we define this.exists = exists =true?
Because by this line:

You also need to add a new parameter to the new Ball() ( ... ) constructor call — the exists parameter should be the 5th parameter, and should be given a value of true .

I can only understand the above thing I mentioned.

Hello, I have just finished the ‘Adding features to our balls demo’, could anyone make some comments of my code? And I also put my answers to the bonus point questions in the code.

/*
my answers
* For a bonus point, let us know which keys the specified keycodes map to.
> 65 maps to A, 68 maps to D, 87 maps to W, and 83 maps to S. (ASCII code)

* For another bonus point, can you tell us why we've had to set let _this = this; in the position it is in? It is something to do with function scope.
> The `this` in `onkeydown` points to a different object than the `this` in `setControls`. (But I do not know which object the `this` in `onkeydown` points to, could you help with me?)
*/

// setup canvas

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

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

let paragraph = document.querySelector('p');
let ballCount = 0;

// function to populate paragraph
function countBall() {
    paragraph.textContent = 'Ball count: ' + ballCount;
}

// function to generate random number

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

// define Shape constructor

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

// define EvilCircle constructor

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);

Object.defineProperty(EvilCircle.prototype, 'constructor', {
    value: EvilCircle,
    enumerable: false,
    writable: false
});

// define evilcircle draw method
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();
}

// define evilcircle checkBounds method
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;
    }
};

// define evilcircle setControls method
EvilCircle.prototype.setControls = function() {
    let _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;
    }
};

// define evilcircle collisionDetect method
EvilCircle.prototype.collisionDetect = function() {
    for (let j = 0; j < balls.length; j++) {
        if (balls[j].exists) {
            let dx = this.x - balls[j].x;
            let dy = this.y - balls[j].y;
            let distance = Math.sqrt(dx * dx + dy * dy);

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

// define Ball constructor

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);
Object.defineProperty(Ball.prototype, 'constructor', {
    value: Ball,
    enumerable: false,
    writable: true
});

// define ball draw method

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

// define ball update method

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;
};

// define ball collision detection

Ball.prototype.collisionDetect = function () {
    for (let j = 0; j < balls.length; j++) {
        if (!(this === balls[j]) && balls[j].exists) {
            let dx = this.x - balls[j].x;
            let dy = this.y - balls[j].y;
            let 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) + ')';
            }
        }
    }
};

// define array to store balls and populate it

let balls = [];

while (balls.length < 25) {
    let size = random(10, 20);
    let ball = new Ball(
        // ball position always drawn at least one ball width
        // away from the adge of the canvas, to avoid drawing errors
        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);
}

// create evil circle
let evilcircle = new EvilCircle(
    random(20, width - 20),
    random(20, height - 20),
    true,
);
evilcircle.setControls();

// define loop that keeps drawing the scene constantly

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

    evilcircle.draw();
    evilcircle.checkBounds();
    evilcircle.collisionDetect();
    ballCount = 0;
    for (let i = 0; i < balls.length; i++) {
        if(balls[i].exists) {
            balls[i].draw();
            balls[i].update();
            balls[i].collisionDetect();
            ++ballCount;
        }
    }
    countBall();
    requestAnimationFrame(loop);
}



loop();

@Tom-Vanderboom Hi there Tom, sorry for taking so long to reply to you on this.

I have tested your code, and looked over the answers, and everything looks very good. Well done!

Thank you for your reply.:wink: (I lost my github account so I have to create a new account to sign in. So this is me.)
But I still can not understand which object the this in onkeydown points to, could you give me a hint or something else?:thinking:

OK, no worries :wink:

So, let’s talk through the this question. We are talking about this block of code:

EvilCircle.prototype.setControls = function() {
    let _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;
    }
};

The problem we have here is that we have a function inside a function (or a function inside a method if you want to nitpick), and this is scoped to the function it is written inside.

So setControls is a function set on the prototype of EvilCircle, therefore this refers to whatever instance of EvilCircle you are creating and using in the code below, i.e. evilcircle, when you do evilcircle.setControls(); later on.

But window.onkeydown = function(e) { ... } also has its own this. If we just called this inside the anonymous function referred to above, this would equal window, and not evilcircle.

We therefore have to set a separate custom variable, _this equal to evilcircle's this, so that inside the window.onkeydown function we can access the properties of evilcircle we want to manipulate, such as x and velX.

Does that make sense? It’s a tricky part of JS.

Thank you very much, and I think I understand it.:smiley: And sorry for taking so long to reply.

@Tom-Vanderboom no worries! Please don’t hesitate to ask more questions if you need more help.

Hello everyone, I’ve just finished the " Creating our new objects" section and now all of the balls have disappeared and all I see is a black screen with the Bouncing Balls header. I believe I’ve followed the steps correctly so far but can someone tell me what I’m doing wrong here’s my code below:

// define shape constructor

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

// define ball constructor

function Ball(x, y, velX, velY, exists, color, size){
Shape.call(this, x, y, velX, velY, exists)

this.size = size;
this.color = color;

}

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

Hi @blairmclaughlin89, and welcome to the community.

I’m sorry to hear you are having trouble with the assessment on the OOJS module. It is one of the trickiest bits of the JavaScript language, so I think you can be forgiven a lot.

I think the best idea here might be for you to go through our final file, and see how it differs from yours? You find it here:

If after looking here you are still confused, maybe you could share your full code somewhere, and I’ll have a good look.

Best regards.

Hi @ericschwartz! This is a good question. Basically the answer is because the EvilCircle.prototype.collisionDetect = function() { ... } block is defining a method that will exist on an instance of the EvilCircle class, when one is instantiated.

But this hasn’t happened yet. This code doesn’t really exist inside the global scope yet, in terms of it actually affecting anything, or being affected by anything.

This doesn’t happen until line 199:

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

If you put line 177 somewhere below line 179, it would cause the code to fail.

1 Like

This is a similar case to the method I asked about, as methods are a kind of function.

Yeah, methods are basically just functions that are available as object members.

I guess it’s all possible because of the way JavaScript is built; I assume other languages will check for the validity the code inside function definitions.

Yeah, other languages have a variety of levels of strictness in terms of this kind of thing. JavaScript is one of the slackest languages in this regard (and in other ways, e.g. dynamic data typing), but it is a strangth as well as a weakness, and there are features available if you want to make your code stricter (e.g. strict mode, and abstractions like TypeScript). I love JS because it gives you a lot of freedom.

Thanks for your help Chris, and thanks for teaching me web development on MDN! I will make companies out of my future apps one day and pay you back!

I am glad to be of help. And no need to pay me back — I just want to empower people and help them learn.

1 Like

Hi everyone! Whew! I finally got this to work.

Please, find my code below. Any feedback on what I should’ve done better is very much appreciated.

// setup canvas

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

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

// Ball count paragraph
let para = document.querySelector('#ballCount');

// function to generate random number

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

// define Shape constructor

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

// define Ball constructor

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;

// define ball draw method

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

// define ball update method

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;
};

// define ball collision detection

Ball.prototype.collisionDetect = function () {
  for (let j = 0; j < balls.length; j++) {
    if (!(this === balls[j])) {
      let dx = this.x - balls[j].x;
      let dy = this.y - balls[j].y;
      let 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) + ')';
      }
    }
  }
};

// define array to store balls and populate it

let balls = [];

let ballCount = 0;

while (balls.length < 30) {
  let size = random(10, 20);
  let ball = new Ball(
    // ball position always drawn at least one ball width
    // away from the adge of the canvas, to avoid drawing errors
    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);
  ballCount++;
  para.innerHTML = `Ball count: ${ballCount}`;
}

// Define the EvilCircle() constructor inherited from Shape()

function EvilCircle(x, y, velX, velY, exists, color, size) {
  Shape.call(this, x, y, 20, 20, exists);

  this.color = 'white';
  this.size = 10;
}

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

// Define the EvilCircle() draw() method
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();
}

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

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

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

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

// Define the EvilCircle() setControls() method
EvilCircle.prototype.setControls = function () {
  let _this = this; // Reference the element responsible for 
  // firing the event handler
  window.onkeydown = function (e) {
    if (e.keyCode === 65) { // keycode 65 maps to 'A' – move left
      _this.x -= _this.velX;
    } else if (e.keyCode === 68) { // keycode 68 maps to 'D' – move right
      _this.x += _this.velX;
    } else if (e.keyCode === 87) { // keycode 87 maps to 'W' – move up
      _this.y -= _this.velY;
    } else if (e.keyCode === 83) { // keycode 68 maps to 'S' – move down
      _this.y += _this.velY;
    }
  }
}

// Define the EvilCircle() collisionDetect() method
EvilCircle.prototype.collisionDetect = function () {
  for (let j = 0; j < balls.length; j++) {
    if (balls[j].exists) {
      let dx = this.x - balls[j].x;
      let dy = this.y - balls[j].y;
      let distance = Math.sqrt(dx * dx + dy * dy);

      if (distance < this.size + balls[j].size) {
        balls[j].exists = false;
        // Decrement the ball count
        ballCount--;
        // Display the updated number of balls
        para.innerHTML = `Ball count: ${ballCount}`;
      }
    }
  }
}

// Create a new evil ball object instance from EvilCircle()

let evilBall = new EvilCircle(random(0, width), random(0, height), true);

// Set evillBall controls once

evilBall.setControls();

// define loop that keeps drawing the scene constantly

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

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

  requestAnimationFrame(loop);
}

loop();