跳转到内容

JavaScript/练习/碰撞

来自维基教科书,开放书籍,开放世界



当物体在周围移动时,它们以一定的速度移动。 因此,您应该在物体的 class 中添加两个属性来表示它在 x 方向和 y 方向上的速度。 正值表示向右或向下的方向,负值表示向左或向上的方向。 此外,该类需要修改速度的函数。

碰撞 - 1

[编辑 | 编辑源代码]

物体可能会与其他物体或画布边界发生碰撞。 检测此类碰撞的算法取决于物体的类型:对于矩形而言,关于 x 方向,左侧由起点给出,而对于圆形,左侧必须从中心点和半径计算得出。 与画布边界的碰撞是“从内到外”的碰撞,物体之间的碰撞始终是“从外到外”的碰撞。

尽管如此,开发解决许多“碰撞”问题的通用算法是可能的。 每个二维物体以及每个此类物体的组都可以被其最小包围盒 (MBB)包围,最小包围盒定义为矩形。 因此,物体的碰撞可以通过检测其 MBB 的碰撞的算法来解决,至少在第一个近似值中是这样。 它并不总是完全准确,但对于我们的示例,它应该足够了。

恒定速度

[编辑 | 编辑源代码]

我们创建一个球(圆形)并让它在画布内移动。

  • 像往常一样,函数 playTheGame 包含游戏的“逻辑”。 它很简单:根据球的“速度”移动球。 “速度”是指它应该前进的像素数。
  • 函数 detectBorderCollision 检查画布的边界是否被当前步骤触碰。 如果是这样,则速度将反转到相反的方向。
  • detectBorderCollision 检查矩形的边界是否被在其内部空间内移动的矩形触碰。 这与两个矩形像两辆汽车一样发生碰撞的情况不同。
  • “外部”矩形是画布本身。 我们使用它的属性作为函数调用的前四个参数。
  • 画布内的球不是矩形;它是圆形。 我们“计算”圆的 MBB,并将 MBB 的属性用作函数调用的最后四个参数。(对于此算法,MBB 不仅提供问题的近似值,它还是精确的解决方案。)
点击查看解决方案
<!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>

变化速度

[编辑 | 编辑源代码]

该示例与上面的示例相同,增加了改变球的速度的功能。 它添加了两个 HTML 元素 input type="range" 作为滑动条。 滑动条指示在 x 和 y 方向上的预期速度。

点击查看解决方案
<!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>

碰撞 - 2

[编辑 | 编辑源代码]

该示例与上面的示例相同,增加了检测障碍物(矩形)的功能。 如果球与障碍物发生碰撞,游戏将停止。

碰撞由函数 detectRectangleCollision 检测。 它比较两个矩形。 我们使用圆的 MBB 作为第二个参数,这会导致轻微的误差。

点击查看解决方案
<!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>
华夏公益教科书