当球的速度较快时,比赛循环失败



我正在构建一个太空入侵者游戏,下面的代码可以工作,球被发射,如果它击中敌人,就会触发isDead((函数,该函数也会将该球的dead切换为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>

问题是你正在覆盖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来说太难了。

最新更新