亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
已解決430363個問題,去搜搜看,總會有你想問的

運行多個 requestAnimation 循環來發射多個球?

運行多個 requestAnimation 循環來發射多個球?

慕姐4208626 2023-05-18 09:44:34
我試圖讓下面的球以設定的間隔繼續出現并在 y 軸上發射,并且總是從球拍(鼠標)的 x 位置開始,我需要在每次發射球之間有一個延遲。我正在嘗試制作太空入侵者,但球會以設定的間隔不斷發射。我是否需要為每個球創建多個 requestAnimationFrame 循環?有人可以提供一個非?;镜氖纠齺碚f明如何完成此操作或鏈接一篇好文章嗎?我堅持為每個球創建一個數組,但不確定如何設計循環來實現這種效果。我能找到的所有例子都太復雜了
查看完整描述

1 回答

?
冉冉說

TA貢獻1877條經驗 獲得超1個贊

基本原則

這是您可以做到的一種方法:

  1. 你需要一個Game對象來處理更新邏輯,存儲所有當前實體,處理游戲循環...... IMO,這是你應該跟蹤最后一次發射的時間Ball以及是否發射新的。

    在這個演示中,這個對象還處理當前時間、增量時間和請求動畫幀,但有些人可能會爭辯說這個邏輯可以外部化,并且只需在每個幀上調用某種形式Game.update(deltaTime)。


  1. 您需要為游戲中的所有實體使用不同的對象。我創建了一個Entity類,因為我想確保所有游戲實體都具有運行所需的最低要求(即更新、繪制、x、y...)。

    有一個Ballextends Entity負責了解它自己的參數(速度,大小,......),如何更新和繪制自己,......

    我留下了一Paddle門課讓你完成。


歸根結底,這完全是關注點分離的問題。誰應該知道誰的事?然后傳遞變量。


至于你的另一個問題:

我是否需要為每個球創建多個 requestAnimationFrame 循環?

這絕對是可能的,但我認為有一個集中的地方來處理lastUpdatedeltaTime,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類,您可以根據需要將其復雜化。僅針對兩個鍵,處理keydown和keyup可以同時按下兩個鍵的事實已經有幾行代碼,因此最好將事情分開,以免弄亂我們的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在更新實體方法的代碼以實際使用 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' && 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 碰撞,所以代碼有點冗長,但要點是:這是我在這里做出的設計決定。我可以成為通才和未來證明這一點,但它看起來像上面......我必須.collision向我的所有游戲對象添加一個屬性,以便它們知道它們是否應該在上述算法中被視為 a'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 以使其聚焦,以便它可以偵聽鍵盤輸入(左箭頭,右箭頭)。



查看完整回答
反對 回復 2023-05-18
  • 1 回答
  • 0 關注
  • 409 瀏覽
慕課專欄
更多

添加回答

舉報

0/150
提交
取消
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號