I'm building a space invaders game and the code below works, the ball is fired and if it hits the enemy the isDead() function is triggered which also switches dead to true for that ball and this is also passed to the enemy class so it causes the enemy to get destroyed. however when I increase the ball frequency the enemy isDead function fails to run, I'm really not sure why when the ball interval is higher this whole system breaks.
when this.newBallInterval = 700 the enemy square dies
when this.newBallInterval = 600 it doesn't
why? and how to fix?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
background-color: aquamarine;
}
</style>
</head>
<body>
<canvas height="300" width="300"></canvas>
</body>
<script>
class Entity {
constructor(x, y) {
this.dead = false;
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if (a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if (b.collision === 'none') { d
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if (a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
}
if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
static testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.dead = false;
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({ deltaTime }) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fillStyle = "#1ee511";
context.fill()
}
isDead(enemy) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithEnemy = Entity.testCollision(enemy, this)
if (outOfBounds) {
return true
}
if (collidesWithEnemy){
//console.log('dead')
this.dead = true;
return true
}
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speed = 0;
this.y = y;
}
update() {
this.x += this.speed;
if (this.x > canvas.width - this.width) {
this.speed -= 5;
}
if (this.x === 0) {
this.speed += 5;
}
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x, this.y, this.width, this.height);
context.fillStyle = "#9995DD";
context.fill();
context.closePath();
}
isDead(enemy, ball) {
//// collision detection
// const collidesWithEnemy = Entity.testCollision(enemy, ball)
// if (collidesWithEnemy){
// console.log('enemy dead')
// game.hitEnemy();
// return true
// }
if (ball.dead){
console.log('enemy dead')
game.hitEnemy();
return true
}
}
}
class Paddle extends Entity {
constructor(x, width) {
super(150, 300)
this.collision = 'rect'
this.speed = 200
this.height = 10
this.width = 50
}
update({ deltaTime, inputs }) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
context.fillStyle = "#0095DD";
context.fill();
context.closePath();
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if (this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if (this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 700 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('end game')
}
hitEnemy() {
const endGame = 1;
game.loop(endGame)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(100, 20)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
//start watching inputs
this.inputsManager = new InputsManager()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// we now pass more data to the update method so that entities that need to can also read from our InputsManager
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
// update every entity
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if (this.lastBallCreated + this.newBallInterval < newTime) {
// this is quick and dirty, you should put some more thought into `x` and `y` here
this.ball = new Ball(this.player.x, 280)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}0
//draw entities
this.entities.forEach(entity => entity.draw(this.context))
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy, this.ball)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if(endGame){
cancelAnimationFrame(this.myLoop);
this.endGame();
return;
}
this.update()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>
The problem is that you are overwriting this.ball, so your enemy only checks against the newly spawned one, which is not dead yet.
You could simply store all the balls in their own Array and check all of them in your enemy.isDead() method:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
background-color: aquamarine;
}
</style>
</head>
<body>
<canvas height="300" width="300"></canvas>
</body>
<script>
class Entity {
constructor(x, y) {
this.dead = false;
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if (a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if (b.collision === 'none') { d
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if (a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
}
if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
static testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.dead = false;
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({ deltaTime }) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fillStyle = "#1ee511";
context.fill()
}
isDead(enemy) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithEnemy = Entity.testCollision(enemy, this)
if (outOfBounds) {
return true
}
if (collidesWithEnemy){
//console.log('dead')
this.dead = true;
return true
}
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speed = 0;
this.y = y;
}
update() {
this.x += this.speed;
if (this.x > canvas.width - this.width) {
this.speed -= 5;
}
if (this.x === 0) {
this.speed += 5;
}
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x, this.y, this.width, this.height);
context.fillStyle = "#9995DD";
context.fill();
context.closePath();
}
isDead(enemy, balls) {
//// collision detection
// const collidesWithEnemy = Entity.testCollision(enemy, ball)
// if (collidesWithEnemy){
// console.log('enemy dead')
// game.hitEnemy();
// return true
// }
if (balls.some(ball => ball.dead)){
console.log('enemy dead')
game.hitEnemy();
return true
}
}
}
class Paddle extends Entity {
constructor(x, width) {
super(150, 300)
this.collision = 'rect'
this.speed = 200
this.height = 10
this.width = 50
}
update({ deltaTime, inputs }) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
context.fillStyle = "#0095DD";
context.fill();
context.closePath();
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if (this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if (this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.balls = [];
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 300 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('end game')
}
hitEnemy() {
const endGame = 1;
game.loop(endGame)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(100, 20)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
//start watching inputs
this.inputsManager = new InputsManager()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// we now pass more data to the update method so that entities that need to can also read from our InputsManager
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
// update every entity
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if (this.lastBallCreated + this.newBallInterval < newTime) {
// this is quick and dirty, you should put some more thought into `x` and `y` here
const newBall = new Ball(this.player.x, 280);
this.balls.push( newBall );
this.entities.push( newBall )
this.lastBallCreated = newTime
}0
//draw entities
this.entities.forEach(entity => entity.draw(this.context))
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy, this.balls)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if(endGame){
cancelAnimationFrame(this.myLoop);
this.endGame();
return;
}
this.update()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>
But honestly the whole logic seems odd here and I'm afraid you find out you'll need to rewrite a lot of it later on (e.g when you'll want to have more than a single enemy), but doing this for you would be too much for an SO answer.
Related
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
class Position {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Player {
constructor() {
this.position = new Position(250, 460)
this.radius = 20;
this.speed = 5;
this.color = "teal";
this.lives = 1;
// Belongs to the handleKeyUp and handleKeyDown functions
this.right = false;
this.left = false;
}
}
let player = new Player();
// Creating a class to handle the enemy balls
class Enemy {
constructor() {
this.position = { x: Math.random() * width, y: 5 };
this.radius = 20;
this.velocity = { x: 0, y: (Math.random() * 8 + 4) }
this.color = "red";
}
move() {
this.position.x += this.velocity.x / 5;
this.position.y += this.velocity.y / 4;
}
draw() {
context.beginPath();
context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
context.fillStyle = this.color;
context.fill();
}
}
let enemy = new Enemy();
function isCircleOutside(entity) {
return (entity.position.x < - entity.radius ||
entity.position.x > width + entity.radius ||
entity.position.y < - entity.radius ||
entity.position.y > height + entity.radius);
}
//Creating our main player
function drawCircle(entity) {
context.beginPath();
context.arc(entity.position.x, entity.position.y, entity.radius, 0, Math.PI * 2);
context.fillStyle = entity.color;
context.fill();
}
//Handles the A and D keyboard buttons
function handleKeyDown(event) {
if (event.key === "a") {
player.left = true;
}
else if (event.key === "d") {
player.right = true;
}
}
function handleKeyUp(event) {
if (event.key === "a") {
player.left = false;
}
else if (event.key === "d") {
player.right = false;
}
}
//Makes sure that the player ball does not go outside the canvas
function handlePlayerMovement(player) {
if (player.right && player.position.x < canvas.width - player.radius)
player.position.x += player.speed;
if (player.left && player.position.x > player.radius)
player.position.x -= player.speed;
}
function circleCollision(circle1, circle2) {
let dx = circle1.position.x - circle2.position.x;
let dy = circle1.position.y - circle2.position.y;
let distance = Math.sqrt((dx * dx) + (dy * dy));
return distance < circle1.radius + circle2.radius;
}
window.addEventListener("keypress", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
let enemyArr = [];
let frameCount = 120;
function tick() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawCircle(player);
handlePlayerMovement(player);
frameCount++;
if (frameCount >= 120) {
frameCount = 0;
enemyArr.push(new Enemy(enemy.position.x, enemy.position.y, enemy.velocity, enemy.radius, enemy.color));
}
for (let i = 0; i < enemyArr.length; i++) {
let enemy = enemyArr[i];
enemy.move();
enemy.draw();
if (isCircleOutside(enemy)) {
enemyArr.splice(i, 1);
continue;
}
if (circleCollision(enemy, player)) {
enemyArr.splice(i, 1);
player.lives -= 1;
if (player.lives <= 0) {
alert("GAME OVER");
document.location.reload();
clearInterval(interval);
return;
}
continue;
}
}
requestAnimationFrame(tick);
}
tick();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <link rel="stylesheet" href="style.css"> -->
</head>
<body>
<canvas id="canvas" width="500" height="500" style="border: 1px solid rgb(0, 0, 0)"></canvas>
<script src="volcano.js"></script>
</body>
</html>
This my second version of the game. With the help I got here, I was able to finish the game the way I wanted to be. Maybe there are some bugs in it that I cannot see. The game is pretty simple, the player can only move the main ball to the left and right at the bottom of the canvas. After a few seconds random balls will start to fall down from the top and the player has to avoid touching the enemy balls. Otherwise the game is over with an alert window saying "Game Over" and you can simply restart the game.
(This is the first part of my question)
I'm trying to create a game where the player has to dodge the balls coming down from the top of the canvas. I have been able to make the player move to the right and left but now I'm stuck in the part where I have to create the falling balls. I know that I have to create an empty array where I store all the falling balls but how do I create a function to generate the positions? I would appreciate any suggestions or ideas to get me going, thank you!
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
class Position {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Player {
constructor() {
this.position = new Position(250, 460)
this.radius = 20;
this.speed = 5;
this.color = "teal";
// Belongs to the handleKeyUp and handleKeyDown functions
this.right = false;
this.left = false;
}
}
let player = new Player();
// Creating a class to handle the enemy balls (stuck!)
class Enemy {
constructor(position, velocity) {
this.position = position;
this.radius = 10;
this.velocity = velocity;
this.color = "red";
}
}
//Creating our main player
function drawCircle(entity) {
context.beginPath();
context.arc(entity.position.x, entity.position.y, entity.radius, 0, Math.PI * 2);
context.fillStyle = entity.color;
context.fill();
}
//Handles the A and D keyboard buttons
function handleKeyDown(event) {
if (event.key === "a") {
player.left = true;
}
else if (event.key === "d") {
player.right = true;
}
}
function handleKeyUp(event) {
if (event.key === "a") {
player.left = false;
}
else if (event.key === "d") {
player.right = false;
}
}
window.addEventListener("keypress", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
//Makes sure that the player ball does not go outside the canvas
function handlePlayerMovement(player) {
if (player.right && player.position.x < canvas.width - player.radius)
player.position.x += player.speed;
if (player.left && player.position.x > player.radius)
player.position.x -= player.speed;
}
function tick() {
context.clearRect(0, 0, canvas.width, canvas.height);
drawCircle(player);
handlePlayerMovement(player);
requestAnimationFrame(tick);
}
tick();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <link rel="stylesheet" href="style.css"> -->
</head>
<body>
<canvas id="canvas" width="500" height="500" style="border: 1px solid rgb(0, 0, 0)"></canvas>
<script src="volcano.js"></script>
</body>
</html>
Every now and then create an Enemy with a random position (x,y) and velocity. push it to array of enemies. update this array and draw it every loop. when an Enemy position (due to speed and time) is off screen, remove it from enemies array.
Update: I threw in a collision detection and some more natural improvements. You should be recognizing your code.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const width = canvas.width = canvas.clientWidth;
const height = canvas.height = canvas.clientHeight
class Position {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Player {
constructor() {
this.radius = 20;
this.position = new Position(width / 2, height - this.radius)
this.speed = 5;
this.color = "teal";
// Belongs to the handleKeyUp and handleKeyDown functions
this.right = false;
this.left = false;
this.up = false;
this.down = false;
}
}
// Creating a class to handle the enemy balls (stuck!)
class Enemy {
constructor(position, velocity) {
this.position = position;
this.radius = 10;
this.velocity = velocity;
this.color = "red";
}
}
function generateEnemy() {
let enemy = new Enemy(new Position(Math.random() * width, 0 - 10), 0.01 + Math.random() / 5)
enemies.push(enemy);
}
// Creating our main player
function drawCircle(entity) {
context.beginPath();
context.arc(entity.position.x, entity.position.y, entity.radius, 0, Math.PI * 2);
context.fillStyle = entity.color;
context.fill();
}
// Handles the A and D keyboard buttons
function handleKeyDown(event) {
if (event.key === "a" || event.key === 'ArrowLeft') {
player.left = true;
event.preventDefault()
}
if (event.key === "d" || event.key === 'ArrowRight') {
player.right = true;
event.preventDefault()
}
if (event.key === "w" || event.key === 'ArrowUp') {
player.up = true;
event.preventDefault()
}
if (event.key === "s" || event.key === 'ArrowDown') {
player.down = true;
event.preventDefault()
}
}
function handleKeyUp(event) {
if (event.key === "a" || event.key === 'ArrowLeft') {
player.left = false;
event.preventDefault()
}
if (event.key === "d" || event.key === 'ArrowRight') {
player.right = false;
event.preventDefault()
}
if (event.key === "w" || event.key === 'ArrowUp') {
player.up = false;
event.preventDefault()
}
if (event.key === "s" || event.key === 'ArrowDown') {
player.down = false;
event.preventDefault()
}
}
// Makes sure that the player ball does not go outside the canvas
function handlePlayerMovement(player) {
if (player.right) {
player.position.x += player.speed;
}
if (player.left) {
player.position.x -= player.speed;
}
player.position.x = Math.min(width - player.radius, player.position.x)
player.position.x = Math.max(player.radius, player.position.x)
if (player.up) {
player.position.y -= player.speed;
}
if (player.down) {
player.position.y += player.speed;
}
player.position.y = Math.min(height - player.radius, player.position.y)
player.position.y = Math.max(player.radius, player.position.y)
}
function update(dt) {
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i]
enemy.position.y += enemy.velocity * dt;
if (enemy.position.y >= height + enemy.radius) {
enemies.splice(i, 1)
}
}
handlePlayerMovement(player);
}
function draw() {
context.clearRect(0, 0, width, height);
drawCircle(player);
enemies.forEach(drawCircle)
}
function is_colliding(player, enemy) {
var dx = player.position.x - enemy.position.x
var dy = player.position.y - enemy.position.y
var safe = player.radius + enemy.radius
if (dx * dx + dy * dy < safe * safe) {
return true;
}
}
function detect_collisions() {
var boom = false
enemies.forEach(function(enemy) {
if (is_colliding(player, enemy)) {
boom = true;
}
})
if (boom) {
player.color = 'gray'
} else {
player.color = 'teal'
}
}
function timestamp() {
return new Date().getTime();
}
function tick() {
var now = timestamp();
var dt = Math.min(now - last, 250);
update(dt);
detect_collisions()
if (Math.random() < 0.05) {
generateEnemy()
}
draw();
last = now;
requestAnimationFrame(tick);
}
var last = timestamp()
var now;
let enemies = [];
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
let player = new Player();
tick();
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#canvas {
box-sizing: border-box;
}
<canvas id="canvas" style="border: 1px solid rgb(0, 0, 0); height: 100vh; width: 100%"></canvas>
EDIT: I rewrote a minimal reproducible example in p5.js, so you can see what's happening here:
let keyInput = []
let player
let block
function setup() {
createCanvas(600, 600)
player = new Player(0, height/2)
block = new Block(200, height-100, 100, 1000)
}
function draw() {
background(220);
player.draw()
block.draw()
player.moveX()
block.checkCollisionsX()
player.moveY()
block.checkCollisionsY()
}
function keyPressed() {
keyInput[keyCode] = true;
}
function keyReleased() {
keyInput[keyCode] = false;
}
function rectCollide(x1, y1, w1, h1, x2, y2, w2, h2) {
return x1 + w1 > x2 && x1 < x2 + w2 && y1 + h1 > y2 && y1 < y2 + h2;
}
//BLOCK CONSTRUCTOR
class Block {
constructor(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.movingSpeed = 3;
this.start = 200
this.stop = 400
}
draw() {
fill(255)
rect(this.x, this.y, this.w, this.h)
this.x += this.movingSpeed;
if (this.x > this.stop) {
this.movingSpeed = -this.movingSpeed;
}
if (this.x < this.start) {
this.movingSpeed = abs(this.movingSpeed);
}
}
checkCollisionsX() {
if (rectCollide(player.pos.x, player.pos.y, player.width, player.height, this.x, this.y, this.w, this.h)) {
if (player.vel.x < 0) { // If the player moved left and collided with the right side of block
player.pos.x = this.x + this.w;
} else { // If the player moved right and collided with the left side of block
player.pos.x = this.x - player.width;
}
player.vel.x = 0;
}
}
checkCollisionsY() {
if (rectCollide(player.pos.x, player.pos.y, player.width, player.height, this.x, this.y, this.w, this.h)) {
if (player.vel.y < 0) {
player.pos.y = this.y + this.h;
player.vel.y *= -yRebound; // Not -1 because collisions are not perfectly elastic
} else {
//player.pos.x += movingSpeed; //Keep player on platform while platform is moving
player.jumps = player.numJumps;
player.pos.y = this.y - player.height;
player.vel.y = 0;
player.acc.y = 0;
}
}
}
}
//PLAYER CONSTRUCTOR
class Player {
constructor(x, y) {
this.width = 50
this.height = 100
// all 3 chars for pretty code
this.pos = new p5.Vector(x, y);
this.vel = new p5.Vector(0, 0);
this.acc = new p5.Vector(0, 0);
this.accSpeed = 0.05;
this.gravity = 0.5;
this.maxVel = 10;
this.jumpForce = 15;
this.friction = 0.15;
this.numJumps = 1;
this.jumps = this.numJumps;
this.isGrounded = false;
this.canMove = true;
this.dir = "DOWN"
}
draw() {
fill(255, 0, 0)
rect(this.pos.x, this.pos.y, this.width, this.height)
}
moveX() {
//MOVE X
if (keyInput[LEFT_ARROW] && this.vel.x > -this.maxVel && this.canMove) {
this.acc.x = -this.accSpeed;
} else if (keyInput[RIGHT_ARROW] && this.vel.x < this.maxVel && this.canMove) {
this.acc.x = this.accSpeed;
} else if (abs(this.vel.x) > 0.2) {
this.acc.x = (this.vel.x < 0) ? this.friction : -this.friction;
} else {
this.vel.x = 0;
this.acc.x = 0;
}
this.vel.x += this.acc.x; // vel += acc
this.pos.x += this.vel.x; // pos += vel
}
moveY() {
//MOVE Y
if (keyInput[UP_ARROW] && this.jumps > 0 && this.canMove) {
this.jumps--;
this.vel.y = -this.jumpForce;
}
this.acc.y += this.gravity;
this.vel.y += this.acc.y;
this.pos.y += this.vel.y;
this.acc.y = 0; // Reset acceleration
if (this.pos.y >= height - this.height) {
this.pos.y = height-this.height
this.jumps = this.numJumps;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
</body>
</html>
I'm making a 2D platformer game, and all block collisions work correctly except for the left side of a horizontally moving block. Here's what the block looks like (ignore the lag):
Here's the code that makes up the moving block:
if (type.equals("moving-horizontal-2x1")) {
image(img2x1, pos.x, pos.y, 200, 100);
pos.x += movingSpeed;
if (pos.x > stop) {
movingSpeed = -movingSpeed;
}
if (pos.x < start) {
movingSpeed = abs(movingSpeed);
}
}
When the player collides with the block, it works for all sides except the left side on the X axis, as shown by the image below.
This video shows what happens when the player collides with the moving block: https://www.youtube.com/watch?v=ewVSYd5h4rg
As you can see, the player gets moved to the other side of the block. I don't know why this is happening, since the collision detection/resolution code is the same as the other static blocks. Why does only one side work?
Here is my collision detection code for the X axis:
if (rectCollide(player.pos.x, player.pos.y, player.width, player.height, pos.x, pos.y, width, height)) {
if (player.vel.x < 0) { // If the player moved left and collided with the right side of block
player.pos.x = pos.x + width;
} else { // If the player moved right and collided with the left side of block (this is the broken side)
player.pos.x = pos.x - player.width;
}
player.vel.x = 0;
}
I've tried fixing this issue by incrementing the player's x pos when it collides with the left side, by doing something like player.pos.x += movingSpeed, but this still doesn't work.
I believe the bug has something to do with how I draw the moving block, since the collision detection/resolution code works perfectly fine with all other static blocks.
Thanks for any help!
The problem occurs when you are slowly moving away from the block, but the block is faster than you. The player's direction of movement therefore says nothing about whether he is to the left or to the right of the obstacle. Don't check the player's velocity, check the player's position relative to the obstacles position:
Replace
if (player.vel.x < 0)
with
if (player.pos.x > this.x)
This is a Canvas game for personal development and I am stuck in here.
Everything works pretty well until I try to give my player a cross-movement
I have a player class like this ==>
And it has methods like dw() {means go up and right} , as() {left and down at the same time } ... etc I want to give a cross moving to my player with the key combinations .
Maybe someone shows me a correct way to do this.
import { ctx } from "./app.js"
export default class Player {
constructor(x, y, radius, color) {
this.x = x
this.y = y
this.color = color
this.radius = radius
}
draw() {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false)
ctx.fillStyle = this.color
ctx.fill()
ctx.closePath()
}
moveLeft() {
this.x -= .01
}
moveRight() {
this.x += .01
}
moveUp() {
this.y -= .01
}
moveDown() {
this.y += .01
}
aw() {
this.x -= .01
this.y -= .01
}
dw() {
this.x += .01
this.y -= .01
}
as() {
this.x -= .01
this.y += .01
}
ds() {
this.x += .01
this.y += .01
}
update() {
this.draw()
window.addEventListener('keydown', (event) => {
switch (event.key) {
case 'a':
this.moveLeft();
break;
case 'd':
this.moveRight();
break;
case 'w':
this.moveUp();
break;
case 's':
this.moveDown();
break;
case 'a' && 'w':
this.aw();
break;
case 'd' && 'w':
this.dw();
break;
case 'a' && 's':
this.as();
break;
case 'd' && 's':
this.ds();
break;
default:
break;
}
});
}
}
And the main file is this
import Bullet from "./bullet.js";
import Player from "./player.js";
import Enemy from "./enemy.js";
import ShatteredEnemy from "./shatteredEnemy.js";
// remove scroolbar
document.getElementById("board").onwheel = function (event) {
event.preventDefault();
};
document.getElementById("board").onmousewheel = function (event) {
event.preventDefault();
};
const modal = document.getElementById("modal")
const scoreElement = document.getElementById('score')
const startButton = document.getElementById('startBtn')
const h1 = document.getElementById('h1') // Modal score
const canvas = document.getElementById('board')
canvas.width = window.innerWidth
canvas.height = window.innerHeight // can be written as ctx.height = innerHeight
export const ctx = canvas.getContext('2d')
const middleX = innerWidth / 2
const middleY = innerHeight / 2
let playerOne = new Player(middleX, middleY, 15, 'white')
playerOne.draw()
let bullets = [] // projectiles
let enemies = []
let shatteredEnemyArray = []
function reset () {
playerOne = new Player(middleX, middleY, 15, 'white')
bullets = []
enemies = []
shatteredEnemyArray = []
score = 0
scoreElement.innerHTML = score
h1.innerHTML = score
}
function spawnEnemy() {
setInterval(() => {
const radius = Math.random() * (30 - 5) + 5
let x, y;
if (Math.random() < 0.5) {
x = Math.random() < 0.5 ? -radius : innerWidth + radius
y = Math.random() * innerHeight
} else {
x = Math.random() * innerWidth
y = Math.random() < 0.5 ? -radius : innerHeight + radius
}
const color = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`
const angle = Math.atan2(middleY - y, middleX - x)
const velocity = {
y: Math.sin(angle),
x: Math.cos(angle)
}
const enemy = new Enemy(x, y, radius, color, velocity)
enemies.push(enemy)
}, 1500)
}
let animationID
let score = 0
function animate() {
animationID = requestAnimationFrame(animate)
ctx.fillStyle = `rgba(0, 0, 0, 0.2) ` // it is for making shadows on the background
ctx.fillRect(0, 0, innerWidth, innerHeight)
playerOne.update()
shatteredEnemyArray.forEach((shattered, sIdx) => { // if you want to render sth on the screen you must call update() method
if ( shattered.alpha <= 0) {
shatteredEnemyArray.splice(sIdx, 1)
} else {
shattered.update()
}
})
bullets.forEach((bullet, bulletIndex) => {
bullet.update()
// remove bullet if it goes out of bounds
if (bullet.x + bullet.radius < 0 || bullet.x - bullet.radius > innerWidth
|| bullet.y + bullet.radius < 0 || bullet.y - bullet.radius > innerHeight) {
setTimeout(() => {
bullets.splice(bulletIndex, 1)
}, 0)
}
})
enemies.forEach((enemy, eIdx) => {
enemy.update()
const killerDist = Math.hypot(playerOne.x - enemy.x, playerOne.y - enemy.y)
if (killerDist < playerOne.radius + enemy.radius) {
// console.log('you lose')
cancelAnimationFrame(animationID)
modal.style.display = 'flex'
h1.innerHTML = score
startButton.innerHTML = 'Restart'
}
// remove an enemy if it touches a bullet
bullets.forEach((bullet, bIdx) => {
const dist = Math.hypot(bullet.x - enemy.x, bullet.y - enemy.y)
if (dist < enemy.radius + bullet.radius) {
score += Math.floor(enemy.radius)
scoreElement.innerHTML = score
// if the bullet hits the enemy create a shattered enemy
for (let i = 0; i < enemy.radius; i++) {
shatteredEnemyArray.push(
new ShatteredEnemy
(bullet.x,
bullet.y,
Math.random() * 2 + 1,
enemy.color, {
x: (Math.random() - .5) * 5,
y: (Math.random() - .5) * 5
}))
}
if (enemy.radius - 10 > 10) {
gsap.to(enemy, { // for shrinking the enemy
duration: 0.5,
radius: enemy.radius - 10,
ease: 'power2.inOut',
})
setTimeout(() => {
bullets.splice(bIdx, 1)
}, 0)
} else {
setTimeout(() => {
enemies.splice(eIdx, 1)
bullets.splice(bIdx, 1)
}, 0)
}
}
})
})
}
addEventListener('click', (e) => {
const angle = Math.atan2(e.clientY - playerOne.y, e.clientX - playerOne.x)
const velocity = {
x: Math.cos(angle) * 5,
y: Math.sin(angle) * 5
}
const bullet = new Bullet(playerOne.x, playerOne.y, 5,
'wheat', velocity)
bullets.push(bullet)
})
startButton.addEventListener('click', () => {
reset()
animate()
spawnEnemy()
modal.style.display = 'none'
})
I have a problem when there is a collision between 2 players (we always see the oppenent shaking).
Here is a video : https://www.youtube.com/watch?v=Xu8DTKngVjg
Here is the client code :
class Player {
static list = new Map();
constructor(properties) {
Player.list.set(properties.id, this);
this.id = properties.id;
this.x = properties.x * zoom;
this.y = properties.y * zoom;
}
update() {
this.sX = this.x - camera.x + document.documentElement.clientWidth / 2;
this.sY = this.y - camera.y + document.documentElement.clientHeight / 2;
//Draw the player
}
}
function animate() {
Player.list.forEach(player => {
player.animate();
});
window.requestAnimationFrame(animate);
}
animate();
socket.on("positions", function (data) {
if (Player.list.has(data.id)) {
if (data.id === clientID && startedGameplay === true) {
camera.x = data.x * zoom;
camera.y = data.y * zoom;
}
Player.list.get(data.id).x = data.x * zoom;
Player.list.get(data.id).y = data.y * zoom;
}
});
And here is the server code :
class Player {
static list = new Map();
constructor(properties) {
Player.list.set(properties.id, this);
this.id = properties.id;
this.x = properties.x;
this.y = properties.y;
}
update() {
//Player moves
move(this);
//Chek for collisions
Player.list.forEach(player => {
if (player.id != this.id) {
const distance = Math.hypot(this.y - player.y, this.x - player.x) || 1;
collisions(distance, this, player);
}
});
io.emit("positions", this.getPosition());
}
getPosition() {
return {
id: this.id,
x: this.x,
y: this.y,
}
}
}
//Collisions function
function collisions(distance, player, obj) {
if (distance <= player.hitboxRadius + obj.hitboxRadius) {
const dX = (player.hitboxRadius + obj.hitboxRadius) * (obj.x - player.x) / distance;
const dY = (player.hitboxRadius + obj.hitboxRadius) * (obj.y - player.y) / distance;
obj.x = player.x + dX;
obj.y = player.y + dY;
}
}
//Function to make the player move
function move(player) {
if (player.movingUp === true) {
player.vy -= player.moveSpeed;
}
else if (player.movingDown === true) {
player.vy += player.moveSpeed;
}
if (player.movingRight === true) {
player.vx += player.moveSpeed;
}
else if (player.movingLeft === true) {
player.vx -= player.moveSpeed;
}
player.x += player.vx;
player.y += player.vy;
player.vx *= 0.9;
player.vy *= 0.9;
}
//Loop every 10ms to update players
setInterval(() => {
Player.list.forEach(player => {
player.update();
});
}, 10);
I dont think the problem come from the collisions detection/response, because if I update sX and sY every time I recieve the position pack and not on the players update function, the collisions work fine without shaking. I actually need to update sX and sY on players update functions.
Can anyone help me ?
(sX and sY are the positions of the players in relation to the position of the client player who is placed in the center of the screen).
I move collision function into the player class. it works for me.
I'm trying to get the ball below to keep appearing and firing up accross the y axis at a set interval and always from where the x position of paddle(mouse) is, i need there to be a delay between each ball firing. I'm trying to make space invaders but with the ball constantly firing at a set interval.
Do I need to create multiple requestAnimationFrame loops for each ball? Can someone help with a very basic example of how this should be done please or link a good article? I am stuck at creating an array for each ball and not sure how to architect the loop to achieve this effect. All the examples I can find are too complex
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* {
padding: 0;
margin: 0;
}
canvas {
background: #eee;
display: block;
margin: 0 auto;
width: 30%;
}
</style>
</head>
<body>
<canvas id="myCanvas" height="400"></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
//start the requestAnimationFrame loop
var myRequestAnimation;
var myRequestAnimationBall;
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
drawLoop();
setInterval(drawBallLoop, 400);
var x = canvas.width / 2;
var y = canvas.height - 30;
var defaultSpeedX = 0;
var defaultSpeedY = 4;
var dx = defaultSpeedX;
var dy = -defaultSpeedY;
var ballRadius = 10;
var paddleX = (canvas.width - paddleWidth) / 2;
var paddleHeight = 10;
var paddleWidth = 70;
//control stuff
var rightPressed = false;
var leftPressed = false;
var brickRowCount = 1;
var brickColumnCount = 1;
var brickWidth = 40;
var brickHeight = 20;
var brickPadding = 10;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;
var score = 0;
var lives = 3;
//paddle
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
//bricks
function drawBricks() {
for (var c = 0; c < brickColumnCount; c++) {
for (var r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status == 1) {
var brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
var brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
//collision detection
function collisionDetection() {
for (var c = 0; c < brickColumnCount; c++) {
for (var r = 0; r < brickRowCount; r++) {
var b = bricks[c][r];
if (b.status == 1) {
if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
dy = -dy;
b.status = 0;
score++;
console.log(score);
if (score == brickRowCount * brickColumnCount) {
console.log("YOU WIN, CONGRATS!");
window.cancelAnimationFrame(myRequestAnimation);
}
}
}
}
}
}
//default bricks
var bricks = [];
for (var c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (var r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
//lives
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
}
// ball1
var ball1 = {
x,
y,
directionX: 0,
directionY: -5
}
// ball1
var ball2 = {
x,
y,
directionX: 0,
directionY: -2
}
// put each ball in a balls[] array
var balls = [ball1, ball2];
function drawBall() {
// clearCanvas();
for (var i = 0; i < balls.length; i++) {
var ball = balls[i]
ctx.beginPath();
ctx.arc(ball.x, ball.y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
///////DRAW BALL LOOP////////
function drawBallLoop() {
myRequestAnimationBall = requestAnimationFrame(drawBallLoop);
// clear frame
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawBall();
//move balls
for (var i = 0; i < balls.length; i++) {
balls[i].y += balls[i].directionY;
}
}
//Clear Canvas
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
///////DRAW MAIN LOOP////////
function drawLoop() {
myRequestAnimation = requestAnimationFrame(drawLoop);
// clear frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawPaddle();
drawBricks();
collisionDetection();
drawLives();
//bounce off walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (rightPressed) {
paddleX += 7;
if (paddleX + paddleWidth > canvas.width) {
paddleX = canvas.width - paddleWidth;
}
}
else if (leftPressed) {
paddleX -= 7;
if (paddleX < 0) {
paddleX = 0;
}
}
}
//keyboard left/right logic
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
function keyDownHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = true;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = false;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = false;
}
}
//relative mouse pos
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect(), // abs. size of element
scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X
scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y
return {
x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have
y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element
}
}
//mouse movemment
document.addEventListener("mousemove", mouseMoveHandler, false);
function mouseMoveHandler(e) {
var mouseX = getMousePos(canvas, e).x;
//e.clientX = the horizontal mouse position in the viewport
//canvas.offsetLeft = the distance between the left edge of the canvas and left edge of the viewport
var relativeX = mouseX;
// console.log('mouse= ',relativeX, canvas.offsetLeft)
// console.log('paddle= ', paddleX);
// console.log(getMousePos(canvas, e).x);
if (relativeX - (paddleWidth / 2) > 0 && relativeX < canvas.width - (paddleWidth / 2)) {
paddleX = relativeX - (paddleWidth / 2);
}
}
</script>
</body>
</html>
Basic principles
Here's one way you could do it:
you need a Game object that will handle the update logic, store all current entities, deal with the game loop... IMO, this is where you should keep track of when was the last Ball fired and whether to fire a new one.
In this demo, this object also handles current time, delta time, and requesting animation frames, but some might argue that this logic could be externalised, and just call some sort of Game.update(deltaTime) on each frame.
you need different objects for all entities in your game. I created an Entity class because I want to make sure all game entities have the minimum required to function (ie. update, draw, x, y...).
There is a Ball class that extends Entity and is responsible for knowing it's own parameters (speed, size, ...), how to update and draw itself, ...
There is a Paddle class that I left bare for you to complete.
The bottom line is it's all a matter of separation of concerns. Who should know what about whom? And then pass variables around.
As for your other question:
Do I need to create multiple requestAnimationFrame loops for each ball?
It's definitely possible, but I'd argue that having a centralized place that handles lastUpdate, deltaTime, lastBallCreated makes things much simpler. And in practice, devs tends to try and have a single animation frame loop for this.
class Entity {
constructor(x, y) {
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.speed = 100 // px per second
this.size = 10 // radius in px
}
update(deltaTime) {
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.size, 0, 2 * Math.PI)
context.fill()
}
isDead() {
return this.y < 0 - this.size
}
}
class Paddle extends Entity {
constructor() {
super(0, 0)
}
update() { /**/ }
draw() { /**/ }
isDead() { return false }
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 1000 // ms between each ball
this.lastBallCreated = 0 // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
const paddle = new Paddle()
this.entities.push(paddle)
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
this.entities.forEach(entity => entity.update(deltaTime))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
const ball = new Ball(100, 300) // this is quick and dirty, you should put some more thought into `x` and `y` here
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
Managing player inputs
Now let's say you want to add keyboard inputs to your game. In that case, I'd actually create a separate class, because depending on how many "buttons" you want to support, it can get very complicated very quick.
So first, let's draw a basic paddle so we can see what's happening:
class Paddle extends Entity {
constructor() {
// we just add a default initial x,y and height,width
super(150, 20)
this.width = 50
this.height = 10
}
update() { /**/ }
/** #param {CanvasRenderingContext2D} context */
draw(context) {
// we just draw a simple rectangle centered on x,y
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
And now we add a basic InputsManager class that you can make as complicated as you want. Just for two keys, handling keydown and keyup and the fact that two keys can be pressed at once it already a few lines of code so it's good to keep things separate so as to not clutter our Game object.
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
Now, we can update our Game class to make use of this new InputsManager
class Game {
// ...
start() {
// ...
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
} // we now pass more data to the update method so that entities that need to can also read from our InputsManager
this.entities.forEach(entity => entity.update(frameData))
}
// ...
}
And after updating the code for the update methods of our entities to actually use the new InputsManager, here's the result:
class Entity {
constructor(x, y) {
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({deltaTime}) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fill()
}
isDead() {
return this.y < 0 - this.radius
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.speed = 200
this.width = 50
this.height = 10
}
update({deltaTime, inputs}) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
// we can now read from this.player to the the position of where to fire a Ball
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Once you click "Run code snippet", you have to click on the iframe to focus it so it can listen for keyboard inputs (arrow left, arrow right).
As a bonus, since we are now able to draw and move the paddle, I added the ability to create a Ball at the same x coordinate as the paddle. You can read the comments I left in the code snippet above for a quick explanation as to how this works.
How to add functionality
Now I want to give you a more general outlook at how to approach future problems you might have when expending on this example. I'll take the example of wanting to test for collision between two game objects. You should ask yourself where to place the logic?
where is a place that all game objects can share logic? (creating the information)
where will you need to know about collisions? (accessing the information)
In this example, all game objects are sub classes of Entity so to me this makes sense to put the code there:
class Entity {
constructor(x, y) {
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if(a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if(b.collision === 'none') {
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if(a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius
}
if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
}
Now there are many kinds of 2D collisions so the code is a bit verbose, but the main point is: this is a design decision that I'm making here. I can be generalist and future proof this but then it looks like the above... And I have to add a .collision property to all of my game objects so that they know whether they should be treated as a 'circle' or a 'rect' in the above algorithm.
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
}
// ...
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
}
// ...
}
Or I can be minimalist and just add what I need, in which case it might make more sense to actually put the code in the Paddle entity:
class Paddle extends Entity {
testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x + ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x - ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
Either way, I now have access to the collision information from the cleanup function the Game loop (where I chose to place the logic of removing dead entities).
With my first generalist solution, I would use it like this:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
With the second minimalist approach, I'd still have to pass the player around for the test:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = player.testBallCollision(this)
return outOfBounds || collidesWithPlayer
}
}
Final result
I hope you've learned something. In the mean time, here's the final result of this very long answer post:
class Entity {
constructor(x, y) {
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if(a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if(b.collision === 'none') {
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if(a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius
}
if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({deltaTime}) {
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fill()
}
isDead(player) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
this.speed = 200
this.width = 50
this.height = 10
}
update({deltaTime, inputs}) {
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
this.player = new Paddle()
this.entities.push(this.player)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Once you click "Run code snippet", you have to click on the iframe to focus it so it can listen for keyboard inputs (arrow left, arrow right).