问题描述
我被这个游戏困扰:(。我只是想在每个循环进行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,2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x,this.y + this.ballsdistanceY,this.y - this.ballsdistanceY,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,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,// 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,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,// x and y - where in the sprite
this.widthOfSingleImage,// width and height
this.x - 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)
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.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.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,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>
解决方法
您可能想做的就是使用一个布尔值,只要您的动画应该运行,它就保持为真,它将调用绘制爆炸的函数。
<form name="quiz" id="quiz">
<ol>
<li>
<div class="q1">
<p><strong>The value of \(\frac{1}{{{{\log }_4}120}} + \frac{1}{{{{\log }_5}120}} + \frac{1}{{{{\log }_6}120}}\) is</strong><br><br>
<input type="radio" name="question1" value="0">A.0<br>
<input type="radio" name="question1" value="1">B.1<br>
<input type="radio" name="question1" value="24">C.24<br>
<input type="radio" name="question1" value="120">D.120<br>
</p>
</div>
<div>
<button class="play">Play Solution</button>
</div>
</li>
<li>
<div>
<p><strong>For a 3x3 matrix A,|A| = 4 and adj A = \(\left( {\begin{array}{*{20}{c}}1&p&3\\1&3&3\\2&4&4\end{array}} \right)\),then the value of p is</strong><br><br>
<input type="radio" name="question2" value="4">A.4<br>
<input type="radio" name="question2" value="11">B.11<br>
<input type="radio" name="question2" value="5">C.5<br>
<input type="radio" name="question2" value="0">D.0<br>
</p>
</div>
</li>
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,2 * Math.PI)
context.fillStyle = this.color;
context.fill()
context.beginPath()
context.arc(this.x,this.y + this.ballsDistanceY,this.y - this.ballsDistanceY,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,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,// 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,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,// x and y - where in the sprite
this.widthOfSingleImage,// width and height
this.x - 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)
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.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.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,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;
}
仅出于补充信息,我本人是ECS体系结构的忠实拥护者,为了实现爆炸效果,或者您可以干净地使用任何其他游戏机制,可以使用系统,这里有一个(实验性)firefox项目,可让您使用实体组件系统架构称为ecsy