Сбой игрового цикла, когда скорость мяча выше

#javascript

#javascript

Вопрос:

Я создаю игру Space invaders, и приведенный ниже код работает, мяч запускается, и если он попадает во врага, запускается функция isDead (), которая также переключает dead на true для этого мяча, и это также передается классу enemy, поэтому враг уничтожается. однако, когда я увеличиваю частоту мяча, функция enemy isDead не запускается, я действительно не уверен, почему, когда интервал между мячами выше, вся эта система ломается.

когда this.newBallInterval = 700 вражеский квадрат умирает

когда this.newBallInterval = 600 это не так

почему? и как это исправить?

 <!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      background-color: rgb(214, 238, 149);
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    canvas {
      background-color: aquamarine;
    }
  </style>
</head>

<body>

  <canvas height="300" width="300"></canvas>
</body>

<script>
  class Entity {
    constructor(x, y) {
      this.dead = false;
      this.collision = 'none'
      this.x = x
      this.y = y
    }

    update() { console.warn(`${this.constructor.name} needs an update() function`) }
    draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
    isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }

    static testCollision(a, b) {
      if (a.collision === 'none') {
        console.warn(`${a.constructor.name} needs a collision type`)
        return undefined
      }
      if (b.collision === 'none') { d
        console.warn(`${b.constructor.name} needs a collision type`)
        return undefined
      }
      if (a.collision === 'circle' 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 = 10 // radius in px
    }

    update({ deltaTime }) {
      // Ball still only needs deltaTime to calculate its update
      this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath()
      context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
      context.fillStyle = "#1ee511";
      context.fill()
    }

    isDead(enemy) {
      const outOfBounds = this.y < 0 - this.radius
      const collidesWithEnemy = Entity.testCollision(enemy, this)
      
      if (outOfBounds) {
        return true
      } 
      if (collidesWithEnemy){
        //console.log('dead')
        this.dead = true;
        return true
      }
    }
  }




  class Enemy extends Entity {
    constructor(x, y) {
      super(x, y)
      this.collision = 'rect'
      this.height = 50;
      this.width = 50;
      this.speed =  0;
      this.y = y;
    }

    update() {
      this.x  = this.speed;

      if (this.x > canvas.width - this.width) {
        this.speed -= 5;
      }

      if (this.x === 0) {
        this.speed  = 5;
      }
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath();
      context.rect(this.x, this.y, this.width, this.height);
      context.fillStyle = "#9995DD";
      context.fill();
      context.closePath();
    }
    isDead(enemy, ball) { 
      //// collision detection 
      // const collidesWithEnemy = Entity.testCollision(enemy, ball)
      // if (collidesWithEnemy){
      //   console.log('enemy dead')
      //   game.hitEnemy();
      //   return true
      // }

      if (ball.dead){
        console.log('enemy dead')
        game.hitEnemy();
        return true
      }
    }
  }




  class Paddle extends Entity {
    constructor(x, width) {
      super(150, 300)
      this.collision = 'rect'
      this.speed = 200
      this.height = 10
      this.width = 50
    }

    update({ deltaTime, inputs }) {
      // Paddle needs to read both deltaTime and inputs
      this.x  = this.speed * deltaTime / 1000 * inputs.direction
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath();
      context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
      context.fillStyle = "#0095DD";
      context.fill();
      context.closePath();
    }
    isDead() { return false }
  }





  class InputsManager {
    constructor() {
      this.direction = 0 // this is the value we actually need in out Game object
      window.addEventListener('keydown', this.onKeydown.bind(this))
      window.addEventListener('keyup', this.onKeyup.bind(this))
    }

    onKeydown(event) {
      switch (event.key) {
        case 'ArrowLeft':
          this.direction = -1
          break
        case 'ArrowRight':
          this.direction = 1
          break
      }
    }

    onKeyup(event) {
      switch (event.key) {
        case 'ArrowLeft':
          if (this.direction === -1) // make sure the direction was set by this key before resetting it
            this.direction = 0
          break
        case 'ArrowRight':
          this.direction = 1
          if (this.direction === 1) // make sure the direction was set by this key before resetting it
            this.direction = 0
          break
      }
    }
  }



  class Game {
    /** @param {HTMLCanvasElement} canvas */
    constructor(canvas) {
      this.entities = [] // contains all game entities (Balls, Paddles, ...)
      this.context = canvas.getContext('2d')
      this.newBallInterval = 700 // ms between each ball
      this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
    }

    endGame() {
      //clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
      console.log('end game')
    }

    hitEnemy() {
      const endGame = 1;
      game.loop(endGame)
    }

    start() {
      this.lastUpdate = performance.now()

      this.enemy = new Enemy(100, 20)
      this.entities.push(this.enemy)

      // we store the new Paddle in this.player so we can read from it later
      this.player = new Paddle()
      // but we still add it to the entities list so it gets updated like every other Entity
      this.entities.push(this.player)

      //start watching inputs
      this.inputsManager = new InputsManager()

      //start game loop
      this.loop()
    }

    update() {
      // calculate time elapsed
      const newTime = performance.now()
      const deltaTime = newTime - this.lastUpdate
     
      // we now pass more data to the update method so that entities that need to can also read from our InputsManager
      const frameData = {
        deltaTime,
        inputs: this.inputsManager,
      }

      // update every entity
      this.entities.forEach(entity => entity.update(frameData))

      // other update logic (here, create new entities)
      if (this.lastBallCreated   this.newBallInterval < newTime) {
        // this is quick and dirty, you should put some more thought into `x` and `y` here
        this.ball = new Ball(this.player.x, 280)
        this.entities.push(this.ball)
        this.lastBallCreated = newTime
      }0
      
      //draw entities
      this.entities.forEach(entity => entity.draw(this.context))

      // remember current time for next update
      this.lastUpdate = newTime
    }

    cleanup() {
      //to prevent memory leak, don't forget to cleanup dead entities
      this.entities.forEach(entity => {
        if (entity.isDead(this.enemy, this.ball)) {
          const index = this.entities.indexOf(entity)
          this.entities.splice(index, 1)
        }
      })
    }

    //main game loop
    loop(endGame) {
      this.myLoop = requestAnimationFrame(() => {
        this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
        if(endGame){
          cancelAnimationFrame(this.myLoop);
          this.endGame();
          return;
        }
        this.update()
        this.cleanup()
        this.loop()
      })
    }
  }

  const canvas = document.querySelector('canvas')
  const game = new Game(canvas)
  game.start()

</script>

</html>  

Ответ №1:

Проблема в том, что вы перезаписываете this.ball , поэтому ваш враг проверяет только недавно созданный, которого еще нет dead .

Вы можете просто сохранить все шары в их собственном массиве и проверить их все в своем enemy.isDead() методе:

 <!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      background-color: rgb(214, 238, 149);
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    canvas {
      background-color: aquamarine;
    }
  </style>
</head>

<body>

  <canvas height="300" width="300"></canvas>
</body>

<script>
  class Entity {
    constructor(x, y) {
      this.dead = false;
      this.collision = 'none'
      this.x = x
      this.y = y
    }

    update() { console.warn(`${this.constructor.name} needs an update() function`) }
    draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
    isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }

    static testCollision(a, b) {
      if (a.collision === 'none') {
        console.warn(`${a.constructor.name} needs a collision type`)
        return undefined
      }
      if (b.collision === 'none') { d
        console.warn(`${b.constructor.name} needs a collision type`)
        return undefined
      }
      if (a.collision === 'circle' 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 = 10 // radius in px
    }

    update({ deltaTime }) {
      // Ball still only needs deltaTime to calculate its update
      this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath()
      context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
      context.fillStyle = "#1ee511";
      context.fill()
    }

    isDead(enemy) {
      const outOfBounds = this.y < 0 - this.radius
      const collidesWithEnemy = Entity.testCollision(enemy, this)
      
      if (outOfBounds) {
        return true
      } 
      if (collidesWithEnemy){
        //console.log('dead')
        this.dead = true;
        return true
      }
    }
  }




  class Enemy extends Entity {
    constructor(x, y) {
      super(x, y)
      this.collision = 'rect'
      this.height = 50;
      this.width = 50;
      this.speed =  0;
      this.y = y;
    }

    update() {
      this.x  = this.speed;

      if (this.x > canvas.width - this.width) {
        this.speed -= 5;
      }

      if (this.x === 0) {
        this.speed  = 5;
      }
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath();
      context.rect(this.x, this.y, this.width, this.height);
      context.fillStyle = "#9995DD";
      context.fill();
      context.closePath();
    }
    isDead(enemy, balls) { 
      //// collision detection 
      // const collidesWithEnemy = Entity.testCollision(enemy, ball)
      // if (collidesWithEnemy){
      //   console.log('enemy dead')
      //   game.hitEnemy();
      //   return true
      // }

      if (balls.some(ball => ball.dead)){
        console.log('enemy dead')
        game.hitEnemy();
        return true
      }
    }
  }




  class Paddle extends Entity {
    constructor(x, width) {
      super(150, 300)
      this.collision = 'rect'
      this.speed = 200
      this.height = 10
      this.width = 50
    }

    update({ deltaTime, inputs }) {
      // Paddle needs to read both deltaTime and inputs
      this.x  = this.speed * deltaTime / 1000 * inputs.direction
    }

    /** @param {CanvasRenderingContext2D} context */
    draw(context) {
      context.beginPath();
      context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
      context.fillStyle = "#0095DD";
      context.fill();
      context.closePath();
    }
    isDead() { return false }
  }





  class InputsManager {
    constructor() {
      this.direction = 0 // this is the value we actually need in out Game object
      window.addEventListener('keydown', this.onKeydown.bind(this))
      window.addEventListener('keyup', this.onKeyup.bind(this))
    }

    onKeydown(event) {
      switch (event.key) {
        case 'ArrowLeft':
          this.direction = -1
          break
        case 'ArrowRight':
          this.direction = 1
          break
      }
    }

    onKeyup(event) {
      switch (event.key) {
        case 'ArrowLeft':
          if (this.direction === -1) // make sure the direction was set by this key before resetting it
            this.direction = 0
          break
        case 'ArrowRight':
          this.direction = 1
          if (this.direction === 1) // make sure the direction was set by this key before resetting it
            this.direction = 0
          break
      }
    }
  }



  class Game {
    /** @param {HTMLCanvasElement} canvas */
    constructor(canvas) {
      this.balls = [];
      this.entities = [] // contains all game entities (Balls, Paddles, ...)
      this.context = canvas.getContext('2d')
      this.newBallInterval = 300 // ms between each ball
      this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
    }

    endGame() {
      //clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
      console.log('end game')
    }

    hitEnemy() {
      const endGame = 1;
      game.loop(endGame)
    }

    start() {
      this.lastUpdate = performance.now()

      this.enemy = new Enemy(100, 20)
      this.entities.push(this.enemy)

      // we store the new Paddle in this.player so we can read from it later
      this.player = new Paddle()
      // but we still add it to the entities list so it gets updated like every other Entity
      this.entities.push(this.player)

      //start watching inputs
      this.inputsManager = new InputsManager()

      //start game loop
      this.loop()
    }

    update() {
      // calculate time elapsed
      const newTime = performance.now()
      const deltaTime = newTime - this.lastUpdate
     
      // we now pass more data to the update method so that entities that need to can also read from our InputsManager
      const frameData = {
        deltaTime,
        inputs: this.inputsManager,
      }

      // update every entity
      this.entities.forEach(entity => entity.update(frameData))

      // other update logic (here, create new entities)
      if (this.lastBallCreated   this.newBallInterval < newTime) {
        // this is quick and dirty, you should put some more thought into `x` and `y` here
        const newBall = new Ball(this.player.x, 280);
        this.balls.push( newBall );
        this.entities.push( newBall )
        this.lastBallCreated = newTime
      }0
      
      //draw entities
      this.entities.forEach(entity => entity.draw(this.context))

      // remember current time for next update
      this.lastUpdate = newTime
    }

    cleanup() {
      //to prevent memory leak, don't forget to cleanup dead entities
      this.entities.forEach(entity => {
        if (entity.isDead(this.enemy, this.balls)) {
          const index = this.entities.indexOf(entity)
          this.entities.splice(index, 1)
        }
      })
    }

    //main game loop
    loop(endGame) {
      this.myLoop = requestAnimationFrame(() => {
        this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
        if(endGame){
          cancelAnimationFrame(this.myLoop);
          this.endGame();
          return;
        }
        this.update()
        this.cleanup()
        this.loop()
      })
    }
  }

  const canvas = document.querySelector('canvas')
  const game = new Game(canvas)
  game.start()

</script>

</html>  

Но, честно говоря, вся логика здесь кажется странной, и я боюсь, что вы обнаружите, что вам нужно будет переписать многое из этого позже (например, когда вы захотите иметь больше одного врага), но сделать это для вас было бы слишком сложно для ответа SO.

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

1. Спасибо! не могли бы вы очень кратко описать, какой должна быть логика, или указать мне на ресурс, который показывает, что вы имеете в виду, пожалуйста? теперь я чешу голову, я подумал, что это аккуратный способ сделать это (хотя мне нужен только один враг.. мне все еще очень нравится изучать классы)

2. Конечно, class — это аккуратный инструмент, но это не то, что делает дизайн вашей логики. Как и здесь, вы предполагаете, что только один враг будет проверять только один шар, я думаю, я бы построил что-то более похожее на график, где каждый шар может воздействовать непосредственно на каждого врага, которого он касается, не заставляя основное обновление позаботиться об этом и сделать каждую «сущность» автономной. Но на самом деле логический дизайн — это почти все, и если вы хотите, чтобы эта игра была вашей, тогда эта логика должна быть вашей, это, вероятно, самая интересная часть программирования в конце концов. В противном случае вы могли бы просто прочитать код, который уже сделали другие.