JavaScript/练习/碰撞
外观
< JavaScript | 练习
当物体在周围移动时,它们以一定的速度移动。 因此,您应该在物体的 class
中添加两个属性来表示它在 x 方向和 y 方向上的速度。 正值表示向右或向下的方向,负值表示向左或向上的方向。 此外,该类需要修改速度的函数。
物体可能会与其他物体或画布边界发生碰撞。 检测此类碰撞的算法取决于物体的类型:对于矩形而言,关于 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>
该示例与上面的示例相同,增加了检测障碍物(矩形)的功能。 如果球与障碍物发生碰撞,游戏将停止。
碰撞由函数 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>