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>