Запуск отдельной анимации холста для анимации игрового цикла

#javascript #canvas #game-development

#javascript #холст #разработка игр

Вопрос:

Я попал в логический узел с этой игрой:(. Я просто хочу удалить взрывы с экрана, скажем, через 1 секунду после выполнения каждого цикла. Как вы можете видеть ниже, они выполняются с частотой кадров игрового цикла. Это единственный способ, которым я мог анимировать взрыв — установив спрайт для перемещения со скоростью игрового цикла (частота кадров).

Я не понимаю, как подключить отдельную анимацию, работающую с другой скоростью, к тому же контексту холста, который по существу очищается каждый кадр.. Я даже не могу понять, как остановить цикл спрайта взрыва.

Я пытался создать отдельный метод drawExplosion() в классе Explosion и использовать setInterval его в конструкторе Explosion, но ему никогда не нравится контекст, к которому я его подключаю, и выдает эту ошибку:

  Cannot read property 'drawImage' of undefined (i.e. the context is undefined)
  

Если бы кто-нибудь мог просто остановить каждый цикл взрыва через 1 секунду, я бы понял, где я сбился с курса

Схема кода такова:

 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' amp;amp; b.collision === 'circle') {
        return Math.sqrt((a.x - b.x) ** 2   (a.y - b.y) ** 2) < a.radius   b.radius
      }
      if (a.collision === 'circle' amp;amp; b.collision === 'rect' || a.collision === 'rect' amp;amp; 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 amp;amp; bottomOfBallIsBelowTopOfRect amp;amp; ballIsRightOfRectLeftSide amp;amp; 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 amp;amp; bottomOfBallIsBelowTopOfRect amp;amp; ballIsRightOfRectLeftSide amp;amp; 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 amp;amp; this.x < canvas.width) {
        if (this.y > 0 amp;amp; this.y < canvas.height) {
          //console.log('inside')
          this.insideCanvas = true;
        } else {
          this.insideCanvas = false;
        }
      }

      if (this.x - 20 > 0 amp;amp; 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>  

Комментарии:

1. Вы пробовали использовать второй HTML-тег canvas?

2. будет ли это просто рисовать на том же холсте?

Ответ №1:

Что вы можете захотеть сделать, так это использовать логическое значение, которое остается true до тех пор, пока должна выполняться ваша анимация, оно вызовет функцию, которая рисует ваш взрыв.

 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' amp;amp; b.collision === 'circle') {
        return Math.sqrt((a.x - b.x) ** 2   (a.y - b.y) ** 2) < a.radius   b.radius
      }
      if (a.collision === 'circle' amp;amp; b.collision === 'rect' || a.collision === 'rect' amp;amp; 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 amp;amp; bottomOfBallIsBelowTopOfRect amp;amp; ballIsRightOfRectLeftSide amp;amp; 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 amp;amp; bottomOfBallIsBelowTopOfRect amp;amp; ballIsRightOfRectLeftSide amp;amp; 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 amp;amp; this.x < canvas.width) {
        if (this.y > 0 amp;amp; this.y < canvas.height) {
          //console.log('inside')
          this.insideCanvas = true;
        } else {
          this.insideCanvas = false;
        }
      }

      if (this.x - 20 > 0 amp;amp; 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>  

просто для дополнительной информации, я сам являюсь поклонником архитектуры ecs, и для достижения взрыва или любого другого игрового механизма, который вы можете использовать чисто, существует (экспериментальный) проект firefox, который позволяет вам использовать системную архитектуру entity component под названием ecsy

https://ecsy.io