May 30, 2021 Article blog
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.
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.
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:
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.
width
and
height
variables for later use.
beginPath()
to start drawing.
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.
ctx.fill()
to color the circle.
This successfully draws a circle, and we treat it here as a small 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:
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.
Now the ball can move:
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
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.
Gameboard
object.
At this time there were two little balls moving.
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:
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:
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.
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.
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:
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 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.
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.
When calculating the speed after a collision, observe the law of momentum conservation and the law of kinetic energy conservation, and the formulas are:
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:
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:
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.
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:
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.
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 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.
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:
window.requestAnimationFrame
method to repeat callback functions.
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