AGrush
AGrush

Reputation: 1167

Running a separate canvas animation to the game loop animation

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

Answers (1)

codeanjero
codeanjero

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

https://ecsy.io

Upvotes: 1

Related Questions