#javascript #canvas #game-development
#javascript #холст #разработка игр
Вопрос:
Я попал в логический узел с этой игрой:(. Я просто хочу удалить взрывы с экрана, скажем, через 1 секунду после выполнения каждого цикла. Как вы можете видеть ниже, они выполняются с частотой кадров игрового цикла. Это единственный способ, которым я мог анимировать взрыв — установив спрайт для перемещения со скоростью игрового цикла (частота кадров).
Я не понимаю, как подключить отдельную анимацию, работающую с другой скоростью, к тому же контексту холста, который по существу очищается каждый кадр.. Я даже не могу понять, как остановить цикл спрайта взрыва.
Я пытался создать отдельный метод drawExplosion()
в классе Explosion и использовать setInterval
его в конструкторе Explosion, но ему никогда не нравится контекст, к которому я его подключаю, и выдает эту ошибку:
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' 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 = 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 amp;amp; this.x < canvas.width) {
if (this.y > 0 amp;amp; this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 amp;amp; 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>
Комментарии:
1. Вы пробовали использовать второй HTML-тег canvas?
2. будет ли это просто рисовать на том же холсте?
Ответ №1:
Что вы можете захотеть сделать, так это использовать логическое значение, которое остается true до тех пор, пока должна выполняться ваша анимация, оно вызовет функцию, которая рисует ваш взрыв.
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 = 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 amp;amp; this.x < canvas.width) {
if (this.y > 0 amp;amp; this.y < canvas.height) {
//console.log('inside')
this.insideCanvas = true;
} else {
this.insideCanvas = false;
}
}
if (this.x - 20 > 0 amp;amp; 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, который позволяет вам использовать системную архитектуру entity component под названием ecsy