Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

JavaScript game development: Hands-on collision physics engine


May 30, 2021 Article blog


Table of contents


v1nAfter and v2nAfter are the speed after the collision of two small balls, now you can first judge, if v1nAfter is less than v2nAfter then the first ball and 2 small ball will be farther and farther away, do not have to deal with collisions: years ago I saw synthetic watermelon game fire, think of never studied the development of the game before, this time want to take this opportunity to see JavaScript game development, from a native point of view how to achieve the physical characteristics of the game, such as motion, collision. Although I've studied physics-related animation libraries before, I'm going to try writing a simple JavaScript physics engine without a framework for the collision of small balls.

Why not use a ready-made game library? B ecause I feel that after understanding the underlying implementation principles, I can more effectively understand the concepts and usage methods of the framework, be more efficient in solving BUGs, and improve my coding skills. I n the study of JavaScript physics engine, found that writing code is secondary, the most important is to understand the relevant physics, mathematical formulas and concepts, although I am a science student, but mathematics and physics has never been my strong point, I did not return knowledge to the teacher, but never mastered. After spending a small half-year study of physics during the Chinese New Year, I still don't have much confidence in some concepts and derivation processes, but in the end it's a simple, more satisfying result, see figure below.

 JavaScript game development: Hands-on collision physics engine1

Let's take a look at how this works.

Infrastructure

We use canvas here to implement the JavaScript physics engine. Start by preparing the project's underlying files and styles, creating a new index.html, index.js, and style .css files for writing canvas html structures, engine code, and canvas styles, respectively.

Introduce style files in <head /> label for index .html:

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

In <body /> add the canvas element, load the index .js file:

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

This code defines <canvas /> element of id for gameboard and places it under <main /> element, <main /> element is primarily used to set the background color and canvas size. The index .js file is introduced below the <main/> element so that the code in the JS can be executed after the DOM load is complete.

The code in the style .css is as follows:

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

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

The style is simple, removing the margins and spacing of all elements, and setting the width and height of <main/> elements to the same as the browser-viewable area, with a dark gray background color.

hsl (hue, saturation, brightness) is one of the css color notations, with parameters for hue, saturation, and brightness.

Draw the ball

Next, draw the ball, mainly using canvas-related api.

In index .js, write the following code:

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();

The code mainly uses two-dimensional context for drawing:

  • Get the canvas element object from canvas's id.
  • To get the drawing context through the canvas element object, getContext() requires a parameter to indicate whether to draw a 2d image or to draw a 3d image using webgl, select 2d here. Context is similar to a brush that changes its color and draws basic shapes.
  • Set the width height of the canvas to the width height of the browser's visible area and save it to the width and height variables for later use.
  • Set the color to context, and then call beginPath() to start drawing.
  • Draw a circle using arc() method, which receives 5 parameters, the first two are the x, y coordinates of the center of the circle, the third is the radius length, and the fourth and fifth are the starting and ending angles, because arc() is actually used to draw an arc, where it draws an arc of 0 to 360 degrees, forming a circle. The angle here is expressed in radian form, and 0 to 360 degrees can be represented by 0 to 2 s Math.PI.
  • Finally, use ctx.fill() to color the circle.

This successfully draws a circle, and we treat it here as a small ball:

 JavaScript game development: Hands-on collision physics engine2

Move the ball

However, the ball is still at this time, if you want it to move, then you have to modify its center coordinates, the specific modification of the value is related to the speed of motion. Before you move the ball, take a look at how canvas animates:

Canvas animates in a similar way to traditional film, drawing images, updating image positions or shapes, clearing the canvas, redrawing images over time, and producing continuous images at 60 frames when performed 60 or more times in a row in 1 second.

In JavaScript, then, the browser provides window.requestAnimationFrame() method, which receives a callback function as an argument, and each time a callback function is executed, it is equivalent to 1 frame animation, and we need to call it continuously through recursion or looping, and the browser executes as many callback functions as possible in 1 second. With it, we can redraw canvas to make the ball move.

Because the call window.requestAnimationFrame() is basically ongoing, we can also call it a game loop.

Let's look at how to animate the infrastructure:

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

The process() function here is a callback function that is executed 60 times in 1 second and continues to call window.requestAnimationFrame(process) for the next loop after each execution. If you want to move the ball, you need to write the code that draws the ball and modifies the round x, y coordinates to the process() function.

To make it easier to update the coordinates, we save the center coordinates of the spheres to variables to make changes to them, and then define two new variables that represent the speed vx in the x-axis direction and the y-axis direction vy respectively, and then place the context-related drawing operation in 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);

To calculate the movement distance of the center coordinates x, y, we need speed and time, the speed is here, so how do you get the time? window.requestAnimationFrame() passes the number of milliseconds of the current time (i.e. timestamp) to the callback function, we can save the timestamp of this call, and then calculate how many seconds it takes to execute the 1 frame animation on the next call, and then calculate the distance of movement based on the number of seconds and the speed in the x, y axis direction, respectively, to add x and y to get the most recent position. Note that the time here is the time interval between the last function call and this function call, not how many seconds have passed from the first function call to the current function call, so it is equivalent to a time increment that needs to be added to the previous x and y values, as follows:

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() now receives the current timestamp as an argument, and then does the following:

  • Calculate the time interval between the last function call and this function call, in seconds, and record the timestamp of the call for the next calculation.
  • The distance of movement is calculated based on the speed in the x, y direction, and the time just calculated.
  • Call clearRect() to clear the rectangular area canvas, where the parameters, the first two are the upper left coordinates, the last two are wide and high, the canvas is passed in will clear the entire canvas.
  • Redraw the ball.

Now the ball can move:

 JavaScript game development: Hands-on collision physics engine3

Refactor the code

The above code is suitable for only one small ball situation, if there are more than one ball to draw, you have to write a lot of duplicate code, then we can abstract the ball into a class, inside there are drawing, update position and other operations, as well as coordinates, speed, radius and other properties, the refactored code is as follows:

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;
  }
}

The code inside, as before, is not repeated here, and it should be noted that the context brush properties of the Circle class are passed in through the constructor, and the code for the update location is placed in the update() method.

For the entire canvas drawing process, it can also be abstracted into a class, as a game or engine controller, for example, by placing it in a class called 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();

In the Gameboard class:

  • startTime saves the property of the timestamp that was last executed by the function and places it in the constructor.
  • init() method creates an array of circles with two sample circles in it, and there is no collision involved. T hen call window.requestAnimationFrame() to turn on the animation. Note that bind() is used here to bind this from Gameboard to callback functions to facilitate access to methods and properties in Gameboard
  • The process() method is also written here, traversing the array of small balls each time you execute, updating the position of each ball, then clearing the canvas and redrawing each ball.
  • Finally, you can start animating when you initialize the Gameboard object.

At this time there were two little balls moving.

 JavaScript game development: Hands-on collision physics engine4

Collision detection

In order to realize the physical characteristics of simulation, collisions between multiple objects will react accordingly, the first step is to detect collisions first. Let's add a few more balls to facilitate collisions, and add a few more in the init() method:

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),
];

Then add a collision state to the ball, and in the event of a collision, set the two balls to a different color:

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%)";
    // 其它代码
  }
}

Now to determine whether there is a collision between the small ball, this condition is very simple, to judge whether the distance between the two small ball center is less than the sum of the radius of the two small balls, if less than or equal to the collision occurred, greater than there is no collision. T he distance of the center of the circle is the distance between two coordinate points, which can be used in a formula:

 JavaScript game development: Hands-on collision physics engine5

x1, y1, and x2, y2 are the center coordinates of the two small balls, respectively. I n comparison, the radius and square operation can be performed, which in turn omits the open-side operation of the distance, i.e. it can be compared using the following formula:

 JavaScript game development: Hands-on collision physics engine6

r1 and r2 are the radius of two balls.

In the Circle class, first add an isCircleCollided(other) method, receive another ball object as an argument, and return the result of the comparison:

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;
}

Add checkCollideWith(other) method, call isCircleCollided(other) to determine the collision, and set the collision state of the two balls to true:

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

Then we need to use a two-loop two-to-two pair of small balls to detect collisions, and since the small ball array is stored in the Gameboard object, we add a checkCollision() method to detect the collision:

checkCollision() {
  // 重置碰撞状态
  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]);
    }
  }
}

Because the ball should bounce off immediately after the collision, we start by setting the collision state of all the balls to false, and then in the loop, each ball is detected. It is noted here that the inner loop starts with i and 1 because there is no need to judge 2 and 1 ball after determining whether 1 ball and 2 balls collide.

Then, in the process() method, perform the detection, noting that the detection should occur only after updating the sphere position with a for loop:

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

Now, you can see that the ball changes color when it collides.

 JavaScript game development: Hands-on collision physics engine7

Boundary collision

After the code above is executed, the ball will cross the boundary and run outside, so let's deal with the boundary collision first. D etecting boundary collisions requires that all four faces be processed to determine whether a collision has occurred with the boundary based on the center coordinates and radius of the circle. F or example, in a collision with the left boundary, the x coordinates of the center of the circle are less than or equal to the length of the radius, and in the case of a collision with the right boundary, the x coordinates of the center of the circle should be greater than or equal to the far right coordinate of the canvas (that is, the width value) minus the length of the radius. T he upper and lower boundaries are similar, using only the center y coordinates and the height values of the canvas. In the event of a collision in the horizontal direction (i.e. left and right boundary), the movement direction of the ball changes, requiring only the speed vy value in the vertical direction to be reversed, and the vx in the vertical direction.

 JavaScript game development: Hands-on collision physics engine8

Now take a look at the implementation of the code, add a checkEdgeCollision() method to the Gameboard class and write the following code based on the rules described above:

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;
    }
  });
}

In the code, in addition to reversing the speed, the coordinates of the ball are modified to close to the boundary to prevent exceeding. Next, add detection of boundary collisions in process()

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

This is when you can see that the ball bounces when it hits the boundary:

 JavaScript game development: Hands-on collision physics engine9

But the collision between the balls has not been handled, before processing, first review the basic operation of vectors, math students can skip directly, only look at the relevant code.

The basic operation of the vector

Because the velocity vector, or vector, needs to be manipulated in the event of a collision, the vector is represented in a coordinate-like form, such as < 3, 5 > (here with <> for vector), which has length and direction, and has certain rules for its operations, which need to be used in this tutorial for vector addition, subtraction, multiplication, point multiplication, and standardized operation.

Vectors add up to only the x and y coordinates of the two vectors, for example: < 3, 5 >, < 1, 2 >, < 4, 7 > <3, 5> , <1, 2> , <4, 7><3,5> , <1,2> , <4,7 >

Subtraction is similar to addition, subtracting x and y coordinates, e.g. < 3, 5 > , < 1, 2 > , < 2, 3 > <3, 5> - <1, 2> - <2, 3><3,5 > - <1,2> - <2,3 >

Multiplication, which refers here to the multiplication of vectors and scales, refers to ordinary numbers, and the result is to multiply x and y by the scales, for example: 3 × < 3, 5 >, < 9, 15 > 3 times<3, 5> - <9, 15>3×< 3,5 > - <9,15 >.

Point multiplication is a way of multiplying two vectors, similar to fork multiplication, but in this example, the dot multiplier is actually calculated as a projection of one vector on another vector, calculated by adding the product of two vectors x plus the product of y, which returns a scale, i.e. the first vector in the The length of the projection on 2 vectors, e.g. < 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

Standardization is to remove the length of the vector, leaving only the direction, such a vector its length is 1, called the unit vector, the process of  JavaScript game development: Hands-on collision physics engine10 standardization is to divide x and y by the length of the vector, because the vector represents and the origin (0, 0) distance, so you can calculate the length directly, for example, < 3, 4 > Standardized results are: < 3, 5 > ⋅ < 1, 2 > , 3 × 1 , 5 × 2 , 13 <3, 5> scot <1, 2> s 3 s times 1 s 5 s times 2 s 13< 3,5>⋅<1,2 > s 3× 1 s 5 × 2 s 13.

Once we understand the basic operations of vectors, let's create a Vector tool class to make it easier for us to perform vector operations, and its code is to implement these rules:

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);
  }

  /**
   * 向量与标量乘法
   * @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;
  }

  /**
   * 向量标准化(除去长度)
   * @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);
  }
}

There's no special syntax or manipulation in the code, so let's not go into it here, so let's look at the collision of the balls.

Collision handling

The most important part of collision handling is to calculate the speed and direction after a collision. U sually the simplest collision problem is the collision of two objects on the same horizontal surface, called a one-dimensional collision, because at this time only need to calculate the speed in the same direction, and we now program the ball is moving in a two-dimensional plane, the probability of a frontal collision between the balls (i.e. in the same direction of motion) is very small, most of it is oblique (in different directions of motion rub shoulder), need to calculate both horizontal and vertical direction of speed and direction, which is a two-dimensional collision problem. However, in fact, the collision between small balls, only in the heart line (two round-centered wire) has a force, and in the direction of the tangent of collision contact there is no force, then we only need to know the velocity change in the direction of the heart line on it, so it is converted into a one-dimensional collision.

 JavaScript game development: Hands-on collision physics engine11

When calculating the speed after a collision, observe the law of momentum conservation and the law of kinetic energy conservation, and the formulas are:

The law of momentum conservation

 JavaScript game development: Hands-on collision physics engine12

The law of kinetic energy conservation

 JavaScript game development: Hands-on collision physics engine13

m1 and m2 are the mass of the two small balls, v1 and v2 are the velocity vectors before the collision of the two small balls, and v1' and v2' are the velocity vectors after the collision. B ased on these two formulas, the speed formula after the collision of two small balls can be derived:

 JavaScript game development: Hands-on collision physics engine14

If you do not consider the quality of the ball, or the same quality, in fact, is the two small ball speed interchange, that is:

 JavaScript game development: Hands-on collision physics engine15

Here we add quality to the ball, and then use a formula to calculate the velocity after the ball collides, first adding the mass attribute of mass mass to the ball in the Circle class:

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

Then add quality to each ball at the initialized ball of the Gameboard class:

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),
];

Adding the changeVelocityAndDirection(other) to the Circle class to calculate the speed after a collision, which receives another small ball object as an argument, and calculates the speed and direction of the two small balls colliding thick, which is the core of the entire engine, let's look at how it is implemented. First, the speed of the two small balls is represented by the Vector vector:

  changeVelocityAndDirection(other) {
    // 创建两小球的速度向量
    let velocity1 = new Vector(this.vx, this.vy);
    let velocity2 = new Vector(other.vx, other.vy);
  }

Because we already use vx and vy to represent velocity vectors horizontally and vertically, we can pass them directly to Vector's constructor. velocity1 and velocity2 represent the current velocity vectors for the current ball and the collision sphere, respectively.

Next, get the vector of the direction of the concentric line, which is the difference between the two center coordinates:

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

Next, get the unit vector in the direction of the centerline and the unit vector in the direction of the tangent, which represents the direction of the centerline and tangent:

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

unitVNorm is the concentric direction unit vector, unitVTan is the tangent direction unit vector, the tangent direction is actually the x, y coordinates of the conjoined heart vector, and the y coordinate is reversed. Based on these two unit vectors, use point multiplication to calculate the projection of the velocity of the ball in both directions:

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

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

The result of the calculation is a scale, i.e. a speed value without direction. v1n and v1t represent the speed value of the current ball in the direction of the heartline and tangent, while v2n and v2t represent the speed value of colliding with the trophies. After calculating the speed value of the two small balls, we have the variable values needed for the speed formula after the collision, and apply the formula directly to the code:

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 and v2nAfter are the speeds after two small balls collide, so you can now tell if v1nAfter is less than v2nAfter then the first ball and the second ball will get farther and farther away, so you don't have to deal with collisions:

if (v1nAfter < v2nAfter) {
  return;
}

Then add direction to the speed after the collision, calculate the speed in the direction of the connecting heart line and tangent direction, only need to let the speed scale and the connected heart line unit vector and tangent unit vector multiplied:

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

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

In this way, with the new velocity vector on the two small spheres connected to the center of the line and the new velocity vector in the tangent direction, and finally the velocity vector on the center line and the speed vector in the tangent direction can be added to the operation, we can obtain the speed vector of the small ball after the collision:

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

We then restore x and y in the vector to the vx and vy properties of the ball, respectively:

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

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

Finally, this method is called in the if statement of the checkCollideWith() method, i.e. when a collision is detected:

checkCollideWith(other) {
  if (this.isCircleCollided(other)) {
    this.colliding = true;
    other.colliding = true;
    this.changeVelocityAndDirection(other); // 在这里调用
  }
}

At this point, the collision effect of the ball is realized.

 JavaScript game development: Hands-on collision physics engine16

Inelastic collisions

Now the collision between the balls is a fully elastic collision, i.e. there is no loss of energy after the collision, so that the ball never stops moving, and we can let the ball lose a little energy after the collision to simulate a more realistic physical effect. For a small ball to lose energy after a collision, a recovery factor can be used, which is a value ranging from 0 to 1, multiplied by it after each collision to slow down the speed, if the recovery factor is 1 is a fully elastic collision, 0 is a completely inelastic collision, the value between the values is inelastic collision, real-life collisions are inelastic collisions.

First look at the boundary collision, which is relatively simple, assuming that the recovery factor of the boundary is 0.8, and then multiply it each time the speed is reversed, and make the following changes to the Gameboard checkEdgeCollision() method:

  checkEdgeCollision() {
    const cor = 0.8;                  // 设置恢复系统
    this.circles.forEach((circle) => {
      // 左右墙壁碰撞
      if (circle.x < circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢复系数
        circle.x = circle.r;
      } else if (circle.x > width - circle.r) {
        circle.vx = -circle.vx * cor; // 加上恢复系数
        circle.x = width - circle.r;
      }

      // 上下墙壁碰撞
      if (circle.y < circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢复系数
        circle.y = circle.r;
      } else if (circle.y > height - circle.r) {
        circle.vy = -circle.vy * cor; // 加上恢复系数
        circle.y = height - circle.r;
      }
    });
  }

Next, set the recovery factor for the ball, add a recovery factor cor property to the Circle class, and each ball can set a different value to give them different elasticity, and then set a random recovery factor when initializing the ball:

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),
    ];
  }
}

After adding the recovery factor, the speed calculation after the small ball collision also needs to be changed, you can simply make v1nAfter and v2nAfter multiply the recovery factor of the ball, you can also use the speed formula with recovery factor (these two ways I am not clear for the time being, interested small partners can study it themselves), the formula is as follows:

 JavaScript game development: Hands-on collision physics engine17

The formula is then converted to code, replacing the formulas v1nAfter and v2nAfter in the changeVelocityAndDirection() method of the Circle class:

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);

It should be noted here that the recovery factor when two small balls collide should take the minimum value of both, according to common sense, small elasticity, whether to hit someone else or someone else hit it, will have the same effect. N ow the speed slows down after the ball collides, but it's a little bit worse, we can add gravity to let the ball fall naturally.  JavaScript game development: Hands-on collision physics engine18

gravity

Adding gravity is simple, first by defining the gravity acceleration constant globally, and then by adding gravity acceleration when the sphere updates the speed in the vertical direction:

const gravity = 980;

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

Gravitational  JavaScript game development: Hands-on collision physics engine19 acceleration is about , but since our canvas is in pictographs, using 9.8 will look like there is no gravity, or it will look like a small ball from a distance, at which point the acceleration of gravity can be magnified by a certain number of times to achieve a more realistic effect.

 JavaScript game development: Hands-on collision physics engine20

summary

Now that our simple JavaScript physics engine is complete, the most basic part of the physics engine can have a complete drop and collision effect, to do a more realistic physics engine also need to consider more factors and more complex formulas, such as friction, air resistance, rotation angle after collision, and so on, and this canvas frame rate will also have some problems, if there is a small ball speed too fast, But if it's too late to execute the next callback function to update its position, it may go straight through the colliding ball to the other side.

To summarize the development process:

  • Draw the ball using context.
  • Build a Canvas animation infrastructure that uses window.requestAnimationFrame method to repeat callback functions.
  • Move the ball and calculate the distance of movement by the speed of the ball and the timestamp at which the function executes.
  • Collision detection, by comparing the distance between two small balls and the sum of their radius.
  • Detection and direction change of boundary collision.
  • The collision between the balls, the speed formula and vector operation are applied to calculate the speed and direction after the collision.
  • Inelastic collisions are achieved using recovery factors.
  • Add gravity effects.

The code can be viewed at:

https://github.com/zxuqian/html-css-examples/tree/master/35-collision-physics

Recommended lessons: JavaScript micro-class, JavaScript basic combat, JavaScript object-oriented programming