跳轉到內容

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>
華夏公益教科書