对游戏循环动画运行单独的画布动画



我在这个游戏中陷入了一个逻辑结:(.我只想在每个循环完成1秒后从屏幕上删除爆炸。正如你在下面看到的,它们以游戏循环的帧速率运行。这是我可以动画化爆炸的唯一方法-设置一个精灵以游戏循环(帧速率(的速度移动。

我不明白如何将以不同速度运行的单独动画连接到基本上每帧都被清除的同一画布上下文。。我甚至不知道如何停止爆炸精灵循环。

我尝试在Explosion类中创建一个单独的方法drawExplosion(),并在Explosion构造函数中使用setInterval,但它从不喜欢我将其连接到的上下文,并抛出以下错误:

Cannot read property 'drawImage' of undefined (i.e. the context is undefined)

如果有人能在1秒后停止每个爆炸循环,我就会明白我偏离了方向

代码的轮廓是这样的:

class Entity
class Ball extends Entity
class Explosion extends Entity
class Enemy extends Entity
class Paddle extends Entity
class InputsManager
class mouseMoveHandler
class Game
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()

<!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;
margin: 0;
padding: 0;
}
canvas {
background: url("https://picsum.photos/200");
width: 100%;
background-size: cover;
}
</style>
</head>
<body>
<canvas height="459"></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 = 2.5 // radius in px
this.color = '#fff'
this.ballsDistanceY = 12
}
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 = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y + this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y - this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
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;
game.hitEnemy();
return true
}
}
}

class Explosion extends Entity {
constructor(x, y, contextFromGameObject){
super(x, y)
this.contextFromGameObject = contextFromGameObject
this.imgExplosion = new Image();
this.imgExplosion.src = "https://i.ibb.co/9Ggfzxr/explosion.png";
this.totalNumberOfFrames = 12 // ten images in the image (see the url above)
this.spriteFrameNumber = 0 // This is changed to make the sprite animate  
this.widthOfSprite = 1200 // this.imgExplosion.width; // find the width of the image
this.heightOfSprite = 100 // this.imgExplosion.height; // find the height of the image
this.widthOfSingleImage = this.widthOfSprite / this.totalNumberOfFrames; // The width of each image in the spirite
//this.timerId = setInterval(this.explode.bind(this), 100)
this.scaleExplosion = 0.5

//this.timerId = setInterval(this.drawExplosion, 100);
}
// drawExplosion(){
//   console.log(this.spriteFrameNumber)
//   //ctx.clearRect(0, 0, 500, 500)
//   this.spriteFrameNumber += 1; // changes the sprite we look at
//   this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...

//   this.contextFromGameObject.drawImage(this.imgExplosion,
//     this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
//     this.widthOfSingleImage, this.heightOfSprite, // width and height
//     this.x - 25, this.y - 25, // x and y - where on the screen
//     this.widthOfSingleImage, this.heightOfSprite // width and height
//   );
//   if (this.spriteFrameNumber > 9) {
//     clearInterval(this.timerId)
//   };
// }
/** @param {CanvasRenderingContext2D} context */
draw(context, frameNumber) {
console.log(frameNumber)

//ctx.clearRect(0, 0, 500, 500)
this.spriteFrameNumber += 1; // changes the sprite we look at
this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...

context.drawImage(this.imgExplosion,
this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
this.widthOfSingleImage, this.heightOfSprite, // width and height
this.x - 25, this.y - 25, // x and y - where on the screen
this.widthOfSingleImage, this.heightOfSprite // width and height
);
}

update() {
}
isDead(ball, isDead) {
if(isDead == 'true'){
clearTimeout(this.timerId);
return true
}
return false
}
} 

class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speedVar = 4;
this.speed = this.speedVar;
this.color = '#EC3AC8';
this.color2 = '#000000';
this.y = y;
this.imgEnemy = new Image();
this.imgEnemy.src = "https://i.ibb.co/kgXsr66/question.png";

this.runCount = 1;
this.timerId = setInterval(this.movePosish.bind(this), 1000);
}
movePosish() {
//console.log(this.runCount)
// x 10 -> 240
// y 10 -> 300
switch (this.runCount) {
case 0:
this.x = 20; this.y = 200;
break
case 1:
this.x = 200; this.y = 300;
break
case 2:
this.x = 30; this.y = 20;
break
case 3:
this.x = 230; this.y = 150;
break
case 4:
this.x = 200; this.y = 20;
break
case 5:
this.x = 30; this.y = 90;
break
case 6:
this.x = 240; this.y = 20;
break
case 7:
this.x = 30; this.y = 150;
break
case 8:
this.x = 180; this.y = 170;
break
case 9:
this.x = 30; this.y = 50;
break
case 10:
this.x = 130; this.y = 170;
break
}
//if 10th image remove image and clear timer
this.runCount += 1;
if (this.runCount > 10) {
//clearInterval(this.timerId)
this.runCount = 0;
console.log('ya missed 10 of em')
};
}
update() {
// //Moving left/right
// this.x += this.speed;
// if (this.x > canvas.width - this.width) {
//   this.speed -= this.speedVar;
// }
// if (this.x === 0) {
//   this.speed += this.speedVar;
// }
}
/** @param {CanvasRenderingContext2D} context */
draw(context) {
// context.beginPath();
// context.rect(this.x, this.y, this.width, this.height);
// context.fillStyle = this.color2;
// context.fill();
// context.closePath();
context.drawImage(this.imgEnemy, this.x, this.y);
}
isDead(enemy) {
//// 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
// }
return false
}
}

class Paddle extends Entity {
constructor(x, width) {
super(x, width)
this.collision = 'rect'
this.speed = 200
this.height = 25
this.width = 30
this.color = "#74FCEF"
}
update({ deltaTime, inputs, mouse }) {
// Paddle needs to read both deltaTime and inputs
// if mouse inside canvas AND not on mobile
if (mouse.insideCanvas) {
this.x = mouse.paddleX
} else {
this.x += this.speed * deltaTime / 1000 * inputs.direction
// stop from going off screen
if (this.x < this.width / 2) {
this.x = this.width / 2;
} else if (this.x > canvas.width - this.width / 2) {
this.x = canvas.width - this.width / 2
}
}
}
/** @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 = this.color;
context.fill();
context.closePath();
context.beginPath();
context.rect(this.x - this.width / 12, this.y - this.height / 1.1, this.width / 6, this.height);
context.fillStyle = this.color;
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 mouseMoveHandler {
constructor() {
// this.paddleWidth = paddleWidth;
this.x = 0;
this.paddleX = 0;
//this.canvas = canvas;
document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
}
//'relative to canvas width' mouse position snippet
getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect(), // abs. size of element
scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y
//console.log('canvas width = ' + canvas.width)
return {
x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
}
}
onMouseMove(e) {
//console.log('moving')
//this.x = 100;
this.x = this.getMousePos(canvas, e).x; //relative x on canvas
this.y = this.getMousePos(canvas, e).y; //relative x on canvas
this.insideCanvas = false;
if (this.x > 0 && this.x < canvas.width) {
if (this.y > 0 && this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 && this.x < canvas.width - 20) {
this.paddleX = this.x;
}
}
}

class Game {
/** @param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 900 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
this.paddleWidth = 50
this.isMobile = false
this.frameNumber = 0;
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('endgame')
const endGame = true;
game.loop(endGame)
}
hitEnemy() {
//this.timerId = setTimeout(endExplosion(), 500);
this.explosion = new Explosion(this.enemy.x, this.enemy.y, this.context)
this.entities.push(this.explosion)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(140, 220)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle(150, 400)
// 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 watching mousemovement
this.mouseMoveHandler = new mouseMoveHandler()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
this.isMobile = window.matchMedia('(max-width: 1199px)');

// 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,
mouse: this.mouseMoveHandler,
context: this.context
}
// 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, 360)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime

}
cleanup() {
//console.log(this.entities[0])//Enemy
//console.log(this.entities[1])//Paddle
//console.log(this.entities[2])//Ball
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
draw() {
//draw entities
this.entities.forEach(entity => entity.draw(this.context, this.frameNumber))
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.frameNumber += 1;
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if (endGame) {
cancelAnimationFrame(this.myLoop);
this.endGame()
return;
}
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>

您可能想使用一个布尔值,只要您的动画应该运行,它就会调用绘制爆炸的函数。

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 = 2.5 // radius in px
this.color = '#fff'
this.ballsDistanceY = 12
}
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 = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y + this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x, this.y - this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
context.fillStyle = this.color;
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;
game.hitEnemy();
return true
}
}
}

class Explosion extends Entity {
constructor(x, y, contextFromGameObject){
super(x, y)
this.contextFromGameObject = contextFromGameObject
this.imgExplosion = new Image();
this.imgExplosion.src = "https://i.ibb.co/9Ggfzxr/explosion.png";
this.totalNumberOfFrames = 12 // ten images in the image (see the url above)
this.spriteFrameNumber = 0 // This is changed to make the sprite animate  
this.widthOfSprite = 1200 // this.imgExplosion.width; // find the width of the image
this.heightOfSprite = 100 // this.imgExplosion.height; // find the height of the image
this.widthOfSingleImage = this.widthOfSprite / this.totalNumberOfFrames; // The width of each image in the spirite
//this.timerId = setInterval(this.explode.bind(this), 100)
this.scaleExplosion = 0.5

this.explosionHappened = 0;

//this.timerId = setInterval(this.drawExplosion, 100);
}
// drawExplosion(){
//   console.log(this.spriteFrameNumber)
//   //ctx.clearRect(0, 0, 500, 500)
//   this.spriteFrameNumber += 1; // changes the sprite we look at
//   this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...

//   this.contextFromGameObject.drawImage(this.imgExplosion,
//     this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
//     this.widthOfSingleImage, this.heightOfSprite, // width and height
//     this.x - 25, this.y - 25, // x and y - where on the screen
//     this.widthOfSingleImage, this.heightOfSprite // width and height
//   );
//   if (this.spriteFrameNumber > 9) {
//     clearInterval(this.timerId)
//   };
// }
/** @param {CanvasRenderingContext2D} context */
draw(context, frameNumber) {
//console.log(frameNumber)

if(this.explosionHappened)
{

//ctx.clearRect(0, 0, 500, 500)
this.spriteFrameNumber += 1; // changes the sprite we look at
this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...

context.drawImage(this.imgExplosion,
this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
this.widthOfSingleImage, this.heightOfSprite, // width and height
this.x - 25, this.y - 25, // x and y - where on the screen
this.widthOfSingleImage, this.heightOfSprite // width and height
);
this.explosionHappened=this.spriteFrameNumber;
}
}

update() {
}
isDead(ball, isDead) {
if(isDead == 'true'){
clearTimeout(this.timerId);
return true
}
return false
}
} 

class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speedVar = 4;
this.speed = this.speedVar;
this.color = '#EC3AC8';
this.color2 = '#000000';
this.y = y;
this.imgEnemy = new Image();
this.imgEnemy.src = "https://i.ibb.co/kgXsr66/question.png";

this.runCount = 1;
this.timerId = setInterval(this.movePosish.bind(this), 1000);
}
movePosish() {
//console.log(this.runCount)
// x 10 -> 240
// y 10 -> 300
switch (this.runCount) {
case 0:
this.x = 20; this.y = 200;
break
case 1:
this.x = 200; this.y = 300;
break
case 2:
this.x = 30; this.y = 20;
break
case 3:
this.x = 230; this.y = 150;
break
case 4:
this.x = 200; this.y = 20;
break
case 5:
this.x = 30; this.y = 90;
break
case 6:
this.x = 240; this.y = 20;
break
case 7:
this.x = 30; this.y = 150;
break
case 8:
this.x = 180; this.y = 170;
break
case 9:
this.x = 30; this.y = 50;
break
case 10:
this.x = 130; this.y = 170;
break
}
//if 10th image remove image and clear timer
this.runCount += 1;
if (this.runCount > 10) {
//clearInterval(this.timerId)
this.runCount = 0;
console.log('ya missed 10 of em')
};
}
update() {
// //Moving left/right
// this.x += this.speed;
// if (this.x > canvas.width - this.width) {
//   this.speed -= this.speedVar;
// }
// if (this.x === 0) {
//   this.speed += this.speedVar;
// }
}
/** @param {CanvasRenderingContext2D} context */
draw(context) {
// context.beginPath();
// context.rect(this.x, this.y, this.width, this.height);
// context.fillStyle = this.color2;
// context.fill();
// context.closePath();
context.drawImage(this.imgEnemy, this.x, this.y);
}
isDead(enemy) {
//// 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
// }
return false
}
}

class Paddle extends Entity {
constructor(x, width) {
super(x, width)
this.collision = 'rect'
this.speed = 200
this.height = 25
this.width = 30
this.color = "#74FCEF"
}
update({ deltaTime, inputs, mouse }) {
// Paddle needs to read both deltaTime and inputs
// if mouse inside canvas AND not on mobile
if (mouse.insideCanvas) {
this.x = mouse.paddleX
} else {
this.x += this.speed * deltaTime / 1000 * inputs.direction
// stop from going off screen
if (this.x < this.width / 2) {
this.x = this.width / 2;
} else if (this.x > canvas.width - this.width / 2) {
this.x = canvas.width - this.width / 2
}
}
}
/** @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 = this.color;
context.fill();
context.closePath();
context.beginPath();
context.rect(this.x - this.width / 12, this.y - this.height / 1.1, this.width / 6, this.height);
context.fillStyle = this.color;
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 mouseMoveHandler {
constructor() {
// this.paddleWidth = paddleWidth;
this.x = 0;
this.paddleX = 0;
//this.canvas = canvas;
document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
}
//'relative to canvas width' mouse position snippet
getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect(), // abs. size of element
scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y
//console.log('canvas width = ' + canvas.width)
return {
x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
}
}
onMouseMove(e) {
//console.log('moving')
//this.x = 100;
this.x = this.getMousePos(canvas, e).x; //relative x on canvas
this.y = this.getMousePos(canvas, e).y; //relative x on canvas
this.insideCanvas = false;
if (this.x > 0 && this.x < canvas.width) {
if (this.y > 0 && this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 && this.x < canvas.width - 20) {
this.paddleX = this.x;
}
}
}

class Game {
/** @param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 900 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
this.paddleWidth = 50
this.isMobile = false
this.frameNumber = 0;
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('endgame')
const endGame = true;
game.loop(endGame)
}
hitEnemy() {
//this.timerId = setTimeout(endExplosion(), 500);
this.explosion = new Explosion(this.enemy.x, this.enemy.y, this.context)
this.explosion.explosionHappened=1;
this.entities.push(this.explosion)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(140, 220)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle(150, 400)
// 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 watching mousemovement
this.mouseMoveHandler = new mouseMoveHandler()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
this.isMobile = window.matchMedia('(max-width: 1199px)');

// 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,
mouse: this.mouseMoveHandler,
context: this.context
}

// 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, 360)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime

}
cleanup() {
//console.log(this.entities[0])//Enemy
//console.log(this.entities[1])//Paddle
//console.log(this.entities[2])//Ball
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
draw() {
//draw entities
this.entities.forEach(entity => entity.draw(this.context, this.frameNumber))
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.frameNumber += 1;
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if (endGame) {
cancelAnimationFrame(this.myLoop);
this.endGame()
return;
}
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
}
canvas {
background: url("https://picsum.photos/200");
width: 100%;
background-size: cover;
}
<canvas height="459"></canvas>

作为补充信息,我自己也是ecs体系结构的粉丝,为了实现爆炸,或者任何其他你可以干净地使用系统的游戏机制,有一个(实验性的(firefox项目允许你使用实体组件系统体系结构,称为ecsy

https://ecsy.io

最新更新