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