gpt4 book ai didi

javascript - 运行多个 requestAnimation 循环来发射多个球?

转载 作者:行者123 更新时间:2023-12-04 08:44:53 24 4
gpt4 key购买 nike

我试图让下面的球继续出现并以设定的间隔在 y 轴上发射,并且总是从桨(鼠标)的 x 位置所在的位置开始,我需要在每次发射球之间有一个延迟。我正在尝试制作太空入侵者,但球会以设定的间隔不断发射。
我是否需要为每个球创建多个 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 && x < b.x + brickWidth && y > b.y && 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 && relativeX < canvas.width - (paddleWidth / 2)) {
paddleX = relativeX - (paddleWidth / 2);
}
}


</script>

</body>

</html>

最佳答案

基本原则
这是您可以做到的一种方法:

  • 您需要一个 Game将处理更新逻辑的对象,存储所有当前实体,处理游戏循环...... IMO,这是您应该跟踪最后一次是什么时候 Ball解雇以及是否解雇一个新的。
    在这个演示中,这个对象还处理当前时间、增量时间和请求动画帧,但有些人可能会争辩说这个逻辑可以被外部化,只需调用某种 Game.update(deltaTime)在每一帧上。
  • 您需要为游戏中的所有实体使用不同的对象。我创建了一个 Entity类,因为我想确保所有游戏实体都具有运行所需的最低限度(即更新、绘制、x、y...)。
    有一个Ball类(class)extends Entity并负责了解自己的参数(速度、大小、...)、如何更新和绘制自身、...
    有一个Paddle我留给你完成的类(class)。

  • 最重要的是,这完全是关注点分离的问题。谁应该知道谁呢?然后传递变量。

    至于你的另一个问题:

    Do I need to create multiple requestAnimationFrame loops for each ball?


    这绝对是可能的,但我认为有一个集中的地方来处理 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>


    管理玩家输入
    现在假设您想在游戏中添加键盘输入。在那种情况下,我实际上会创建一个单独的类,因为根据您想要支持的“按钮”数量,它可能会很快变得非常复杂。
    首先,让我们画一个基本的桨,以便我们可以看到发生了什么:
    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 }
    }
    现在我们添加一个基本的 InputsManager您可以根据需要制作复杂的类(class)。仅用于两个键,处理 keydownkeyup并且可以同时按下两个键这一事实已经是几行代码,因此最好将事情分开以免弄乱我们的 Game目的。
    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 的代码后我们实体实际使用新 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坐标作为桨。您可以阅读我在上面的代码片段中留下的注释,以快速解释其工作原理。

    如何添加功能
    现在,我想让您更全面地了解如何解决在使用此示例时可能遇到的 future 问题。我将以想要测试两个游戏对象之间的碰撞为例。你应该问问自己 把逻辑放在哪里?
  • 所有游戏对象可以共享逻辑的地方在哪里? (创建信息)
  • 您需要在哪里了解碰撞? (访问信息)

  • 在这个例子中,所有的游戏对象都是 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' && 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 / 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 && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
    }
    console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
    return undefined
    }
    }
    现在有很多种 2D 碰撞,所以代码有点冗长,但重点是:这是我在这里做出的设计决定。 我可以成为通才和 future 的证明 但后来看起来像上面那样......我必须添加一个 .collision我所有游戏对象的属性,以便他们知道是否应该将它们视为 'circle'或 ' 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'
    }
    // ...
    }
    或者我可以是极简主义者,只添加我需要的东西,在这种情况下,将代码实际放在 Paddle 中可能更有意义。实体:
    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 && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
    }
    }
    无论哪种方式,我现在都可以访问来自 cleanup 的碰撞信息。函数 Game循环(我选择放置删除死实体的逻辑)。
    使用我的第一个通才解决方案,我会像这样使用它:
    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
    }
    }
    使用第二种极简主义方法,我仍然需要通过播放器进行测试:
    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' && 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 / 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 && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && 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 使其聚焦,以便它可以监听键盘输入(向左箭头,向右箭头)。

    关于javascript - 运行多个 requestAnimation 循环来发射多个球?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64371736/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com