Has anyone else noticed just how bad the platformer game from the JavaScript and Data Structures Certification is? Specifically, the movement and collision engine. You can fly by holding spacebar/up, if you keep going to the right you eventually leave the screen, etc. Overall, it looks like the code is there to fix most of these, but none of it is working as intended. Additionally, there are several concepts covered in the previous projects that would be beneficial to this project but seem to be ignored (deconstruction being one of them). The biggest culprit seems to be that they are adjusting the players velocity in 3 or 4 sections of the code instead of centralizing it.
So I, in the interest of getting more practice, fixed it. I tried to keep the overall structure similar to what they had, and in cases where it didn’t impede the legibility or functionality of the game, I tried to keep the same methods. I also limited myself to the concepts that they had already covered to this point. In the end, the keypress system, movement system, collision system, etc. all got a massive revamp. And it looks like like my code is any longer than the original. If anything, it seems to be more succinct.
So, here’s my JS code (didn’t change the HTML/CSS). Feel free to provide thoughts on any improvements I missed or changes you would add.
const startBtn = document.getElementById("start-btn");
const canvas = document.getElementById("canvas");
const startScreen = document.querySelector(".start-screen");
const checkpointScreen = document.querySelector(".checkpoint-screen");
const checkpointMessage = document.querySelector(".checkpoint-screen > p");
const ctx = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;
const margin = {
left: canvas.width / 4,
right: canvas.width / 4,
top: 10,
bottom: 10,
};
const gravity = 0.5;
/* Define classes and initialize objects*/
class Platform {
constructor(x, y) {
this.width = 200;
this.height = 40;
this.position = {
x,
y,
};
}
draw() {
ctx.fillStyle = "#acd157";
ctx.fillRect(this.position.x, this.position.y, this.width, this.height);
}
}
const platformPositions = [
{ x: 400, y: 450 },
{ x: 600, y: 400 },
{ x: 800, y: 350 },
{ x: 950, y: 150 },
{ x: 2400, y: 450 },
{ x: 2800, y: 400 },
{ x: 3050, y: 350 },
{ x: 3800, y: 450 },
{ x: 4100, y: 400 },
{ x: 4300, y: 200 },
{ x: 4600, y: 150 },
];
const platforms = platformPositions.map(({ x, y }) => new Platform(x, y));
class CheckPoint {
constructor(x, y) {
this.width = 40;
this.height = 70;
this.position = {
x,
y,
};
}
draw() {
ctx.fillStyle = "#f1be32";
ctx.fillRect(this.position.x, this.position.y, this.width, this.height);
}
}
const checkpointPositions = [
{ x: 1070, y: 80 },
{ x: 2800, y: 330 },
{ x: 4700, y: 80 },
];
const checkpoints = checkpointPositions.map(({ x, y }) => new CheckPoint(x, y));
//Checkpoints are removed when they are collided with. Game is over when there are no more checkpoints
const isGameActive = () => checkpoints.length > 0;
class Player {
constructor() {
this.width = 40;
this.height = 40;
this.position = {
x: 0,
y: canvas.height - this.height - margin.bottom,
};
this.velocity = {
x: 0,
y: 0,
};
this.acceleration = 1; //additive
this.deceleration = 0.9; //multiplicative. Speed cap will be acceleration/(1-deceleration)
this.jumpForce = 20; //additive, may need to be adjusted depending on screen size
this.isGrounded = true;
}
draw() {
ctx.fillStyle = "#99c9ff";
ctx.fillRect(this.position.x, this.position.y, this.width, this.height);
}
move() {
this.verticalMovement();
this.horizontalMovement();
}
verticalMovement() {
//Update vertical velocity
if(!this.isGrounded) {
this.velocity.y += gravity;
} else if (keys.up && isGameActive()){
this.velocity.y -= this.jumpForce;
}
//Update vertical position
this.position.y += this.velocity.y;
//Assume the player is no longer grounded
this.isGrounded = false;
//Check for vertical platform collisions
platforms.forEach((platform) => {
const topsideCollisionRules = [
this.position.x + this.width > platform.position.x,
this.position.x < platform.position.x + platform.width,
this.position.y < platform.position.y,
this.position.y + this.height > platform.position.y,
];
const undersideCollisionRules = [
this.position.x + this.width > platform.position.x,
this.position.x < platform.position.x + platform.width,
this.position.y > platform.position.y,
this.position.y < platform.position.y + platform.height,
];
if (topsideCollisionRules.every((rule) => rule)) {
this.position.y = platform.position.y - this.height;
this.velocity.y = 0;
this.isGrounded = true;
} else if (undersideCollisionRules.every((rule) => rule)) {
this.position.y = platform.position.y + platform.height;
this.velocity.y = 0;
}
});
//Check for top and bottom collisions
if (this.position.y < margin.top) {
this.position.y = margin.top;
this.velocity.y = 0;
} else if (this.position.y + this.height >= canvas.height - margin.bottom) {
this.position.y = canvas.height - this.height - margin.bottom;
this.velocity.y = 0;
this.isGrounded = true;
}
}
horizontalMovement() {
//Update horizonal velocity
if (keys.right && isGameActive()) {
this.velocity.x += this.acceleration;
}
if (keys.left && isGameActive()) {
this.velocity.x -= this.acceleration;
}
//Both brings the player to a stop if not accelerating, and forces a speed cap
this.velocity.x *= this.deceleration;
//Update position
this.position.x += this.velocity.x;
//Check for horizontal platform collisions
platforms.forEach((platform) => {
const frontalCollisionRules = [
this.position.y + this.height > platform.position.y,
this.position.y < platform.position.y + platform.height,
this.position.x < platform.position.x,
this.position.x + this.width > platform.position.x
];
const rearCollisionRules = [
this.position.y + this.height > platform.position.y,
this.position.y < platform.position.y + platform.height,
this.position.x > platform.position.x,
this.position.x < platform.position.x + platform.width
];
if (frontalCollisionRules.every(rule => rule)) {
this.position.x = platform.position.x - this.width;
this.velocity.x = 0;
} else if (rearCollisionRules.every(rule => rule)) {
this.position.x = platform.position.x + platform.width;
this.velocity.x = 0;
}
});
//Check left and right border collisions and set necessary adjustment for shifting
const horizonalAdjustment =
this.position.x < margin.left
? margin.left - this.position.x
: this.position.x + this.width > canvas.width - margin.right
? canvas.width - margin.right - this.position.x - this.width
: 0;
//Shift if necessary
if(horizonalAdjustment !== 0) {
checkpoints.forEach((checkpoint) => {
checkpoint.position.x += horizonalAdjustment;
});
platforms.forEach((platform) => {
platform.position.x += horizonalAdjustment;
});
this.position.x += horizonalAdjustment;
}
}
evaluateCheckpoints() {
checkpoints.forEach((checkpoint, index) => {
const collisionRules = [
this.position.x + this.width > checkpoint.position.x,
this.position.x < checkpoint.position.x + checkpoint.width,
this.position.y + this.height > checkpoint.position.y,
this.position.y < checkpoint.position.y + checkpoint.height
];
if (collisionRules.every(rule => rule)) {
//Remove checkpoint
checkpoints.splice(index, 1);
//Display checkpointMessage
checkpointScreen.style.display = "block";
if (isGameActive()) {
checkpointMessage.innerText = "You reached a checkpoint!";
setTimeout(() => (checkpointScreen.style.display = "none"), 2000);
} else {
checkpointMessage.textContent = "You reached the final checkpoint!";
}
}
});
}
}
const player = new Player();
/* Define animation sequence */
const animate = () => {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
player.move();
player.evaluateCheckpoints();
player.draw();
platforms.forEach((platform) => {
platform.draw();
});
checkpoints.forEach((checkpoint) => {
checkpoint.draw();
});
};
/* Event listeners */
//Start game
startBtn.addEventListener("click", () => {
canvas.style.display = "block";
startScreen.style.display = "none";
animate();
});
//Key press and release
const keys = {
right: false,
left: false,
up: false,
};
const updateKey = (key, isPressed) => {
switch (key) {
case "ArrowLeft":
keys.left = isPressed;
break;
case "ArrowUp":
case " ":
case "Spacebar":
keys.up = isPressed;
break;
case "ArrowRight":
keys.right = isPressed;
}
};
window.addEventListener("keydown", ({ key }) => {
updateKey(key, true);
});
window.addEventListener("keyup", ({ key }) => {
updateKey(key, false);
});