"Adding features to our bouncing balls demo" assessment

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 @chrisdavidmills, 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.

As I followed the assignment I tried to refactor it using ECMA 2015 classes but I can’t get the collusion between the evil ball and the others to work. It just go through it.

I would be appreciated if someone could help me.

// setup canvas

const para = document.querySelector('p');
let count = 0;

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

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

// function to generate random number

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

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

	get exists() {
		return this._exists;
	}

	set exists(newExists) {
		this._exists = newExists;
	}
}

class Ball extends Shape {
	constructor(x, y, velX, velY, color, size, exists) {
		super(x, y, velX, velY, exists);

		this.color = color;
		this._size = size;
	}

	get size() {
		return this._size;
	}

	set size(newSize) {
		this._size = newSize;
	}

	draw() {
		ctx.beginPath();
		ctx.fillStyle = this.color;
		ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
		ctx.fill();
	}
	
	update() {
		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;
	}
	
	collisionDetect() {
		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].exists) {
					balls[j].color = this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';
				}
			}
		}
	}
}

class EvilCircle extends Shape {
	constructor(x, y, exists) {
		super(x, y, exists);

		this.velX = 20;
		this.velY = 20;
		this.color = "white";
		this.size = 10;
	}

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

	checkBounds() {
		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;
		}
	}

	setControls() {
		let _this = this;
		window.onkeydown = function(e) {
			switch(e.keyCode) {
				case 65:
					_this.x -= _this.velX;
					break;
				case 68:
					_this.x += _this.velX;
					break;
				case 87:
					_this.y -= _this.velY;
					break;
				case 83:
					_this.y += _this.velY;
					break;					
			}
		}
	}

	collisionDetect() {
		for (let j = 0; j < balls.length; j++) {
			if (balls[j].exist) {
				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;
					count--;
					para.textContent = 'Ball count: ' + count;
				}
			}
		}
	}
}

let balls = [];

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

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

	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 edge of the canvas, to avoid drawing errors
			random(0 + size, width - size),
			random(0 + size, height - size),
			random(-7, 7),
			random(-7, 7),
			'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')',
			size,
			true
		);
		balls.push(ball);
		count++;
    para.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();
		}
	}

	evilBall.draw();
	evilBall.checkBounds();
	evilBall.collisionDetect();

	requestAnimationFrame(loop);
}

loop();

Hi @Rance,

Thanks so much for publishing a version that uses ES 2015 classes; this is really useful. Are you OK if I include it inside our repo?

I had a good look at this — your problem was a simple typo in the end (aren’t they always! :wink: )

In the EvilCircle class’ collisonDetect() definition, you had the line

if (balls[j].exist) {

Changing it to

if (balls[j].exists) {

Fixed the problem.

1 Like

Thank you very much. That was an embarrassing mistake.

Sure, no problem. I’m glad I can be of any help. I’m a beginner so I made some minor mistakes that can be adjusted. Below there’s a slightly updated version if you haven’t already corrected the first one:

Updated code
// setup canvas

const para = document.querySelector('p');
let count = 0;

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

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

// function to generate random number

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

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

class Ball extends Shape {
	constructor(x, y, velX, velY, color, size, exists) {
		super(x, y, velX, velY, exists);

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

	draw() {
		ctx.beginPath();
		ctx.fillStyle = this.color;
		ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
		ctx.fill();
	}
	
	update() {
		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;
	}
	
	collisionDetect() {
		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].exists) {
					balls[j].color = this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';
				}
			}
		}
	}
}

class EvilCircle extends Shape {
	constructor(x, y, exists) {
		super(x, y, exists);

		this.velX = 20;
		this.velY = 20;
		this.color = "white";
		this.size = 10;
	}

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

	checkBounds() {
		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;
		}
	}

	setControls() {
		const _this = this;
		window.onkeydown = function(e) {
			switch(e.keyCode) {
				case 65:
					_this.x -= _this.velX;
					break;
				case 68:
					_this.x += _this.velX;
					break;
				case 87:
					_this.y -= _this.velY;
					break;
				case 83:
					_this.y += _this.velY;
					break;					
			}
		}
	}

	collisionDetect() {
		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;
					count--;
					para.textContent = 'Ball count: ' + count;
				}
			}
		}
	}
}

const balls = [];

const evilBall = new EvilCircle(			
	random(0, width),
	random(0, height),
	true
);
evilBall.setControls();

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

	while (balls.length < 25) {
		const size = random(10, 20);
		const ball = new Ball(
			// ball position always drawn at least one ball width
			// away from the edge of the canvas, to avoid drawing errors
			random(0 + size, width - size),
			random(0 + size, height - size),
			random(-7, 7),
			random(-7, 7),
			'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')',
			size,
			true
		);
		balls.push(ball);
		count++;
    para.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();
		}
	}

	evilBall.draw();
	evilBall.checkBounds();
	evilBall.collisionDetect();

	requestAnimationFrame(loop);
}

loop();

I removed 2 useless getters/setters that I initially created because I though it could be the issue with the collision.
Some constant variables were using let so I changed them to const.

Thank you very much. That was an embarrassing mistake.

no worries — it happens to us all from time to time.

Thanks so much for publishing a version that uses ES 2015 classes; this is really useful. Are you OK if I include it inside our repo?

Sure, no problem. I’m glad I can be of any help. I’m a beginner so I made some minor mistakes that can be adjusted.

Cool, thanks for the updated code.

I removed 2 useless getters/setters that I initially created because I though it could be the issue with the collision.
Some constant variables were using let so I changed them to const.

OK, perfect! I’ll get this published in the repo soon.

Thank you for sharing!

Hi, I had a problem with the way I created the evilCircle object. Originally I put the creation of the evil circle object inside the loop() function. I used an if-test, (!(theEvilOne instanceof EvilCircle)), so that it would be created only once. It created the evil circle successfully, and drew it OK.

But, the next time loop() executed, my evil circle variable was undefined! (This was confirmed by the Chrome Developer Tools console.) So, my if-test failed, so, the code created a new evil circle; quite wrong.

I moved the code for creation of the evil circle, to before the loop() function, and now it works fine. But I cannot understand why that is necessary, and why, in my earlier version, the variable became undefined the next time loop() ran.

Hi there @ElHombre55! This sounds like it might have something to do with scoping, but I really can’t comment further without seeing the code. For the if test, you could just put if(!theEvilOne) {} surely?

Hi, my code works perfectly fine but i cant see the evil circle. I tried different values for color, size but nothing works.

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

var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
var p = document.querySelector('p');
var count=0;

// function to generate random number

function random(min,max) {
  var num = Math.floor(Math.random()*(max-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;


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 (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) +')';
      }
    }
  }
}



function EvilCircle(x,y,exists){
	Shape.call(this, x,y,20,20,exists);
	this.color='red';
	this.size=50;
}

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


EvilCircle.prototype.draw = function() {
  ctx.beginPath();
  ctx.StrokeStyle = 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==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-=1;
        p.textContent = "Ball Count: "+count;

      }
    }
  }
}


// Array to store all our balls
var balls = [];


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

  var size = random(10,20);
  var evil_circle = new EvilCircle(
  random(0 + size,width - size),
  random(0 + size,height - size),
  true
);

evil_circle.setControls();

  

  while (balls.length < 25) {
  	count+=1;
  	p.textContent = "Ball Count: "+count;
  	var size = random(10,20);
    var ball = new Ball(
      // ball position always drawn at least one ball width
      // away from the edge 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);
  }

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

  	evil_circle.draw();
  	evil_circle.checkBounds();
  	evil_circle.collisionDetect();
  }

  requestAnimationFrame(loop);
}

loop();

Hi @saikrishna7, thanks for writing in.

I had a bit of a look, and there seems to be a number of unexpected things going on here. I’d like to suggest that first you look over our version, and see where yours differs:

Feel free to let me know if you are still having trouble afterwards, and I’ll be glad to dig in.