溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript怎么實現(xiàn)碰撞物理引擎

發(fā)布時間:2022-02-21 16:30:08 來源:億速云 閱讀:165 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細介紹“JavaScript怎么實現(xiàn)碰撞物理引擎”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當(dāng),希望這篇“JavaScript怎么實現(xiàn)碰撞物理引擎”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

效果圖:

JavaScript怎么實現(xiàn)碰撞物理引擎

接下來看一下怎么實現(xiàn)這樣的效果。

基礎(chǔ)結(jié)構(gòu)

我們這里使用 canvas 來實現(xiàn) JavaScript 物理引擎。首先準(zhǔn)備項目的基礎(chǔ)文件和樣式,新建一個 index.html、index.js 和 style.css 文件,分別用于編寫 canvas 的 html 結(jié)構(gòu)、引擎代碼和畫布樣式。

在 index.html 的 <head /> 標(biāo)簽中引入樣式文件:

<link rel="stylesheet" href="./style.css" />

在 <body /> 中,添加 canvas 元素、加載 index.js 文件:

<main>
  <canvas id="gameboard"></canvas>
</main>
<script src="./index.js"></script>

這段代碼定義了 id 為 gameboard 的 <canvas /> 元素,并放在了 <main /> 元素下, <main /> 元素主要是用來設(shè)置背景色和畫布大小。在 <main/> 元素的下方引入 index.js 文件,這樣可以在 DOM 加載完成之后再執(zhí)行 JS 中的代碼。

style.css 中的代碼如下:

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  font-family: sans-serif;
}

main {
  width: 100vw;
  height: 100vh;
  background: hsl(0deg, 0%, 10%);
}

樣式很簡單,去掉所有元素的外邊距、內(nèi)間距,并把 <main/> 元素的寬高設(shè)置為與瀏覽器可視區(qū)域相同,背景色為深灰色。

hsl(hue, saturation, brightness) 為 css 顏色表示法之一,參數(shù)分別為色相,飽和度和亮度。

繪制小球

接下來繪制小球,主要用到了 canvas 相關(guān)的 api。

在 index.js 中,編寫如下代碼:

const canvas = document.getElementById("gameboard");
const ctx = canvas.getContext("2d");

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

let width = canvas.width;
let height = canvas.height;

ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(100, 100, 60, 0, 2 * Math.PI);
ctx.fill();

代碼中主要利用了二維 context 進行繪圖操作:

  • 通過 canvas 的 id 獲取 canvas 元素對象。

  • 通過 canvas 元素對象獲取繪圖 context, getContext() 需要一個參數(shù),用于表明是繪制 2d 圖像,還是使用 webgl 繪制 3d 圖象,這里選擇 2d。context 就類似是一支畫筆,可以改變它的顏色和繪制基本的形狀。

  • 給 canvas 的寬高設(shè)置為瀏覽器可視區(qū)域的寬高,并保存到 width 和 height 變量中方便后續(xù)使用。

  • 給 context 設(shè)置顏色,然后調(diào)用 beginPath() 開始繪圖。

  • 使用 arc() 方法繪制圓形,它接收 5 個參數(shù),前兩個為圓心的 x、y 坐標(biāo),第 3 個為半徑長度, 第 4 個和第 5 個分別是起始角度和結(jié)束角度,因為 arc() 其實是用來繪制一段圓弧,這里讓它畫一段 0 到 360 度的圓弧,就形成了一個圓形。這里的角度是使用 radian 形式表示的,0 到 360 度可以用 0 到 2 * Math.PI 來表示。

  • 最后使用 ctx.fill() 給圓形填上顏色。

這樣就成功的繪制了一個圓形,我們在這把它當(dāng)作一個小球:

JavaScript怎么實現(xiàn)碰撞物理引擎

移動小球

不過,這個時候的小球還是靜止的,如果想讓它移動,那么得修改它的圓心坐標(biāo),具體修改的數(shù)值則與運動速度有關(guān)。在移動小球之前,先看一下 canvas 進行動畫的原理:

Canvas 進行動畫的原理與傳統(tǒng)的電影膠片類似,在一段時間內(nèi),繪制圖像、更新圖像位置或形狀、清除畫布,重新繪制圖像,當(dāng)在 1 秒內(nèi)連續(xù)執(zhí)行 60 次或以上這樣的操作時,即以 60 幀的速度,就可以產(chǎn)生連續(xù)的畫面。

那么在 JavaScript 中,瀏覽器提供了 window.requestAnimationFrame() 方法,它接收一個回調(diào)函數(shù)作為參數(shù),每一次執(zhí)行回調(diào)函數(shù)就相當(dāng)于 1 幀動畫,我們需要通過遞歸或循環(huán)連續(xù)調(diào)用它,瀏覽器會盡可能的在 1 秒內(nèi)執(zhí)行 60 次回調(diào)函數(shù)。那么利用它,我們就可以對 canvas 進行重繪,以實現(xiàn)小球的移動效果。

由于 window.requestAnimationFrame() 的調(diào)用基本是持續(xù)進行的,所以我們也可以把它稱為游戲循環(huán)(Game loop)。

接下來我們來看如何編寫動畫的基礎(chǔ)結(jié)構(gòu):

function process() {
  window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);

這里的 process() 函數(shù)就是 1 秒鐘要執(zhí)行 60 次的回調(diào)函數(shù),每次執(zhí)行完畢后繼續(xù)調(diào)用 window.requestAnimationFrame(process)進行下一次循環(huán)。如果要移動小球,那么就需要把繪制小球和修改圓心 x、y 坐標(biāo)的代碼寫到 process() 函數(shù)中。

為了方便更新坐標(biāo),我們把小球的圓心坐標(biāo)保存到變量中,以方便對它們進行修改,然后再定義兩個新的變量,分別表示在 x 軸方向上的速度vx,和 y 軸方向上的速度 vy,然后把 context 相關(guān)的繪圖操作放到 process() 中:

let x = 100;
let y = 100;
let vx = 12;
let vy = 25;

function process() {
  ctx.fillStyle = "hsl(170, 100%, 50%)";
  ctx.beginPath();
  ctx.arc(x, y, 60, 0, 2 * Math.PI);
  ctx.fill();
  window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);

要計算圓心坐標(biāo) x、y 的移動距離,我們需要速度和時間,速度這里有了, 那么時間要怎么獲取呢? window.requestAnimationFrame() 會把當(dāng)前時間的毫秒數(shù)(即時間戳)傳遞給回調(diào)函數(shù),我們可以把本次調(diào)用的時間戳保存起來,然后在下一次調(diào)用時計算出執(zhí)行這 1 幀動畫消耗了多少秒,然后根據(jù)這個秒數(shù)和 x、y 軸方向上的速度去計算移動距離,分別加到 x 和 y 上,以獲得最新的位置。注意這里的時間是上一次函數(shù)調(diào)用和本次函數(shù)調(diào)用的時間間隔,并不是第 1 次函數(shù)調(diào)用到當(dāng)前函數(shù)調(diào)用總共過去了多少秒,所以相當(dāng)于是時間增量,需要在之前 x 和 y 的值的基礎(chǔ)上進行相加,代碼如下:

let startTime;

function process(now) {
  if (!startTime) {
    startTime = now;
  }
  let seconds = (now - startTime) / 1000;
  startTime = now;

  // 更新位置
  x += vx * seconds;
  y += vy * seconds;

  // 清除畫布
  ctx.clearRect(0, 0, width, height);
  // 繪制小球
  ctx.fillStyle = "hsl(170, 100%, 50%)";
  ctx.beginPath();
  ctx.arc(x, y, 60, 0, 2 * Math.PI);
  ctx.fill();

  window.requestAnimationFrame(process);
}

process() 現(xiàn)在接收當(dāng)前時間戳作為參數(shù),然后做了下面這些操作:

  • 計算上次函數(shù)調(diào)用與本次函數(shù)調(diào)用的時間間隔,以秒計,記錄本次調(diào)用的時間戳用于下一次計算。

  • 根據(jù) x、y 方向上的速度,和剛剛計算出來的時間,計算出移動距離。

  • 調(diào)用 clearRect() 清除矩形區(qū)域畫布,這里的參數(shù),前兩個是左上角坐標(biāo),后兩個是寬高,把 canvas 的寬高傳進去就會把整個畫布清除。

  • 重新繪制小球。

現(xiàn)在小球就可以移動了:

JavaScript怎么實現(xiàn)碰撞物理引擎

重構(gòu)代碼

上邊的代碼適合只有一個小球的情況,如果有多個小球需要繪制,就得編寫大量重復(fù)的代碼,這時我們可以把小球抽象成一個類,里邊有繪圖、更新位置等操作,還有坐標(biāo)、速度、半徑等屬性,重構(gòu)后的代碼如下:

class Circle {
  constructor(context, x, y, r, vx, vy) {
    this.context = context;
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = vx;
    this.vy = vy;
  }
  
    // 繪制小球
  draw() {
    this.context.fillStyle = "hsl(170, 100%, 50%)";
    this.context.beginPath();
    this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
    this.context.fill();
  }

  /**
   * 更新畫布
   * @param {number} seconds
   */
  update(seconds) {
    this.x += this.vx * seconds;
    this.y += this.vy * seconds;
  }
}

里邊的代碼跟之前的一樣,這里就不再贅述了,需要注意的是,Circle 類的 context 畫筆屬性是通過構(gòu)造函數(shù)傳遞進來的,更新位置的代碼放到了 update() 方法中。

對于整個 canvas 的繪制過程,也可以抽象成一個類,當(dāng)作是游戲或引擎控制器,例如把它放到一個叫 Gameboard 的類中:

class Gameboard {
  constructor() {
    this.startTime;
    this.init();
  }

  init() {
    this.circles = [
      new Circle(ctx, 100, 100, 60, 12, 25),
      new Circle(ctx, 180, 180, 30, 70, 45),
    ];
    window.requestAnimationFrame(this.process.bind(this));
  }

  process(now) {
    if (!this.startTime) {
      this.startTime = now;
    }
    let seconds = (now - this.startTime) / 1000;
    this.startTime = now;

    for (let i = 0; i < this.circles.length; i++) {
      this.circles[i].update(seconds);
    }
    ctx.clearRect(0, 0, width, height);

    for (let i = 0; i < this.circles.length; i++) {
      this.circles[i].draw(ctx);
    }
    window.requestAnimationFrame(this.process.bind(this));
  }
}

new Gameboard();

在 Gameboard 類中:

  • startTime 保存了上次函數(shù)執(zhí)行的時間戳的屬性,放到了構(gòu)造函數(shù)中。

  • init() 方法創(chuàng)建了一個 circles 數(shù)組,里邊放了兩個示例的小球,這里先不涉及碰撞問題。然后調(diào)用 window.requestAnimationFrame() 開啟動畫。注意這里使用了 bind() 來把 Gameboard 的 this 綁定到回調(diào)函數(shù)中,以便于訪問 Gameboard 中的方法和屬性。

  • process() 方法也寫到了這里邊,每次執(zhí)行時會遍歷小球數(shù)組,對每個小球進行位置更新,然后清除畫布,再重新繪制每個小球。

  • 最后初始化 Gameboard 對象就可以開始執(zhí)行動畫了。

這個時候有兩個小球在移動了。

JavaScript怎么實現(xiàn)碰撞物理引擎

碰撞檢測

為了實現(xiàn)仿真的物理特性,多個物體間碰撞會有相應(yīng)的反應(yīng),第一步就是要先檢測碰撞。我們先再多加幾個小球,以便于碰撞的發(fā)生,在 Gameboard 類的 init() 方法中再添加幾個小球:

this.circles = [
  new Circle(ctx, 30, 50, 30, -100, 390),
  new Circle(ctx, 60, 180, 20, 180, -275),
  new Circle(ctx, 120, 100, 60, 120, 262),
  new Circle(ctx, 150, 180, 10, -130, 138),
  new Circle(ctx, 190, 210, 10, 138, -280),
  new Circle(ctx, 220, 240, 10, 142, 350),
  new Circle(ctx, 100, 260, 10, 135, -460),
  new Circle(ctx, 120, 285, 10, -165, 370),
  new Circle(ctx, 140, 290, 10, 125, 230),
  new Circle(ctx, 160, 380, 10, -175, -180),
  new Circle(ctx, 180, 310, 10, 115, 440),
  new Circle(ctx, 100, 310, 10, -195, -325),
  new Circle(ctx, 60, 150, 10, -138, 420),
  new Circle(ctx, 70, 430, 45, 135, -230),
  new Circle(ctx, 250, 290, 40, -140, 335),
];

然后給小球添加一個碰撞狀態(tài),在碰撞時,給兩個小球設(shè)置為不同的顏色:

class Circle {
  constructor(context, x, y, r, vx, vy) {
    // 其它代碼
    this.colliding = false;
  }
  draw() {
    this.context.fillStyle = this.colliding
      ? "hsl(300, 100%, 70%)"
      : "hsl(170, 100%, 50%)";
    // 其它代碼
  }
}

現(xiàn)在來判斷小球之間是否發(fā)生了碰撞,這個條件很簡單,判斷兩個小球圓心的距離是否小于兩個小球的半徑之和就可以了,如果小于等于則發(fā)生了碰撞,大于則沒有發(fā)生碰撞。

x1、y1 和 x2、y2 分別兩個小球的圓心坐標(biāo)。在比較時,可以對半徑和進行平方運算,進而省略對距離的開方運算

r1 和 r2 為兩球的半徑。

在 Circle 類中,先添加一個isCircleCollided(other)方法,接收另一個小球?qū)ο笞鳛閰?shù),返回比較結(jié)果:

isCircleCollided(other) {
  let squareDistance =
      (this.x - other.x) * (this.x - other.x) +
      (this.y - other.y) * (this.y - other.y);
  let squareRadius = (this.r + other.r) * (this.r + other.r);
  return squareDistance <= squareRadius;
}

再添加 checkCollideWith(other) 方法,調(diào)用 isCircleCollided(other) 判斷碰撞后,把兩球的碰撞狀態(tài)設(shè)置為 true:

checkCollideWith(other) {
  if (this.isCircleCollided(other)) {
    this.colliding = true;
    other.colliding = true;
  }
}

接著我們需要使用雙循環(huán)兩兩比對小球是否發(fā)生了碰撞,由于小球數(shù)組存放在 Gameboard 對象中,我們給它添加一個 checkCollision() 方法來檢測碰撞:

checkCollision() {
  // 重置碰撞狀態(tài)
  this.circles.forEach((circle) => (circle.colliding = false));

  for (let i = 0; i < this.circles.length; i++) {
    for (let j = i + 1; j < this.circles.length; j++) {
      this.circles[i].checkCollideWith(this.circles[j]);
    }
  }
}

因為小球在碰撞后就應(yīng)立即彈開,所以我們一開始要把所有小球的碰撞狀態(tài)設(shè)置為 false,之后在循環(huán)中,對每個小球進行檢測。這里注意到內(nèi)層循環(huán)是從 i + 1 開始的,這是因為在判斷 1 球和 2 球是否碰撞后,就無須再判斷 2 球 和 1 球了。

之后在 process() 方法中,執(zhí)行檢測,注意檢測應(yīng)該發(fā)生在使用 for 循環(huán)更新小球位置的后邊才準(zhǔn)確:

for (let i = 0; i < this.circles.length; i++) {
  this.circles[i].update(seconds);
}
this.checkCollision();

現(xiàn)在,可以看到小球在碰撞時,會改變顏色了。

JavaScript怎么實現(xiàn)碰撞物理引擎

邊界碰撞

上邊的代碼在執(zhí)行之后,小球都會穿過邊界跑到外邊去,那么我們先處理一下邊界碰撞的問題。檢測邊界碰撞需要把四個面全部都處理到,根據(jù)圓心坐標(biāo)和半徑來判斷是否和邊界發(fā)生了碰撞。例如跟左邊界發(fā)生碰撞時,圓心的 x 坐標(biāo)是小于或等于半徑長度的,而跟右邊界發(fā)生碰撞時,圓心 x 坐標(biāo)應(yīng)該大于或等于畫布最右側(cè)坐標(biāo)(即寬度值)減去半徑的長度。上邊界和下邊界類似,只是使用圓心 y 坐標(biāo)和畫布的高度值。在水平方向上(即左右邊界)發(fā)生碰撞時,小球的運動方向發(fā)生改變,只需要把垂直方向上的速度 vy 值取反即可,在垂直方向上碰撞則把 vx 取反。

JavaScript怎么實現(xiàn)碰撞物理引擎

現(xiàn)在看一下代碼的實現(xiàn),在 Gameboard 類中添加一個 checkEdgeCollision() 方法,根據(jù)上邊描述的規(guī)則編寫如下代碼:

checkEdgeCollision() {
  this.circles.forEach((circle) => {
    // 左右墻壁碰撞
    if (circle.x < circle.r) {
      circle.vx = -circle.vx;
      circle.x = circle.r;
    } else if (circle.x > width - circle.r) {
      circle.vx = -circle.vx;
      circle.x = width - circle.r;
    }

    // 上下墻壁碰撞
    if (circle.y < circle.r) {
      circle.vy = -circle.vy;
      circle.y = circle.r;
    } else if (circle.y > height - circle.r) {
      circle.vy = -circle.vy;
      circle.y = height - circle.r;
    }
  });
}

在代碼中,碰撞時,除了對速度進行取反操作之外,還把小球的坐標(biāo)修改為緊臨邊界,防止超出。接下來在 process() 中添加對邊界碰撞的檢測:

this.checkEdgeCollision();
this.checkCollision();

這時候可以看到小球在碰到邊界時,可以反彈了:

JavaScript怎么實現(xiàn)碰撞物理引擎

但是小球間的碰撞還沒有處理,在處理之前,先復(fù)習(xí)一下向量的基本操作,數(shù)學(xué)好的同學(xué)可以直接跳過,只看相關(guān)的代碼。

向量的基本操作

由于在碰撞時,需要對速度向量(或稱為矢量)進行操作,向量是使用類似坐標(biāo)的形式表示的,例如 < 3, 5 > (這里用 <> 表示向量),它有長度和方向,對于它的運算有一定的規(guī)則,本教程中需要用到向量的加法、減法、乘法、點乘和標(biāo)準(zhǔn)化操作。

向量相加只需要把兩個向量的 x 坐標(biāo)和 y 坐標(biāo)相加即可,例如:< 3 , 5 > + < 1 , 2 > = < 4 , 7 > <3, 5> + <1, 2> = <4, 7><3,5>+<1,2>=<4,7>

減法與加法類似,把 x 坐標(biāo)和 y 坐標(biāo)相減,例如:< 3 , 5 > ? < 1 , 2 > = < 2 , 3 > <3, 5> - <1, 2> = <2, 3><3,5>?<1,2>=<2,3>

乘法,這里指的是向量和標(biāo)量的乘法,標(biāo)量指的就是普通的數(shù)字,結(jié)果是把 x 和 y 分別和標(biāo)量相乘,例如:3 × < 3 , 5 > = < 9 , 15 > 3\times<3, 5> = <9, 15>3×<3,5>=<9,15>。

點乘是兩個向量相乘的一種方式,類似的還有叉乘,但是在本示例中用不到,點乘其實計算的是一個向量在另一個向量上的投影,它的計算方式為兩個向量的 x 的積加上 y 的積,它返回的是一個標(biāo)量,即第 1 個向量在第 2 個向量上投影的長度,例如:< 3 , 5 > ? < 1 , 2 > = 3 × 1 + 5 × 2 = 13 <3, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13<3,5>?<1,2>=3×1+5×2=13

標(biāo)準(zhǔn)化是除掉向量的長度,只剩下方向,這樣的向量它的長度為 1,稱為單位向量,標(biāo)準(zhǔn)化的過程是讓 x 和 y 分別除以向量的長度,因為向量表示的是和原點(0, 0)的距離,所以可以直接使用JavaScript怎么實現(xiàn)碰撞物理引擎 計算長度,例如 < 3, 4 > 標(biāo)準(zhǔn)化后的結(jié)果為:< 3 , 5 > ? < 1 , 2 > = 3 × 1 + 5 × 2 = 13 <3, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13<3,5>?<1,2>=3×1+5×2=13。

了解了向量的基本運算后,我們來創(chuàng)建一個 Vector 工具類,來方便我們進行向量的運算,它的代碼就是實現(xiàn)了這些運算規(guī)則:

class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  /**
   * 向量加法
   * @param {Vector} v
   */
  add(v) {
    return new Vector(this.x + v.x, this.y + v.y);
  }

  /**
   * 向量減法
   * @param {Vector} v
   */
  substract(v) {
    return new Vector(this.x - v.x, this.y - v.y);
  }

  /**
   * 向量與標(biāo)量乘法
   * @param {Vector} s
   */
  multiply(s) {
    return new Vector(this.x * s, this.y * s);
  }

  /**
   * 向量與向量點乘(投影)
   * @param {Vector} v
   */
  dot(v) {
    return this.x * v.x + this.y * v.y;
  }

  /**
   * 向量標(biāo)準(zhǔn)化(除去長度)
   * @param {number} distance
   */
  normalize() {
    let distance = Math.sqrt(this.x * this.x + this.y * this.y);
    return new Vector(this.x / distance, this.y / distance);
  }
}

代碼中沒有什么特殊的語法和操作,這里就不再贅述了,接下來我們看一下小球的碰撞問題。

碰撞處理

碰撞處理最主要的部分就是計算碰撞后的速度和方向。通常最簡單的碰撞問題是在同一個水平面上的兩個物體的碰撞,稱為一維碰撞,因為此時只需要計算同一方向上的速度,而我們現(xiàn)在的程序小球是在一個二維平面內(nèi)運動的,小球之間發(fā)生正面相碰(即在同一運動方向)的概率很小,大部分是斜碰(在不同運動方向上擦肩相碰),需要同時計算水平和垂直方向上的速度和方向,這就屬于是二維碰撞問題。不過,其實小球之間的碰撞,只有在連心線(兩個圓心的連線)上有作用力,而在碰撞接觸的切線方向上沒有作用力,那么我們只需要知道連心線方向的速度變化就可以了,這樣就轉(zhuǎn)換成了一維碰撞。

JavaScript怎么實現(xiàn)碰撞物理引擎

計算碰撞后的速度時,遵守動量守恒定律和動能守恒定律,公式分別為:

動量守恒定律

JavaScript怎么實現(xiàn)碰撞物理引擎

動能守恒定律

JavaScript怎么實現(xiàn)碰撞物理引擎

m1、m2 分別為兩小球的質(zhì)量,v1 和 v2 為兩小球碰撞前的速度向量,v1' 和 v2' 為碰撞后的速度向量。根據(jù)這兩個公式可以推導(dǎo)出兩小球碰撞后的速度公式:

JavaScript怎么實現(xiàn)碰撞物理引擎

如果不考慮小球的質(zhì)量,或質(zhì)量相同,其實就是兩小球速度互換,即:

JavaScript怎么實現(xiàn)碰撞物理引擎

這里我們給小球加上質(zhì)量,然后套用公式來計算小球碰撞后速度,先在 Circle 類中給小球加上質(zhì)量 mass 屬性:

class Circle {
  constructor(context, x, y, r, vx, vy, mass = 1) {
    // 其它代碼
    this.mass = mass;
  }
}

然后在 Gameboard 類的初始化小球處,給每個小球添加質(zhì)量:

this.circles = [
  new Circle(ctx, 30, 50, 30, -100, 390, 30),
  new Circle(ctx, 60, 180, 20, 180, -275, 20),
  new Circle(ctx, 120, 100, 60, 120, 262, 100),
  new Circle(ctx, 150, 180, 10, -130, 138, 10),
  new Circle(ctx, 190, 210, 10, 138, -280, 10),
  new Circle(ctx, 220, 240, 10, 142, 350, 10),
  new Circle(ctx, 100, 260, 10, 135, -460, 10),
  new Circle(ctx, 120, 285, 10, -165, 370, 10),
  new Circle(ctx, 140, 290, 10, 125, 230, 10),
  new Circle(ctx, 160, 380, 10, -175, -180, 10),
  new Circle(ctx, 180, 310, 10, 115, 440, 10),
  new Circle(ctx, 100, 310, 10, -195, -325, 10),
  new Circle(ctx, 60, 150, 10, -138, 420, 10),
  new Circle(ctx, 70, 430, 45, 135, -230, 45),
  new Circle(ctx, 250, 290, 40, -140, 335, 40),
];

在 Circle 類中加上 changeVelocityAndDirection(other) 方法來計算碰撞后的速度,它接收另一個小球?qū)ο笞鳛閰?shù),同時計算這兩個小球碰撞厚的速度和方向,這個是整個引擎的核心,我們一點一點的來看它是如何實現(xiàn)的。首先把兩個小球的速度使用 Vector 向量來表示:

  changeVelocityAndDirection(other) {
    // 創(chuàng)建兩小球的速度向量
    let velocity1 = new Vector(this.vx, this.vy);
    let velocity2 = new Vector(other.vx, other.vy);
  }

因為我們本身就已經(jīng)使用 vx 和 vy 來表示水平和垂直方向上的速度向量了,所以直接把它們傳給 Vector 的構(gòu)造函數(shù)就可以了。velocity1 和 velocity2 分別代表當(dāng)前小球和碰撞小球的速度向量。

接下來獲取連心線方向的向量,也就是兩個圓心坐標(biāo)的差:

let vNorm = new Vector(this.x - other.x, this.y - other.y);

接下來獲取連心線方向的單位向量和切線方向上的單位向量,這些單位向量代表的是連心線和切線的方向:

let unitVNorm = vNorm.normalize();
let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x);

unitVNorm 是連心線方向單位向量,unitVTan 是切線方向單位向量,切線方向其實就是把連心線向量的 x、y 坐標(biāo)互換,并把 y 坐標(biāo)取反。根據(jù)這兩個單位向量,使用點乘計算小球速度在這兩個方向上的投影:

let v1n = velocity1.dot(unitVNorm);
let v1t = velocity1.dot(unitVTan);

let v2n = velocity2.dot(unitVNorm);
let v2t = velocity2.dot(unitVTan);

計算結(jié)果是一個標(biāo)量,也就是沒有方向的速度值。v1n 和 v1t 表示當(dāng)前小球在連心線和切線方向的速度值,v2n 和 v2t 則表示的是碰撞小球 的速度值。在計算出兩小球的速度值之后,我們就有了碰撞后的速度公式所需要的變量值了,直接用代碼把公式套用進去:

let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);

v1nAfter 和 v2nAfter 分別是兩小球碰撞后的速度,現(xiàn)在可以先判斷一下,如果 v1nAfter 小于 v2nAfter,那么第 1 個小球和第 2 個小球會越來越遠,此時不用處理碰撞:

if (v1nAfter < v2nAfter) {
  return;
}

然后再給碰撞后的速度加上方向,計算在連心線方向和切線方向上的速度,只需要讓速度標(biāo)量跟連心線單位向量和切線單位向量相乘:

let v1VectorNorm = unitVNorm.multiply(v1nAfter);
let v1VectorTan = unitVTan.multiply(v1t);

let v2VectorNorm = unitVNorm.multiply(v2nAfter);
let v2VectorTan = unitVTan.multiply(v2t);

這樣有了兩個小球連心線上的新速度向量和切線方向上的新速度向量,最后把連心線上的速度向量和切線方向的速度向量進行加法操作,就能獲得碰撞后小球的速度向量:

let velocity1After = v1VectorNorm.add(v1VectorTan);
let velocity2After = v2VectorNorm.add(v2VectorTan);

之后我們把向量中的 x 和 y 分別還原到小球的 vx 和 vy 屬性中:

this.vx = velocity1After.x;
this.vy = velocity1After.y;

other.vx = velocity2After.x;
other.vy = velocity2After.y;

最后在 checkCollideWith() 方法的 if 語句中調(diào)用此方法,即在檢測到碰撞時:

checkCollideWith(other) {
  if (this.isCircleCollided(other)) {
    this.colliding = true;
    other.colliding = true;
    this.changeVelocityAndDirection(other); // 在這里調(diào)用
  }
}

這時,小球的碰撞效果就實現(xiàn)了。

JavaScript怎么實現(xiàn)碰撞物理引擎

非彈性碰撞

現(xiàn)在小球之間的碰撞屬于完全彈性碰撞,即碰撞之后不會有能量損失,這樣小球永遠不會停止運動,我們可以讓小球在碰撞之后損失一點能量,來模擬更真實的物理效果。要讓小球碰撞后有能量損失,可以使用恢復(fù)系數(shù),它是一個取值范圍為 0 到 1 的數(shù)值,每次碰撞后,乘以它就可以減慢速度,如果恢復(fù)系數(shù)為 1 則為完全彈性碰撞,為 0 則是完全非彈性碰撞,之間的數(shù)值為非彈性碰撞,現(xiàn)實生活中的碰撞都是非彈性碰撞。

先看一下邊界碰撞,這個比較簡單,假設(shè)邊界的恢復(fù)系數(shù)為 0.8,然后在每次對速度取反的時候乘以它就可以了,把 Gameboard checkEdgeCollision()方法作如下改動:

  checkEdgeCollision() {
    const cor = 0.8;                  // 設(shè)置恢復(fù)系統(tǒng)
    this.circles.forEach((circle) => {
      // 左右墻壁碰撞
      if (circle.x < circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢復(fù)系數(shù)
        circle.x = circle.r;
      } else if (circle.x > width - circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢復(fù)系數(shù)
        circle.x = width - circle.r;
      }

      // 上下墻壁碰撞
      if (circle.y < circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢復(fù)系數(shù)
        circle.y = circle.r;
      } else if (circle.y > height - circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢復(fù)系數(shù)
        circle.y = height - circle.r;
      }
    });
  }

接下來設(shè)置小球的恢復(fù)系數(shù),給 Circle 類再加上一個恢復(fù)系數(shù) cor 屬性,每個小球可以設(shè)置不同的數(shù)值,來讓它們有不同的彈性,然后在初始化小球時設(shè)置隨意的恢復(fù)系數(shù):

class Circle {
  constructor(context, x, y, r, vx, vy, mass = 1, cor = 1) {
    // 其它代碼
    this.cor = cor;
  }
}

class Gameboard {
  init() {
   this.circles = [
      new Circle(ctx, 30, 50, 30, -100, 390, 30, 0.7),
      new Circle(ctx, 60, 180, 20, 180, -275, 20, 0.7),
      new Circle(ctx, 120, 100, 60, 120, 262, 100, 0.3),
      new Circle(ctx, 150, 180, 10, -130, 138, 10, 0.7),
      new Circle(ctx, 190, 210, 10, 138, -280, 10, 0.7),
      new Circle(ctx, 220, 240, 10, 142, 350, 10, 0.7),
      new Circle(ctx, 100, 260, 10, 135, -460, 10, 0.7),
      new Circle(ctx, 120, 285, 10, -165, 370, 10, 0.7),
      new Circle(ctx, 140, 290, 10, 125, 230, 10, 0.7),
      new Circle(ctx, 160, 380, 10, -175, -180, 10, 0.7),
      new Circle(ctx, 180, 310, 10, 115, 440, 10, 0.7),
      new Circle(ctx, 100, 310, 10, -195, -325, 10, 0.7),
      new Circle(ctx, 60, 150, 10, -138, 420, 10, 0.7),
      new Circle(ctx, 70, 430, 45, 135, -230, 45, 0.7),
      new Circle(ctx, 250, 290, 40, -140, 335, 40, 0.7),
    ];
  }
}

加上恢復(fù)系數(shù)之后,小球碰撞后的速度計算也需要改變一下,可以簡單的讓 v1nAfter 和 v2nAfter 乘以小球的恢復(fù)系數(shù),也可以使用帶有恢復(fù)系數(shù)的速度公式

接著把公式轉(zhuǎn)換為代碼,在 Circle 類的 changeVelocityAndDirection() 方法中,替換掉 v1nAfter 和 v2nAfter 的計算公式:

let cor = Math.min(this.cor, other.cor);
let v1nAfter =
    (this.mass * v1n + other.mass * v2n + cor * other.mass * (v2n - v1n)) /
    (this.mass + other.mass);

let v2nAfter =
    (this.mass * v1n + other.mass * v2n + cor * this.mass * (v1n - v2n)) /
    (this.mass + other.mass);

這里要注意的是兩小球碰撞時的恢復(fù)系數(shù)應(yīng)取兩者的最小值,按照常識,彈性小的無論是去撞別人還是別人撞它,都會有同樣的效果?,F(xiàn)在小球碰撞后速度會有所減慢,不過還差一點,我們可以加上重力來讓小球自然下落。JavaScript怎么實現(xiàn)碰撞物理引擎

重力

添加重力比較簡單,先在全局定義重力加速度常量,然后在小球更新垂直方向上的速度時,累計重力加速度就可以了:

const gravity = 980;

class Circle {
  update(seconds) {
    this.vy += gravity * seconds; // 重力加速度
    this.x += this.vx * seconds;
    this.y += this.vy * seconds;
  }
}

重力加速度大約是 JavaScript怎么實現(xiàn)碰撞物理引擎,但是由于我們的畫布是以象素為單位的,所以使用 9.8 看起來會像是沒有重力,或者像是從很遠的地方觀察小球,這時候可以把重力加速度放大一定倍數(shù)來達到更逼真的效果。

JavaScript怎么實現(xiàn)碰撞物理引擎

讀到這里,這篇“JavaScript怎么實現(xiàn)碰撞物理引擎”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

相關(guān)推薦