JavaScript/Exercises/MovingWalls

From Wikibooks, open books for an open world
Jump to navigation Jump to search



The example combines some of the shown features:

  • It contains a class Rect and its sub-class Smiley.
  • Some walls with a rectangular shape move from right to left across the scene. They reappear on the right side after reaching the left border.
  • Two smileys populate the scene.
  • One smiley can be moved by using the buttons or the keyboard keys 'ArrowRight', ... .
  • Two helper-functions support the detection of rectangles: detectBorderCollision and detectRectangleCollision. The first one detects the collision of a graphical object with a surrounding rectangle, e.g., the canvas. The second one detects a collision of two rectangles.

You can extend the example in various ways:

  • Create walls randomly Math.random.
  • Introduce some kind of a 'level' by changing the speed, the number, the size, or the shape of walls.
Click to see solution
<!DOCTYPE html>
<html>
<head>
  <title>Collision</title>

  <style>
  .grid-container {
    display: grid;
    justify-content: left;
    grid-template-columns: auto auto auto auto auto auto;
    gap: 20px;
    margin-left: 1em;
    margin-top:  2em;
  }
  </style>

  <script>
  "use strict";

  // ----------------------------------------------------------------------------
  // class Rect  (should be implemented in a separate file 'Rect.js')
  // ----------------------------------------------------------------------------
  class Rect {
    // constructor with default values
    constructor(context, x = 0, y = 0, width = 10, height = 10, color = "red") {
      this.ctx = context;
      this.x = x;
      this.y = y;
      this.width  = width
      this.height = height;
      this.color  = color;

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

    // set the speed (= step size)
    setSpeed(x, y) {
      this.speedX = x;
      this.speedY = y;
    }
    
    // change the position according to the speed
    move() {
      this.x += this.speedX;
      this.y += this.speedY;
    }

    render() {
      this.ctx.fillStyle = this.color;
      this.ctx.fillRect(this.x, this.y, this.width, this.height);
    }

  }   // end of class

  // ----------------------------------------------
  // class 'Smiley'. It's derived from 'Rect' to use
  //   - the MBB for collision detection
  //   - methods of 'Rect' for movements
  // -----------------------------------------------
  class Smiley extends Rect {
    // constructor with default values
    constructor(context, text = "?", x = 10, y = 10, width = 30, height = 30) {
      // 
      super (context, x, y, width, height);
      this.ctx = context;
      this.text = text;
    }
    render() {
      this.ctx.font = "30px Arial";
      this.ctx.fillText(this.text, this.x, this.y);
      //this.ctx.fillRect(this.x, this.y-25, this.width, this.height);
    }

  }  // end of class


  // -------------------------------------------------------------
  // constants and variables which are known in the complete file
  // -------------------------------------------------------------

  // global constants
  const SPEED_WALLS  = -0.3;    // px
  const SPEED_SMILEY = 1;       // px

  // global variables
  let obstacles = [];
  let he, she;
  let requestId, stopFlag;


  // ----------------------------------------------------
  // functions
  // ----------------------------------------------------
  function start() {

    // init all graphical objects
    const canvas = document.getElementById("canvas");
    const context = canvas.getContext("2d");

    obstacles[0] = new Rect(context, 200, 80, 10, 180, "red");
    obstacles[1] = new Rect(context, 350, 20, 10, 100, "red");
    obstacles[2] = new Rect(context, 500, 80, 10, 200, "red");
    for (let i = 0; i < obstacles.length; i++) {
      obstacles[i].setSpeed(SPEED_WALLS, 0);
    }

    he  = new Smiley(context, "\u{1F60E}", 20, canvas.height - 40);
    she = new Smiley(context, "\u{1F60D}", canvas.width - 50, canvas.height - 40);

    // (re-)start the game
    if (requestId) {
      // cancel old frame chain
      cancelAnimationFrame(requestId);
    }

    stopFlag = false;
    playTheGame(canvas, context);

  }

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

    // collision of 'he' with border?
    // y-direction is different because (x,y) of text is at left/bottom
    const [crashL, crashR, crashT, crashB] = detectBorderCollision(
            0, 0, canvas.width, canvas.height,
            he.x, he.y-25, he.width, he.height);
    if (!crashL && !crashR && !crashT && !crashB) {
      he.move();
    } else {
      switch (true) {
        // move back a single px
        case crashL:
          he.x++;
          break;
        case crashR:
          he.x--;
          break;
        case crashT:
          he.y++;
          break;
        case crashB:
          he.y--;
          break;
      }
    }

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

      // collision of 'he' with  wall ?
      // y-direction is different because (x,y) of text is at left/bottom
      if (detectRectangleCollision(
            obstacles[i].x, obstacles[i].y, obstacles[i].width, obstacles[i].height,
            he.x, he.y-25, he.width, he.height)) {
        stopFlag = true;
        continue;
      }

      // if an obstacle reaches the left border, restart at the right side
      if (obstacles[i].x <= 0) {
        obstacles[i].x = canvas.width - 100;
      }
      obstacles[i].move();
    }

    // 'he' and 'she' meets?
    if (detectRectangleCollision(
          she.x, she.y-25, she.width, she.height,
          he.x, he.y-25, he.width, he.height)) {
      alert("Hola chica");
      stopFlag = true;
    }

    renderAll(canvas, context);
  }

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

    if (stopFlag) {
      // cancel the old animation and don't request frames
      cancelAnimationFrame(requestId);
      return;
    }

    // clear complete canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // draw objects at their current position
    for (let i = 0; i < obstacles.length; i++) {
     obstacles[i].render();
    }
    he.render();
    she.render();

    // request a new frame with a parameter that re-starst the game's logic
    // (and lastly leads to this line again)
    requestId = window.requestAnimationFrame(() => playTheGame(canvas, context)); 

  }

  // control the game via buttons
  function stopEvent() {
    stopFlag = true;
  }
  function leftEvent() {
    he.setSpeed(-SPEED_SMILEY, 0);
  }
  function rightEvent() {
    he.setSpeed(+SPEED_SMILEY, 0);
  }
  function upEvent() {
    he.setSpeed(0, -SPEED_SMILEY);
  }
  function downEvent() {
    he.setSpeed(0, +SPEED_SMILEY);
  }

  // control the game via keybord
  function keyDownEvent(event) {
    switch (event.code) {
        case 'ArrowUp':
            upEvent();
            break;
        case 'ArrowDown':
            downEvent();
            break;
        case 'ArrowLeft':
            leftEvent();
            break;
        case 'ArrowRight':
            rightEvent();
            break;
    }
  }
  function keyUpEvent(event) {
    switch (event.code) {
        case 'ArrowUp':
        case 'ArrowDown':
        case 'ArrowLeft':
        case 'ArrowRight':
          he.setSpeed(0, 0);
    }
  }

  // ----------------------------------------------------------------------------
  // helper functions (should be in a separate file, e.g.: 'tools.js')
  // ----------------------------------------------------------------------------

  function detectBorderCollision(borderX, borderY, borderWidth, borderHeight,
                                   rectX,   rectY,   rectWidth,   rectHeight)
  {

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

    if (rectX              <= borderX                ) {collisionLeft  = true}
    if (rectX + rectWidth  >= borderX + borderWidth  ) {collisionRight = true}
    if (rectY              <= borderY                ) {collisionTop   = true}
    if (rectY + rectHeight >= borderY + borderHeight ) {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 onload="start()" onkeydown="keyDownEvent(event)" onkeyup="keyUpEvent(event)" >

  <h4 style="margin-left:1em; text-align:center">Initiate motions by buttons or keyboard keys (ArrowUp, ...)</h4>

  <!-- the drawing area  -->
  <canvas id="canvas" width="700" height="300"
          style="margin-left:1em; background-color:yellow" >
  </canvas>

  <!-- a grid for the buttons: 3 rows, 6 columns per row -->
  <div class="grid-container">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <button onclick="upEvent()">Up</button>
    <div></div>

    <button onclick="start()">Reset</button>
    <button onclick="stopEvent()">Stop</button>
    <div style="margin-right: 2em"></div>
    <button onclick="leftEvent()">Left</button>
    <div></div>
    <button onclick="rightEvent()">Right</button>

    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <button onclick="downEvent()">Down</button>
    <div></div>
  </div>

</body>
</html>