JavaScript/Exercises/MovingWalls
Appearance
< JavaScript | Exercises
The example combines some of the shown features:
- It contains a class
Rect
and its sub-classSmiley
. - 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
anddetectRectangleCollision
. 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.
Click to see solution
<!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>