JavaScript/Exercises/Collisions



Speed

edit

When objects move around, they do this at a certain speed. Therefore you should add two properties to the object's class representing its speed in x-direction and in y-direction. Positive values represent the direction to the right resp. bottom, and negative to the left resp. top. Furthermore, the class needs functions to modify the speed.

Collision - 1

edit

Objects can collide with other objects or with the border of the canvas. Algorithms to detect such collisions depend on the object's type: concerning the x-direction for rectangles, the left side is given by the starting point, and for circles, the left side must be computed from the central point and the radius. A collision with the canvas' border is a collision 'from inner to outer', a collision between objects is always 'between outer and outer'.

Nevertheless, it's possible to develop generic algorithms that solve many of the 'collision' problems. Every single 2-dimensional object and every group of such objects can be surrounded by their minimum bounding box (MBB), which is a rectangle by definition. Hence the collision of objects can be solved by an algorithm that detects the collision of their MBBs, at least in the first approximation. It is not absolutely exact in all cases, but for our examples, it should be sufficient.

Constant speed

edit

We create a ball (circle) and let him move within the canvas.

  • As usual, the function playTheGame contains the 'logic' of the game. It is straightforward: move the ball according to it's 'speed'. The 'speed' is the number of pixels by which it should advance.
  • A function detectBorderCollision checks whether the border of the canvas is touched by the current step. If that is the case, the speed is reversed to the opposite direction.
  • detectBorderCollision checks whether the border of a rectangle is touched by a rectangle that moves within its inner space. This is different from the case where two rectangles collide like two cars.
  • The 'outer' rectangle is the canvas itself. We use its properties as the four first arguments of the function call.
  • The ball inside the canvas is not a rectangle; it's a circle. We 'compute' the MBB of the circle and use the properties of the MBB as the four last arguments of the function call. (For this algorithm, the MBB delivers not only an approximation of the problem, it's an exact solution.)
Click to see solution
<!DOCTYPE html>
<html>
<head>
  <title>SPEED 1</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------------
  // class 'Circle' (should be implemented in a separate file 'circle.js')
  // ---------------------------------------------------------------------
  class Circle {
    constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
      this.ctx = ctx;
      // position of the center
      this.x = x;
      this.y = y;
      this.radius = radius;

      // movement
      this.speedX = 0;
      this.speedY = 0;

      this.color = color;
    }

    // render the circle
    render() {
      this.ctx.beginPath(); // restart colors and lines
      this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
      this.ctx.fillStyle = this.color;
      this.ctx.fill();
    }

    // set the speed (= step size)
    setSpeed(x, y) {
      this.speedX = x;
      this.speedY = y;
    }
    setSpeedX(x) {
      this.speedX = x;
    }
    setSpeedY(y) {
      this.speedY = y;
    }

    // change the position according to the speed
    move() {
      this.x += this.speedX;
      this.y += this.speedY;
    }
  }   // end of class 'circle'


  // ----------------------------------------------------
  // variables which are known in the complete file
  // ----------------------------------------------------
  let ball;           // an instance of class 'circle'
  let stop = false;   // indication
  let requestId;      // ID of animation frame


  // ----------------------------------------------------
  // functions
  // ----------------------------------------------------

  // initialize all objects, variables, .. of the game
  function start() {
    // provide canvas and context
    const canvas = document.getElementById("canvas");
    const context = canvas.getContext("2d");

    // create a circle at a certain position
    ball = new Circle(context, 400, 100, 40, 'lime');
    ball.setSpeedX(2);  // 45° towards upper right corner
    ball.setSpeedY(-2); // 45° towards upper right corner

    // adjust the buttons
    document.getElementById("stop").disabled = false;
    document.getElementById("reset").disabled = true;

    // start the game
    stop = false;
    playTheGame(canvas, context);
  }

  // the game's logic
  function playTheGame(canvas, context) {

    // move the ball according to its speed
    ball.move();

    // if we detect a collision with a border, the speed
    // keeps constant but the direction reverses
    const [crashL, crashR, crashT, crashB] = 
      detectBorderCollision(
        0, 0, canvas.width, canvas.height,
        ball.x - ball.radius, ball.y - ball.radius,
        2 * ball.radius, 2 * ball.radius
     );
    if (crashL || crashR) {ball.speedX = -ball.speedX};
    if (crashT || crashB) {ball.speedY = -ball.speedY};

    renderAll(canvas, context);
  }

  // rendering consists off:
  //   - clear the complete screen
  //   - re-paint the complete screen
  //   - call the game's logic again via requestAnimationFrame()
 function renderAll(canvas, context) {

    // remove every old drawing from the canvas (before re-rendering)
    context.clearRect(0, 0, canvas.width, canvas.height);

    // draw the sceen
    ball.render();

    if (stop) {
      // if the old animation is still running, it must be canceled
      cancelAnimationFrame(requestId);
      // no call to 'requestAnimationFrame'. The loop terminates.
    } else {
      // re-start the game's logic, which lastly leads to 
      // a rendering of the canvas
      requestId = window.requestAnimationFrame(() => playTheGame(canvas, context)); 
    }
  }

  // terminate the rendering by setting a boolean flag
  function stopEvent() {
    stop = true;
    document.getElementById("stop").disabled = true;
    document.getElementById("reset").disabled = false;
  }


  // -------------------------------------------------------
  // helper function (can be in a separate file: 'tools.js')
  // -------------------------------------------------------

  function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
                                  rectX,    rectY,    rectWidth,    rectHeight)
  {

    // the rectangle touches the (outer) boarder, if x <= borderX, ...
    let collisionLeft   = false;
    let collisionRight  = false;
    let collisionTop    = false;
    let collisionBottom = false;

    if (rectX              <= boarderX                ) {collisionLeft  = true}
    if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
    if (rectY              <= boarderY                ) {collisionTop   = true}
    if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

    return [collisionLeft, collisionRight, collisionTop, collisionBottom];
  }
  </script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Moving ball</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <p></p>
  <button id="reset" onClick="start()" >Reset</button>
  <button id="stop"  onClick="stopEvent()" >Stop</button>

</body>
</html>

Changing speed

edit

The example is identical to the above one with the additional feature of changing the speed of the ball. It adds two HTML elements input type="range" as a slider. The sliders indicate the intended speed in x- and y- directions.

Click to see solution
<!DOCTYPE html>
<html>
<head>
  <title>SPEED 2</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------------
  // class 'Circle' (should be implemented in a separate file 'circle.js')
  // ---------------------------------------------------------------------
  class Circle {
    constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
      this.ctx = ctx;
      // position of the center
      this.x = x;
      this.y = y;
      this.radius = radius;

      // movement
      this.speedX = 0;
      this.speedY = 0;

      this.color = color;
    }

    // render the circle
    render() {
      this.ctx.beginPath(); // restart colors and lines
      this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
      this.ctx.fillStyle = this.color;
      this.ctx.fill();
    }

    // set the speed (= step size)
    setSpeed(x, y) {
      this.speedX = x;
      this.speedY = y;
    }
    setSpeedX(x) {
      this.speedX = x;
    }
    setSpeedY(y) {
      this.speedY = y;
    }

    // change the position according to the speed
    move() {
      this.x += this.speedX;
      this.y += this.speedY;
    }
  }   // end of class 'circle'



  // ----------------------------------------------------
  // variables which are known in the complete file
  // ----------------------------------------------------
  let ball;           // an instance of class 'circle'
  let stop = false;   // indication
  let requestId;      // ID of animation frame


  // ----------------------------------------------------
  // functions
  // ----------------------------------------------------

  // initialize all objects, variables, .. of the game
  function start() {
    // provide canvas and context
    const canvas = document.getElementById("canvas");
    const context = canvas.getContext("2d");

    // create a circle at a certain position
    ball = new Circle(context, 400, 100, 40, 'lime');
    ball.setSpeedX(2);  // 45° towards upper right corner
    ball.setSpeedY(-2); // 45° towards upper right corner

    // adjust the slider
    document.getElementById("sliderSpeedX").value = 2;
    document.getElementById("sliderSpeedY").value = 2;

    // adjust the buttons
    document.getElementById("stop").disabled = false;
    document.getElementById("reset").disabled = true;

    // start the game
    stop = false;
    playTheGame(canvas, context);
  }

  // the game's logic
  function playTheGame(canvas, context) {

    // move the ball according to its speed
    ball.move();

    // if we detect a collision with a border, the speed
    // keeps constant but the direction reverses
    const [crashL, crashR, crashT, crashB] = 
      detectBorderCollision(
        0, 0, canvas.width, canvas.height,
        ball.x - ball.radius, ball.y - ball.radius,
        2 * ball.radius, 2 * ball.radius
     );
    if (crashL || crashR) {ball.speedX = -ball.speedX};
    if (crashT || crashB) {ball.speedY = -ball.speedY};

    renderAll(canvas, context);
  }

  // rendering consists off:
  //   - clear the complete screen
  //   - re-paint the complete screen
  //   - call the game's logic again via requestAnimationFrame()
 function renderAll(canvas, context) {

    // remove every old drawing from the canvas (before re-rendering)
    context.clearRect(0, 0, canvas.width, canvas.height);

    // draw the sceen
    ball.render();

    if (stop) {
      // if the old animation is still running, it must be canceled
      cancelAnimationFrame(requestId);
      // no call to 'requestAnimationFrame'. The loop terminates.
    } else {
      // re-start the game's logic, which lastly leads to 
      // a rendering of the canvas
      requestId = window.requestAnimationFrame(() => playTheGame(canvas, context)); 
    }
  }

  // terminate the rendering by setting a boolean flag
  function stopEvent() {
    stop = true;
    document.getElementById("stop").disabled = true;
    document.getElementById("reset").disabled = false;
  }

  function speedEventX(event) {
    // read the slider's value and change speed
    const value = event.srcElement.value;
    ball.setSpeedX(parseFloat(value));
  }
  function speedEventY(event) {
    // read the slider's value and change speed
    const value = event.srcElement.value;
    ball.setSpeedY(parseFloat(value));
  }


  // -------------------------------------------------------
  // helper function (can be in a separate file: 'tools.js')
  // -------------------------------------------------------

  function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
                                  rectX,    rectY,    rectWidth,    rectHeight)
  {

    // the rectangle touches the (outer) boarder, if x <= borderX, ...
    let collisionLeft   = false;
    let collisionRight  = false;
    let collisionTop    = false;
    let collisionBottom = false;

    if (rectX              <= boarderX                ) {collisionLeft  = true}
    if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
    if (rectY              <= boarderY                ) {collisionTop   = true}
    if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

    return [collisionLeft, collisionRight, collisionTop, collisionBottom];
  }
  </script>

</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Moving ball</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <p></p>
  <button id="reset" onClick="start()" >Reset</button>
  <button id="stop"  onClick="stopEvent()" >Stop</button>

  <!-- sliders to indicate speed  -->
  <div>
    <input type="range" id="sliderSpeedX" name="sliderSpeedX" min="1" max="10"
           step=".1" onchange="speedEventX(event)">
    <label for="sliderSpeedX">Speed X</label>
  </div>
  <div>
    <input type="range" id="sliderSpeedY" name="sliderSpeedY" min="1" max="10"
           step=".1" onchange="speedEventY(event)">
    <label for="sliderSpeedY">Speed Y</label>
  </div>

</body>
</html>

Collision - 2

edit

The example is identical to the above one with the additional feature of detecting an obstacle (rectangle). If the ball collides with the obstacle, the game stops.

The collision is detected by the function detectRectangleCollision. It compares two rectangles. We use the MBB of the circle as the second parameter, which leads to a slight inaccuracy.

Click to see solution
<!DOCTYPE html>
<html>
<head>
  <title>Collision 2</title>
  <script>
  "use strict";

  // ---------------------------------------------------------------------
  // class 'Circle' (should be implemented in a separate file 'circle.js')
  // ---------------------------------------------------------------------
  class Circle {
    constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
      this.ctx = ctx;
      // position of the center
      this.x = x;
      this.y = y;
      this.radius = radius;

      // movement
      this.speedX = 0;
      this.speedY = 0;

      this.color = color;
    }

    // render the circle
    render() {
      this.ctx.beginPath(); // restart colors and lines
      this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
      this.ctx.fillStyle = this.color;
      this.ctx.fill();
    }

    // set the speed (= step size)
    setSpeed(x, y) {
      this.speedX = x;
      this.speedY = y;
    }
    setSpeedX(x) {
      this.speedX = x;
    }
    setSpeedY(y) {
      this.speedY = y;
    }

    // change the position according to the speed
    move() {
      this.x += this.speedX;
      this.y += this.speedY;
    }
  }   // end of class 'circle'



  // ----------------------------------------------------
  // variables which are known in the complete file
  // ----------------------------------------------------
  let ball;           // an instance of class 'circle'
  let stop = false;   // indication
  let requestId;      // ID of animation frame


  // ----------------------------------------------------
  // functions
  // ----------------------------------------------------

  // initialize all objects, variables, .. of the game
  function start() {
    // provide canvas and context
    const canvas = document.getElementById("canvas");
    const context = canvas.getContext("2d");

    // create a circle at a certain position
    ball = new Circle(context, 400, 100, 40, 'lime');
    ball.setSpeedX(2);  // 45° towards upper right corner
    ball.setSpeedY(-2); // 45° towards upper right corner

    // adjust the slider
    document.getElementById("sliderSpeedX").value = 2;
    document.getElementById("sliderSpeedY").value = 2;

    // adjust the buttons
    document.getElementById("stop").disabled = false;
    document.getElementById("reset").disabled = true;

    // start the game
    stop = false;
    playTheGame(canvas, context);
  }

  // the game's logic
  function playTheGame(canvas, context) {

    // move the ball according to its speed
    ball.move();

    // if we detect a collision with a border, the speed
    // keeps constant but the direction reverses
    const [crashL, crashR, crashT, crashB] = 
      detectBorderCollision(
        // the MBB of the canvas
        0, 0, canvas.width, canvas.height,
        // the MBB of the circle
        ball.x - ball.radius, ball.y - ball.radius,
        2 * ball.radius, 2 * ball.radius
     );
    if (crashL || crashR) {ball.speedX = -ball.speedX};
    if (crashT || crashB) {ball.speedY = -ball.speedY};

    // if we detect a collision with the 'obstacle' the game stops
    const collision = detectRectangleCollision(
           // the MBB of the obstacle
           330, 130, 30, 30,
           // the MBB of the circle
           ball.x - ball.radius, ball.y - ball.radius,
           2 * ball.radius, 2 * ball.radius)
    if (collision) {
      stopEvent();
    }

    renderAll(canvas, context);
  }

  // rendering consists off:
  //   - clear the complete screen
  //   - re-paint the complete screen
  //   - call the game's logic again via requestAnimationFrame()
 function renderAll(canvas, context) {

    // remove every old drawing from the canvas (before re-rendering)
    context.clearRect(0, 0, canvas.width, canvas.height);

    // draw the scene: 'obstacle' plus ball
    context.fillStyle = "red";
    context.fillRect(330, 130, 30, 30);
    ball.render();

    if (stop) {
      // if the old animation is still running, it must be canceled
      cancelAnimationFrame(requestId);
      // no call to 'requestAnimationFrame'. The loop terminates.
    } else {
      // re-start the game's logic, which lastly leads to 
      // a rendering of the canvas
      requestId = window.requestAnimationFrame(() => playTheGame(canvas, context)); 
    }
  }

  // terminate the rendering by setting a boolean flag
  function stopEvent() {
    stop = true;
    document.getElementById("stop").disabled = true;
    document.getElementById("reset").disabled = false;
  }

  function speedEventX(event) {
    // read the slider's value and change speed
    const value = event.srcElement.value;
    ball.setSpeedX(parseFloat(value));
  }
  function speedEventY(event) {
    // read the slider's value and change speed
    const value = event.srcElement.value;
    ball.setSpeedY(parseFloat(value));
  }


  // ----------------------------------------------------------
  // helper function (should be in a separate file: 'tools.js')
  // ----------------------------------------------------------

  function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
                                  rectX,    rectY,    rectWidth,    rectHeight)
  {

    // the rectangle touches the (outer) boarder, if x <= borderX, ...
    let collisionLeft   = false;
    let collisionRight  = false;
    let collisionTop    = false;
    let collisionBottom = false;

    if (rectX              <= boarderX                ) {collisionLeft  = true}
    if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
    if (rectY              <= boarderY                ) {collisionTop   = true}
    if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

    return [collisionLeft, collisionRight, collisionTop, collisionBottom];
  }

  // ---
  function detectRectangleCollision(x1, y1, width1, height1,
                                    x2, y2, width2, height2) {

    // The algorithm takes its decision by detecting areas
    // WITHOUT ANY overlapping

    // No overlapping if one rectangle is COMPLETELY on the 
    // left side of the other
    if (x1 > x2 + width2 || x2 > x1 + width1) {
      return false;
    }
     // No overlapping if one rectangle is COMPLETELY
     // above the other
    if (y1 > y2 + height2 || y2 > y1 + height1) {
      return false;
    }

    // all other cases
    return true;
  }
</script>
</head>

<body style="padding:1em" onload="start()">

  <h1 style="text-align: center">Moving ball</h1>

  <canvas id="canvas" width="700" height="300"
          style="margin-top:1em; background-color:yellow" >
  </canvas>

  <p></p>
  <button id="reset" onClick="start()" >Reset</button>
  <button id="stop"  onClick="stopEvent()" >Stop</button>

  <!-- sliders to indicate speed  -->
  <div>
    <input type="range" id="sliderSpeedX" name="sliderSpeedX" min="1" max="10"
           step=".1" onchange="speedEventX(event)">
    <label for="sliderSpeedX">Speed X</label>
  </div>
  <div>
    <input type="range" id="sliderSpeedY" name="sliderSpeedY" min="1" max="10"
           step=".1" onchange="speedEventY(event)">
    <label for="sliderSpeedY">Speed Y</label>
  </div>

</body>
</html>

See also

edit