Monday 26 October 2020

Game loop failing when speed of ball is faster

I'm building a space invaders game and the code below works, the ball is fired and if it hits the enemy the isDead() function is triggered which also switches dead to true for that ball and this is also passed to the enemy class so it causes the enemy to get destroyed. however when I increase the ball frequency the enemy isDead function fails to run, I'm really not sure why when the ball interval is higher this whole system breaks.

when this.newBallInterval = 700 the enemy square dies

when this.newBallInterval = 600 it doesn't

why? and how to fix?

<!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>


from Game loop failing when speed of ball is faster

No comments:

Post a Comment