411 REFERENCE · Wikibooks
JavaScript/Exercises/MovingWalls
Reference material by Wikibooks contributors. This retains its source identity and is not 411 news reporting.
The example combines some of the shown features:
It contains a class Rect and its sub-class Smiley.
Some walls with a rectangular shape move from right to left across the scene. They reappear on the right side after reaching the left border.
Two smileys populate the scene.
One smiley can be moved by using the buttons or the keyboard keys 'ArrowRight', ... .
Two helper-functions support the detection of rectangles: detectBorderCollision and detectRectangleCollision. The first one detects the collision of a graphical object with a surrounding rectangle, e.g., the canvas. The second one detects a collision of two rectangles.
You can extend the example in various ways:
Create walls randomly Math.random.
Introduce some kind of a 'level' by changing the speed, the number, the size, or the shape of walls.
100% <!DOCTYPE html>
<html>
<head>
<title>Collision</title>
<style>
.grid-container {
display: grid;
justify-content: left;
grid-template-columns: auto auto auto auto auto auto;
gap: 20px;
margin-left: 1em;
margin-top: 2em;
}
</style>
<script>
"use strict";
// ----------------------------------------------------------------------------
// class Rect (should be implemented in a separate file 'Rect.js')
// ----------------------------------------------------------------------------
class Rect {
// constructor with default values
constructor(context, x = 0, y = 0, width = 10, height = 10, color = "red") {
this.ctx = context;
this.x = x;
this.y = y;
this.width = width
this.height = height;
this.color = color;
// movement
this.speedX = 0;
this.speedY = 0;
}
// set the speed (= step size)
setSpeed(x, y) {
this.speedX = x;
this.speedY = y;
}
// change the position according to the speed
move() {
this.x += this.speedX;
this.y += this.speedY;
}
render() {
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
} // end of class
// ----------------------------------------------
// class 'Smiley'. It's derived from 'Rect' to use
// - the MBB for collision detection
// - methods of 'Rect' for movements
// -----------------------------------------------
class Smiley extends Rect {
// constructor with default values
constructor(context, text = "?", x = 10, y = 10, width = 30, height = 30) {
//
super (context, x, y, width, height);
this.ctx = context;
this.text = text;
}
render() {
this.ctx.font = "30px Arial";
this.ctx.fillText(this.text, this.x, this.y);
//this.ctx.fillRect(this.x, this.y-25, this.width, this.height);
}
} // end of class
// -------------------------------------------------------------
// constants and variables which are known in the complete file
// -------------------------------------------------------------
// global constants
const SPEED_WALLS = -0.3; // px
const SPEED_SMILEY = 1; // px
// global variables
let obstacles = [];
let he, she;
let requestId, stopFlag;
// ----------------------------------------------------
// functions
// ----------------------------------------------------
function start() {
// init all graphical objects
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
obstacles[0] = new Rect(context, 200, 80, 10, 180, "red");
obstacles[1] = new Rect(context, 350, 20, 10, 100, "red");
obstacles[2] = new Rect(context, 500, 80, 10, 200, "red");
for (let i = 0; i < obstacles.length; i++) {
obstacles[i].setSpeed(SPEED_WALLS, 0);
}
he = new Smiley(context, "\u{1F60E}", 20, canvas.height - 40);
she = new Smiley(context, "\u{1F60D}", canvas.width - 50, canvas.height - 40);
// (re-)start the game
if (requestId) {
// cancel old frame chain
cancelAnimationFrame(requestId);
}
stopFlag = false;
playTheGame(canvas, context);
}
// the game's logic
function playTheGame(canvas, context) {
// collision of 'he' with border?
// y-direction is different because (x,y) of text is at left/bottom
const [crashL, crashR, crashT, crashB] = detectBorderCollision(
0, 0, canvas.width, canvas.height,
he.x, he.y-25, he.width, he.height);
if (!crashL && !crashR && !crashT && !crashB) {
he.move();
} else {
switch (true) {
// move back a single px
case crashL:
he.x++;
break;
case crashR:
he.x--;
break;
case crashT:
he.y++;
break;
case crashB:
he.y--;
break;
}
}
for (let i = 0; i < obstacles.length; i++) {
// collision of 'he' with wall ?
// y-direction is different because (x,y) of text is at left/bottom
if (detectRectangleCollision(
obstacles[i].x, obstacles[i].y, obstacles[i].width, obstacles[i].height,
he.x, he.y-25, he.width, he.height)) {
stopFlag = true;
continue;
}
// if an obstacle reaches the left border, restart at the right side
if (obstacles[i].x <= 0) {
obstacles[i].x = canvas.width - 100;
}
obstacles[i].move();
}
// 'he' and 'she' meets?
if (detectRectangleCollision(
she.x, she.y-25, she.width, she.height,
he.x, he.y-25, he.width, he.height)) {
alert("Hola chica");
stopFlag = true;
}
renderAll(canvas, context);
}
// rendering consists off:
// - clear the complete scene
// - re-paint the complete scene
// - call the game's logic again via requestAnimationFrame()
function renderAll(canvas, context) {
if (stopFlag) {
// cancel the old animation and don't request frames
cancelAnimationFrame(requestId);
return;
}
// clear complete canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// draw objects at their current position
for (let i = 0; i < obstacles.length; i++) {
obstacles[i].render();
}
he.render();
she.render();
// request a new frame with a parameter that re-starst the game's logic
// (and lastly leads to this line again)
requestId = window.requestAnimationFrame(() => playTheGame(canvas, context));
}
// control the game via buttons
function stopEvent() {
stopFlag = true;
}
function leftEvent() {
he.setSpeed(-SPEED_SMILEY, 0);
}
function rightEvent() {
he.setSpeed(+SPEED_SMILEY, 0);
}
function upEvent() {
he.setSpeed(0, -SPEED_SMILEY);
}
function downEvent() {
he.setSpeed(0, +SPEED_SMILEY);
}
// control the game via keybord
function keyDownEvent(event) {
switch (event.code) {
case 'ArrowUp':
upEvent();
break;
case 'ArrowDown':
downEvent();
break;
case 'ArrowLeft':
leftEvent();
break;
case 'ArrowRight':
rightEvent();
break;
}
}
function keyUpEvent(event) {
switch (event.code) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
he.setSpeed(0, 0);
}
}
// ----------------------------------------------------------------------------
// helper functions (should be in a separate file, e.g.: 'tools.js')
// ----------------------------------------------------------------------------
function detectBorderCollision(borderX, borderY, borderWidth, borderHeight,
rectX, rectY, rectWidth, rectHeight)
{
// the rectangle touches the (outer) border, if x <= borderX, ...
let collisionLeft = false;
let collisionRight = false;
let collisionTop = false;
let collisionBottom = false;
if (rectX <= borderX ) {collisionLeft = true}
if (rectX + rectWidth >= borderX + borderWidth ) {collisionRight = true}
if (rectY <= borderY ) {collisionTop = true}
if (rectY + rectHeight >= borderY + borderHeight ) {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 onload="start()" onkeydown="keyDownEvent(event)" onkeyup="keyUpEvent(event)" >
<h4 style="margin-left:1em; text-align:center">Initiate motions by buttons or keyboard keys (ArrowUp, ...)</h4>
<!-- the drawing area -->
<canvas id="canvas" width="700" height="300"
style="margin-left:1em; background-color:yellow" >
</canvas>
<!-- a grid for the buttons: 3 rows, 6 columns per row -->
<div class="grid-container">
<div></div>
<div></div>
<div></div>
<div></div>
<button onclick="upEvent()">Up</button>
<div></div>
<button onclick="start()">Reset</button>
<button onclick="stopEvent()">Stop</button>
<div style="margin-right: 2em"></div>
<button onclick="leftEvent()">Left</button>
<div></div>
<button onclick="rightEvent()">Right</button>
<div></div>
<div></div>
<div></div>
<div></div>
<button onclick="downEvent()">Down</button>
<div></div>
</div>
</body>
</html>