Introducing JavaScript objects: Assessment: Adding features to our bouncing balls demo

Hi Mozillians and @chrisdavidmills,

Having logic trouble re: Adding features to our bouncing balls demo.

Note: I’m trying to figure this out without looking at the solution. So far, I can generate/draw the evil circle on the page; however, when I add the other 3 evil methods (collisionDetect, setControls, checkBounds) the code breaks.

More so, my set up for the exist property does not make working sense. Finally, the counter is saved for last until I can figure out the evilCircle methods.

Please see code in JSFiddle.

Current code with comments below:

// setup canvas
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");

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

// ball counter setup
//var counterText = document.getElementById("#counter");
//counterText.textContent = 0;

// function to generate random number
function random(min, max) {
  var 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 = true;
}

// define Ball constructor
function Ball(x, y, velX, velY, color, size, exists) {
  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's 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's collison detection
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) + ")";
      }
    }
  }
};

// define EvilCircle()
function EvilCircle(x, y, exists) {
  Shape.call(this, x, y, exists);

  this.color = 'white';
  this.size = 50;
  this.x = 400;
  this.y = 100;

  //this.exists = true;
}

// need to better understand the assignments for prototype and Obj.create
EvilCircle.prototype = Object.create(Shape.prototype);
EvilCircle.prototype.constructor = EvilCircle; // why do we need to do this?


// define EvilCircle methods
EvilCircle.prototype.draw = function() {
  //  Ball.prototype.draw.call(this); // can I do this?
  //    ctx.fillStyle = ctx.strokeStyle = this.color;  // and this?

  ctx.beginPath();
  ctx.lineWidth = 10;
  ctx.strokeStyle = 'white';

  ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
  ctx.stroke();

};

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

// setControls()
/*
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;
    }
  }
};
*/

// collisionDetect()
/*
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 = this.exists = false;
      }
    }
  }
};
*/

// define array to store balls
var balls = [];

// define loop that keeps drawing the scene constantly
function loop() {
  ctx.fillStyle = "rgba(0,0,0,0.25)";
  ctx.fillRect(0, 0, width, height);

  // create EvilCircle instance
  var evil = new EvilCircle();
  evil.draw();
  // evil.setControls();

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

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

    //  if (ball !== exist) {
    balls[i].draw();
    balls[i].update();
    balls[i].collisionDetect();
    //  }
  }

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

  requestAnimationFrame(loop);
}

// start animation
loop();
// need to better understand the assignments for prototype and Obj.create
EvilCircle.prototype = Object.create(Shape.prototype);

You have to clone the object (which Object.create does) so that when you modify the EvilCircle.prototype you don’t modify the Shape.prototype. Without the clone EvilCircle and Shape will act as the same object constructor, that is, they have a reference to the exact same prototype object. If that’s not entirely clear say something.

EvilCircle.prototype.constructor = EvilCircle; // why do we need to do this?

You shouldn’t have to?
Edit:
Turns out it has some uses but not always required.

 EvilCircle.prototype.draw = function() {
   //  Ball.prototype.draw.call(this); // can I do this?

You could. But I don’t know why you would. The assignment wants you to draw a different path for the EvilCircle, so I’d just keep them separate. There’s some other things you could do by defining ‘builder’ methods, but that can be discussed in more advanced topics.

// setControls()
/*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;
    }
  }
};*/

Your EvilCircle doesn’t have velX or velY properties. If you look at your constructor:

function EvilCircle(x, y, exists) {
  Shape.call(this, x, y, exists);

You don’t pass in the velX and velY arguments that Shape requires: function Shape(x, y, velX, velY, exists).

function loop() {
...
  var evil = new EvilCircle();
...
}

You recreate your EvilCircle on every loop iteration. Create the EvilCircle outside the function. Further, you should probably move the Ball creation loop outside the function too.

I think this stuff should help get you on the right track.

1 Like

Thanks for jumping in here @Sxderp, much appreciated.

Yeah, making sure it is always wired up correctly seemed like a good step to include, to reduce the risk of people running into unexplainable edge cases.

1 Like