Reputation: 1167
I'm caught in a logic knot with this game :(. I simply want to remove the explosions from the screen after say 1 second of each doing its loop. As you can see below they run at the frame rate of the game loop. This is the only way I could animate the explosion - by setting a sprite to move at the speed of the game loop (frame rate).
I don't understand how to connect a separate animation going at a different speed to this same canvas context that is being essentially cleared every frame.. I can't even figure out how to stop the explosion sprite loop.
I've tried creating a separate method drawExplosion()
in the Explosion class and using setInterval
in the Explosion constructor but it never likes the context I connect it to and throws this error:
Cannot read property 'drawImage' of undefined (i.e. the context is undefined)
If someone could just stop each explosion loop after 1 second I would understand where I went off course
The outline of the code is this:
class Entity
class Ball extends Entity
class Explosion extends Entity
class Enemy extends Entity
class Paddle extends Entity
class InputsManager
class mouseMoveHandler
class Game
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<!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;
margin: 0;
padding: 0;
}
canvas {
background: url("https://picsum.photos/200");
width: 100%;
background-size: cover;
}
</style>
</head>
<body>
<canvas height="459"></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 = 2.5 // radius in px
this.color = '#fff'
this.ballsDistanceY = 12
}
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 = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y + this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y - this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
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;
game.hitEnemy();
return true
}
}
}
class Explosion extends Entity {
constructor(x, y, contextFromGameObject){
super(x, y)
this.contextFromGameObject = contextFromGameObject
this.imgExplosion = new Image();
this.imgExplosion.src = "https://i.ibb.co/9Ggfzxr/explosion.png";
this.totalNumberOfFrames = 12 // ten images in the image (see the url above)
this.spriteFrameNumber = 0 // This is changed to make the sprite animate
this.widthOfSprite = 1200 // this.imgExplosion.width; // find the width of the image
this.heightOfSprite = 100 // this.imgExplosion.height; // find the height of the image
this.widthOfSingleImage = this.widthOfSprite / this.totalNumberOfFrames; // The width of each image in the spirite
//this.timerId = setInterval(this.explode.bind(this), 100)
this.scaleExplosion = 0.5
//this.timerId = setInterval(this.drawExplosion, 100);
}
// drawExplosion(){
// console.log(this.spriteFrameNumber)
// //ctx.clearRect(0, 0, 500, 500)
// this.spriteFrameNumber += 1; // changes the sprite we look at
// this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
// this.contextFromGameObject.drawImage(this.imgExplosion,
// this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
// this.widthOfSingleImage, this.heightOfSprite, // width and height
// this.x - 25, this.y - 25, // x and y - where on the screen
// this.widthOfSingleImage, this.heightOfSprite // width and height
// );
// if (this.spriteFrameNumber > 9) {
// clearInterval(this.timerId)
// };
// }
/** @param {CanvasRenderingContext2D} context */
draw(context, frameNumber) {
console.log(frameNumber)
//ctx.clearRect(0, 0, 500, 500)
this.spriteFrameNumber += 1; // changes the sprite we look at
this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
context.drawImage(this.imgExplosion,
this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
this.widthOfSingleImage, this.heightOfSprite, // width and height
this.x - 25, this.y - 25, // x and y - where on the screen
this.widthOfSingleImage, this.heightOfSprite // width and height
);
}
update() {
}
isDead(ball, isDead) {
if(isDead == 'true'){
clearTimeout(this.timerId);
return true
}
return false
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speedVar = 4;
this.speed = this.speedVar;
this.color = '#EC3AC8';
this.color2 = '#000000';
this.y = y;
this.imgEnemy = new Image();
this.imgEnemy.src = "https://i.ibb.co/kgXsr66/question.png";
this.runCount = 1;
this.timerId = setInterval(this.movePosish.bind(this), 1000);
}
movePosish() {
//console.log(this.runCount)
// x 10 -> 240
// y 10 -> 300
switch (this.runCount) {
case 0:
this.x = 20; this.y = 200;
break
case 1:
this.x = 200; this.y = 300;
break
case 2:
this.x = 30; this.y = 20;
break
case 3:
this.x = 230; this.y = 150;
break
case 4:
this.x = 200; this.y = 20;
break
case 5:
this.x = 30; this.y = 90;
break
case 6:
this.x = 240; this.y = 20;
break
case 7:
this.x = 30; this.y = 150;
break
case 8:
this.x = 180; this.y = 170;
break
case 9:
this.x = 30; this.y = 50;
break
case 10:
this.x = 130; this.y = 170;
break
}
//if 10th image remove image and clear timer
this.runCount += 1;
if (this.runCount > 10) {
//clearInterval(this.timerId)
this.runCount = 0;
console.log('ya missed 10 of em')
};
}
update() {
// //Moving left/right
// this.x += this.speed;
// if (this.x > canvas.width - this.width) {
// this.speed -= this.speedVar;
// }
// if (this.x === 0) {
// this.speed += this.speedVar;
// }
}
/** @param {CanvasRenderingContext2D} context */
draw(context) {
// context.beginPath();
// context.rect(this.x, this.y, this.width, this.height);
// context.fillStyle = this.color2;
// context.fill();
// context.closePath();
context.drawImage(this.imgEnemy, this.x, this.y);
}
isDead(enemy) {
//// 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
// }
return false
}
}
class Paddle extends Entity {
constructor(x, width) {
super(x, width)
this.collision = 'rect'
this.speed = 200
this.height = 25
this.width = 30
this.color = "#74FCEF"
}
update({ deltaTime, inputs, mouse }) {
// Paddle needs to read both deltaTime and inputs
// if mouse inside canvas AND not on mobile
if (mouse.insideCanvas) {
this.x = mouse.paddleX
} else {
this.x += this.speed * deltaTime / 1000 * inputs.direction
// stop from going off screen
if (this.x < this.width / 2) {
this.x = this.width / 2;
} else if (this.x > canvas.width - this.width / 2) {
this.x = canvas.width - this.width / 2
}
}
}
/** @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 = this.color;
context.fill();
context.closePath();
context.beginPath();
context.rect(this.x - this.width / 12, this.y - this.height / 1.1, this.width / 6, this.height);
context.fillStyle = this.color;
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 mouseMoveHandler {
constructor() {
// this.paddleWidth = paddleWidth;
this.x = 0;
this.paddleX = 0;
//this.canvas = canvas;
document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
}
//'relative to canvas width' mouse position snippet
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
//console.log('canvas width = ' + canvas.width)
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
}
}
onMouseMove(e) {
//console.log('moving')
//this.x = 100;
this.x = this.getMousePos(canvas, e).x; //relative x on canvas
this.y = this.getMousePos(canvas, e).y; //relative x on canvas
this.insideCanvas = false;
if (this.x > 0 && this.x < canvas.width) {
if (this.y > 0 && this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 && this.x < canvas.width - 20) {
this.paddleX = this.x;
}
}
}
class Game {
/** @param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 900 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
this.paddleWidth = 50
this.isMobile = false
this.frameNumber = 0;
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('endgame')
const endGame = true;
game.loop(endGame)
}
hitEnemy() {
//this.timerId = setTimeout(endExplosion(), 500);
this.explosion = new Explosion(this.enemy.x, this.enemy.y, this.context)
this.entities.push(this.explosion)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(140, 220)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle(150, 400)
// 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 watching mousemovement
this.mouseMoveHandler = new mouseMoveHandler()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
this.isMobile = window.matchMedia('(max-width: 1199px)');
// 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,
mouse: this.mouseMoveHandler,
context: this.context
}
// 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, 360)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//console.log(this.entities[0])//Enemy
//console.log(this.entities[1])//Paddle
//console.log(this.entities[2])//Ball
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
draw() {
//draw entities
this.entities.forEach(entity => entity.draw(this.context, this.frameNumber))
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.frameNumber += 1;
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if (endGame) {
cancelAnimationFrame(this.myLoop);
this.endGame()
return;
}
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>
Upvotes: 1
Views: 143
Reputation: 694
What you might want to do is to use a boolean that stays true as long as your animation should be running, it will call the function that draws your explosion.
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 = 2.5 // radius in px
this.color = '#fff'
this.ballsDistanceY = 12
}
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 = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y + this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y - this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
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;
game.hitEnemy();
return true
}
}
}
class Explosion extends Entity {
constructor(x, y, contextFromGameObject){
super(x, y)
this.contextFromGameObject = contextFromGameObject
this.imgExplosion = new Image();
this.imgExplosion.src = "https://i.ibb.co/9Ggfzxr/explosion.png";
this.totalNumberOfFrames = 12 // ten images in the image (see the url above)
this.spriteFrameNumber = 0 // This is changed to make the sprite animate
this.widthOfSprite = 1200 // this.imgExplosion.width; // find the width of the image
this.heightOfSprite = 100 // this.imgExplosion.height; // find the height of the image
this.widthOfSingleImage = this.widthOfSprite / this.totalNumberOfFrames; // The width of each image in the spirite
//this.timerId = setInterval(this.explode.bind(this), 100)
this.scaleExplosion = 0.5
this.explosionHappened = 0;
//this.timerId = setInterval(this.drawExplosion, 100);
}
// drawExplosion(){
// console.log(this.spriteFrameNumber)
// //ctx.clearRect(0, 0, 500, 500)
// this.spriteFrameNumber += 1; // changes the sprite we look at
// this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
// this.contextFromGameObject.drawImage(this.imgExplosion,
// this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
// this.widthOfSingleImage, this.heightOfSprite, // width and height
// this.x - 25, this.y - 25, // x and y - where on the screen
// this.widthOfSingleImage, this.heightOfSprite // width and height
// );
// if (this.spriteFrameNumber > 9) {
// clearInterval(this.timerId)
// };
// }
/** @param {CanvasRenderingContext2D} context */
draw(context, frameNumber) {
//console.log(frameNumber)
if(this.explosionHappened)
{
//ctx.clearRect(0, 0, 500, 500)
this.spriteFrameNumber += 1; // changes the sprite we look at
this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
context.drawImage(this.imgExplosion,
this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
this.widthOfSingleImage, this.heightOfSprite, // width and height
this.x - 25, this.y - 25, // x and y - where on the screen
this.widthOfSingleImage, this.heightOfSprite // width and height
);
this.explosionHappened=this.spriteFrameNumber;
}
}
update() {
}
isDead(ball, isDead) {
if(isDead == 'true'){
clearTimeout(this.timerId);
return true
}
return false
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speedVar = 4;
this.speed = this.speedVar;
this.color = '#EC3AC8';
this.color2 = '#000000';
this.y = y;
this.imgEnemy = new Image();
this.imgEnemy.src = "https://i.ibb.co/kgXsr66/question.png";
this.runCount = 1;
this.timerId = setInterval(this.movePosish.bind(this), 1000);
}
movePosish() {
//console.log(this.runCount)
// x 10 -> 240
// y 10 -> 300
switch (this.runCount) {
case 0:
this.x = 20; this.y = 200;
break
case 1:
this.x = 200; this.y = 300;
break
case 2:
this.x = 30; this.y = 20;
break
case 3:
this.x = 230; this.y = 150;
break
case 4:
this.x = 200; this.y = 20;
break
case 5:
this.x = 30; this.y = 90;
break
case 6:
this.x = 240; this.y = 20;
break
case 7:
this.x = 30; this.y = 150;
break
case 8:
this.x = 180; this.y = 170;
break
case 9:
this.x = 30; this.y = 50;
break
case 10:
this.x = 130; this.y = 170;
break
}
//if 10th image remove image and clear timer
this.runCount += 1;
if (this.runCount > 10) {
//clearInterval(this.timerId)
this.runCount = 0;
console.log('ya missed 10 of em')
};
}
update() {
// //Moving left/right
// this.x += this.speed;
// if (this.x > canvas.width - this.width) {
// this.speed -= this.speedVar;
// }
// if (this.x === 0) {
// this.speed += this.speedVar;
// }
}
/** @param {CanvasRenderingContext2D} context */
draw(context) {
// context.beginPath();
// context.rect(this.x, this.y, this.width, this.height);
// context.fillStyle = this.color2;
// context.fill();
// context.closePath();
context.drawImage(this.imgEnemy, this.x, this.y);
}
isDead(enemy) {
//// 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
// }
return false
}
}
class Paddle extends Entity {
constructor(x, width) {
super(x, width)
this.collision = 'rect'
this.speed = 200
this.height = 25
this.width = 30
this.color = "#74FCEF"
}
update({ deltaTime, inputs, mouse }) {
// Paddle needs to read both deltaTime and inputs
// if mouse inside canvas AND not on mobile
if (mouse.insideCanvas) {
this.x = mouse.paddleX
} else {
this.x += this.speed * deltaTime / 1000 * inputs.direction
// stop from going off screen
if (this.x < this.width / 2) {
this.x = this.width / 2;
} else if (this.x > canvas.width - this.width / 2) {
this.x = canvas.width - this.width / 2
}
}
}
/** @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 = this.color;
context.fill();
context.closePath();
context.beginPath();
context.rect(this.x - this.width / 12, this.y - this.height / 1.1, this.width / 6, this.height);
context.fillStyle = this.color;
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 mouseMoveHandler {
constructor() {
// this.paddleWidth = paddleWidth;
this.x = 0;
this.paddleX = 0;
//this.canvas = canvas;
document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
}
//'relative to canvas width' mouse position snippet
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
//console.log('canvas width = ' + canvas.width)
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
}
}
onMouseMove(e) {
//console.log('moving')
//this.x = 100;
this.x = this.getMousePos(canvas, e).x; //relative x on canvas
this.y = this.getMousePos(canvas, e).y; //relative x on canvas
this.insideCanvas = false;
if (this.x > 0 && this.x < canvas.width) {
if (this.y > 0 && this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 && this.x < canvas.width - 20) {
this.paddleX = this.x;
}
}
}
class Game {
/** @param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 900 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
this.paddleWidth = 50
this.isMobile = false
this.frameNumber = 0;
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('endgame')
const endGame = true;
game.loop(endGame)
}
hitEnemy() {
//this.timerId = setTimeout(endExplosion(), 500);
this.explosion = new Explosion(this.enemy.x, this.enemy.y, this.context)
this.explosion.explosionHappened=1;
this.entities.push(this.explosion)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(140, 220)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle(150, 400)
// 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 watching mousemovement
this.mouseMoveHandler = new mouseMoveHandler()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
this.isMobile = window.matchMedia('(max-width: 1199px)');
// 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,
mouse: this.mouseMoveHandler,
context: this.context
}
// 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, 360)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//console.log(this.entities[0])//Enemy
//console.log(this.entities[1])//Paddle
//console.log(this.entities[2])//Ball
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
draw() {
//draw entities
this.entities.forEach(entity => entity.draw(this.context, this.frameNumber))
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.frameNumber += 1;
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if (endGame) {
cancelAnimationFrame(this.myLoop);
this.endGame()
return;
}
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
}
canvas {
background: url("https://picsum.photos/200");
width: 100%;
background-size: cover;
}
<canvas height="459"></canvas>
just for an additionnal information, i myself am a fan of ecs architecture and in order to achieve an explosion, or any other game mechanism cleanly you can use systems, there is an(experimental) firefox project that allows you to use entity component system architecture called ecsy
Upvotes: 1