球速更快时,游戏循环失败

时间:2020-10-21 10:31:25

标签: javascript

我正在构建一个太空侵略者游戏,并且下面的代码起作用,该球被发射,并且如果它击中了敌人,则isDead()函数将被触发,该函数还将该球的死点切换为true,这也将传递给敌人的阶级,因此导致敌人被摧毁。但是,当我提高击球频率时,敌人的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' && b.collision === 'circle') {
        return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
      }
      if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
        let circle = a.collision === 'circle' ? a : b
        let rect = a.collision === 'rect' ? a : b
        // this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
        const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height 
        const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height 
        const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
        const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
        return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
      }
      console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
      return undefined
    }

    static testBallCollision(ball) {
      const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
      const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
      const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
      const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
      return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
    }
  }

  class Ball extends Entity {
    constructor(x, y) {
      super(x, y)
      this.dead = false;
      this.collision = 'circle'
      this.speed = 300 // px per second
      this.radius = 10 // radius in px
    }

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

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

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




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

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

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

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

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

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




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

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

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





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

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

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



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

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

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

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

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

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

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

      //start game loop
      this.loop()
    }

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

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

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

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

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

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

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

</script>

</html>

1 个答案:

答案 0 :(得分:2)

问题在于您正在覆盖this.ball,所以您的敌人只会检查新产生的敌人,而现在还没有dead

您可以将所有的球简单地存储在它们自己的Array中,并通过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' && b.collision === 'circle') {
        return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
      }
      if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
        let circle = a.collision === 'circle' ? a : b
        let rect = a.collision === 'rect' ? a : b
        // this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
        const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height 
        const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height 
        const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
        const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
        return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
      }
      console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
      return undefined
    }

    static testBallCollision(ball) {
      const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
      const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
      const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
      const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
      return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
    }
  }

  class Ball extends Entity {
    constructor(x, y) {
      super(x, y)
      this.dead = false;
      this.collision = 'circle'
      this.speed = 300 // px per second
      this.radius = 10 // radius in px
    }

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

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

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




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

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

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

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

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

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




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

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

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





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

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

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



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

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

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

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

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

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

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

      //start game loop
      this.loop()
    }

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

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

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

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

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

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

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

</script>

</html>

但是说实话,这里的整个逻辑似乎很奇怪,而且恐怕您会发现以后需要重写很多内容(例如,当您想拥有一个以上的敌人时),但是这样做对于一个SO答案,您将太多了。