#javascript
#javascript
Вопрос:
Я пытаюсь заставить шар ниже продолжать появляться и запускаться по оси y с заданным интервалом и всегда с того места, где находится положение x весла (мыши), мне нужно, чтобы между каждым запуском шара была задержка. Я пытаюсь создать space invaders, но с мячом, постоянно стреляющим с заданным интервалом.
Нужно ли создавать несколько циклов requestAnimationFrame для каждого шара? Может кто-нибудь помочь с очень простым примером того, как это должно быть сделано, пожалуйста, или дать ссылку на хорошую статью? Я застрял на создании массива для каждого шара и не уверен, как спроектировать цикл для достижения этого эффекта. Все примеры, которые я могу найти, слишком сложны
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* {
padding: 0;
margin: 0;
}
canvas {
background: #eee;
display: block;
margin: 0 auto;
width: 30%;
}
</style>
</head>
<body>
<canvas id="myCanvas" height="400"></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
//start the requestAnimationFrame loop
var myRequestAnimation;
var myRequestAnimationBall;
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
drawLoop();
setInterval(drawBallLoop, 400);
var x = canvas.width / 2;
var y = canvas.height - 30;
var defaultSpeedX = 0;
var defaultSpeedY = 4;
var dx = defaultSpeedX;
var dy = -defaultSpeedY;
var ballRadius = 10;
var paddleX = (canvas.width - paddleWidth) / 2;
var paddleHeight = 10;
var paddleWidth = 70;
//control stuff
var rightPressed = false;
var leftPressed = false;
var brickRowCount = 1;
var brickColumnCount = 1;
var brickWidth = 40;
var brickHeight = 20;
var brickPadding = 10;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;
var score = 0;
var lives = 3;
//paddle
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
//bricks
function drawBricks() {
for (var c = 0; c < brickColumnCount; c ) {
for (var r = 0; r < brickRowCount; r ) {
if (bricks[c][r].status == 1) {
var brickX = (c * (brickWidth brickPadding)) brickOffsetLeft;
var brickY = (r * (brickHeight brickPadding)) brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
//collision detection
function collisionDetection() {
for (var c = 0; c < brickColumnCount; c ) {
for (var r = 0; r < brickRowCount; r ) {
var b = bricks[c][r];
if (b.status == 1) {
if (x > b.x amp;amp; x < b.x brickWidth amp;amp; y > b.y amp;amp; y < b.y brickHeight) {
dy = -dy;
b.status = 0;
score ;
console.log(score);
if (score == brickRowCount * brickColumnCount) {
console.log("YOU WIN, CONGRATS!");
window.cancelAnimationFrame(myRequestAnimation);
}
}
}
}
}
}
//default bricks
var bricks = [];
for (var c = 0; c < brickColumnCount; c ) {
bricks[c] = [];
for (var r = 0; r < brickRowCount; r ) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
//lives
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Lives: " lives, canvas.width - 65, 20);
}
// ball1
var ball1 = {
x,
y,
directionX: 0,
directionY: -5
}
// ball1
var ball2 = {
x,
y,
directionX: 0,
directionY: -2
}
// put each ball in a balls[] array
var balls = [ball1, ball2];
function drawBall() {
// clearCanvas();
for (var i = 0; i < balls.length; i ) {
var ball = balls[i]
ctx.beginPath();
ctx.arc(ball.x, ball.y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
///////DRAW BALL LOOP////////
function drawBallLoop() {
myRequestAnimationBall = requestAnimationFrame(drawBallLoop);
// clear frame
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawBall();
//move balls
for (var i = 0; i < balls.length; i ) {
balls[i].y = balls[i].directionY;
}
}
//Clear Canvas
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
///////DRAW MAIN LOOP////////
function drawLoop() {
myRequestAnimation = requestAnimationFrame(drawLoop);
// clear frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawPaddle();
drawBricks();
collisionDetection();
drawLives();
//bounce off walls
if (x dx > canvas.width - ballRadius || x dx < ballRadius) {
dx = -dx;
}
if (rightPressed) {
paddleX = 7;
if (paddleX paddleWidth > canvas.width) {
paddleX = canvas.width - paddleWidth;
}
}
else if (leftPressed) {
paddleX -= 7;
if (paddleX < 0) {
paddleX = 0;
}
}
}
//keyboard left/right logic
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
function keyDownHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = true;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = false;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = false;
}
}
//relative mouse pos
function 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
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
}
}
//mouse movemment
document.addEventListener("mousemove", mouseMoveHandler, false);
function mouseMoveHandler(e) {
var mouseX = getMousePos(canvas, e).x;
//e.clientX = the horizontal mouse position in the viewport
//canvas.offsetLeft = the distance between the left edge of the canvas and left edge of the viewport
var relativeX = mouseX;
// console.log('mouse= ',relativeX, canvas.offsetLeft)
// console.log('paddle= ', paddleX);
// console.log(getMousePos(canvas, e).x);
if (relativeX - (paddleWidth / 2) > 0 amp;amp; relativeX < canvas.width - (paddleWidth / 2)) {
paddleX = relativeX - (paddleWidth / 2);
}
}
</script>
</body>
</html>
Ответ №1:
Основные принципы
Вот один из способов, которым вы могли бы это сделать:
-
вам нужен
Game
объект, который будет обрабатывать логику обновления, хранить все текущие объекты, обрабатывать игровой цикл… ИМО, именно здесь вы должны отслеживать, когда был запущен последнийBall
и запускать ли новый.В этой демонстрации этот объект также обрабатывает текущее время, дельта-время и запрашивает кадры анимации, но некоторые могут возразить, что эта логика может быть экстернализирована и просто вызывать какой-то вид
Game.update(deltaTime)
для каждого кадра.
-
вам нужны разные объекты для всех объектов в вашей игре. Я создал
Entity
класс, потому что хочу убедиться, что все игровые объекты имеют минимум, необходимый для функционирования (т. е. Обновление, рисование, x, y …).Существует
Ball
класс, которыйextends Entity
и отвечает за знание собственных параметров (скорость, размер, …), как обновлять и рисовать себя…Есть
Paddle
класс, который я оставил открытым для вас.
Суть в том, что все зависит от разделения задач. Кто должен знать, что о ком? А затем передавайте переменные.
Что касается вашего другого вопроса:
Нужно ли создавать несколько циклов requestAnimationFrame для каждого шара?
Это определенно возможно, но я бы сказал, что наличие централизованного места, которое обрабатывает lastUpdate
, deltaTime
, lastBallCreated
значительно упрощает. И на практике разработчики, как правило, пытаются использовать для этого один цикл анимации.
class Entity {
constructor(x, y) {
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`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.speed = 100 // px per second
this.size = 10 // radius in px
}
update(deltaTime) {
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.size, 0, 2 * Math.PI)
context.fill()
}
isDead() {
return this.y < 0 - this.size
}
}
class Paddle extends Entity {
constructor() {
super(0, 0)
}
update() { /**/ }
draw() { /**/ }
isDead() { return false }
}
class Game {
/** @param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 1000 // ms between each ball
this.lastBallCreated = 0 // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
const paddle = new Paddle()
this.entities.push(paddle)
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
this.entities.forEach(entity => entity.update(deltaTime))
// other update logic (here, create new entities)
if(this.lastBallCreated this.newBallInterval < newTime) {
const ball = new Ball(100, 300) // this is quick and dirty, you should put some more thought into `x` and `y` here
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
Managing player inputs
Now let’s say you want to add keyboard inputs to your game. In that case, I’d actually create a separate class, because depending on how many «buttons» you want to support, it can get very complicated very quick.
So first, let’s draw a basic paddle so we can see what’s happening:
class Paddle extends Entity {
constructor() {
// we just add a default initial x,y and height,width
super(150, 20)
this.width = 50
this.height = 10
}
update() { /**/ }
/** @param {CanvasRenderingContext2D} context */
draw(context) {
// we just draw a simple rectangle centered on x,y
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
And now we add a basic InputsManager
class that you can make as complicated as you want. Just for two keys, handling keydown
and keyup
and the fact that two keys can be pressed at once it already a few lines of code so it’s good to keep things separate so as to not clutter our Game
object.
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
}
}
}
Теперь мы можем обновить наш Game
класс, чтобы использовать этот новый InputsManager
class Game {
// ...
start() {
// ...
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
} // we now pass more data to the update method so that entities that need to can also read from our InputsManager
this.entities.forEach(entity => entity.update(frameData))
}
// ...
}
И после обновления кода для update
методов наших объектов, чтобы фактически использовать new InputsManager
, вот результат:
class Entity {
constructor(x, y) {
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`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
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.fill()
}
isDead() {
return this.y < 0 - this.radius
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.speed = 200
this.width = 50
this.height = 10
}
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.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
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)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
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 = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
// 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)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated this.newBallInterval < newTime) {
// we can now read from this.player to the the position of where to fire a Ball
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Как только вы нажмете «Запустить фрагмент кода», вам нужно щелкнуть по iframe, чтобы сфокусировать его, чтобы он мог прослушивать ввод с клавиатуры (стрелка влево, стрелка вправо).
В качестве бонуса, поскольку теперь мы можем рисовать и перемещать весло, я добавил возможность создавать шар в той же x
координате, что и весло. Вы можете прочитать комментарии, которые я оставил во фрагменте кода выше, для быстрого объяснения того, как это работает.
Как добавить функциональность
Теперь я хочу дать вам более общее представление о том, как подходить к будущим проблемам, которые могут возникнуть при использовании этого примера. Я возьму пример с желанием проверить столкновение между двумя игровыми объектами. Вы должны спросить себя, где разместить логику?
- где находится место, где все игровые объекты могут использовать общую логику? (создание информации)
- где вам нужно знать о столкновениях? (доступ к информации)
В этом примере все игровые объекты являются подклассами Entity
, поэтому для меня имеет смысл поместить туда код:
class Entity {
constructor(x, y) {
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') {
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 / 2
const bottomOfBallIsBelowTopOfRect = circle.y circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x rect.width / 2
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
}
}
Теперь существует много видов 2D-коллизий, поэтому код немного подробный, но главное: это дизайнерское решение, которое я здесь принимаю. Я могу быть универсалом и будущим доказательством этого, но тогда это выглядит так, как указано выше… И я должен добавить .collision
свойство ко всем моим игровым объектам, чтобы они знали, следует ли их рассматривать как a 'circle'
или a ‘ rect'
в приведенном выше алгоритме.
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
}
// ...
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
}
// ...
}
Or I can be minimalist and just add what I need, in which case it might make more sense to actually put the code in the Paddle
entity:
class Paddle extends Entity {
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
}
}
Either way, I now have access to the collision information from the cleanup
function the Game
loop (where I chose to place the logic of removing dead entities).
With my first generalist solution, I would use it like this:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
With the second minimalist approach, I’d still have to pass the player around for the test:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = player.testBallCollision(this)
return outOfBounds || collidesWithPlayer
}
}
Конечный результат
Надеюсь, вы чему-то научились. В то же время, вот конечный результат этого очень длинного сообщения с ответом:
class Entity {
constructor(x, y) {
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') {
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 / 2
const bottomOfBallIsBelowTopOfRect = circle.y circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x rect.width / 2
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
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({deltaTime}) {
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.fill()
}
isDead(player) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
this.speed = 200
this.width = 50
this.height = 10
}
update({deltaTime, 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.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
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)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
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 = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
this.player = new Paddle()
this.entities.push(this.player)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated this.newBallInterval < newTime) {
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Как только вы нажмете «Запустить фрагмент кода», вам нужно щелкнуть по iframe, чтобы сфокусировать его, чтобы он мог прослушивать ввод с клавиатуры (стрелка влево, стрелка вправо).
Комментарии:
1. большое спасибо за этот .. вопрос.. куда вы помещаете прослушиватели событий keydown и keyup, чтобы они могли влиять на классы ball и paddle? Я предполагаю, что наличие двух прослушивателей событий — плохая практика.. структура классов выводит меня из себя. Кроме того, я использую game.paddleMiddle изнутри игрового класса для перемещения весла .. это нормально? codepen.io/agrushevskiy/pen/PozzVxV
2. Есть много вещей, которые нужно решить, я отредактирую свой пост несколько раз подряд 😉
3. потрясающе, большое вам спасибо.. просто смотрел на это github.com/dwmkerr/spaceinvaders/blob/master/js / … почему он использует функции в качестве классов для своей игры, а затем добавляет методы к этим функциям, используя цепочку прототипов? это запах кода??
4. @AGrush совсем не пахнет кодом. Это старый школьный способ делать то же самое.
Class
это просто более свежий способ его написания, но в итоге он делает то же самое. Это создает «область видимости» (на самом деле объект), где вы можете хранить переменные и функции.5. @AGrush Я обновил код, включив в него объяснение управления вводом с клавиатуры (которое вы можете расширить для управления любым типом ввода).