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

我试图让下面的球以设定的间隔继续出现并在 y 轴上发射,并且总是从球拍(鼠标)的 x 位置开始,我需要在每次发射球之间有一个延迟。我正在尝试制作太空入侵者,但球会以设定的间隔不断发射。

我是否需要为每个球创建多个 requestAnimationFrame 循环?有人可以提供一个非常基本的示例来说明如何完成此操作或链接一篇好文章吗?我坚持为每个球创建一个数组,但不确定如何设计循环来实现这种效果。我能找到的所有例子都太复杂了



慕姐4208626
浏览 379回答 1
1回答

冉冉说

基本原则这是您可以做到的一种方法:你需要一个Game对象来处理更新逻辑,存储所有当前实体,处理游戏循环...... IMO,这是你应该跟踪最后一次发射的时间Ball以及是否发射新的。在这个演示中,这个对象还处理当前时间、增量时间和请求动画帧,但有些人可能会争辩说这个逻辑可以外部化,并且只需在每个帧上调用某种形式Game.update(deltaTime)。您需要为游戏中的所有实体使用不同的对象。我创建了一个Entity类,因为我想确保所有游戏实体都具有运行所需的最低要求(即更新、绘制、x、y...)。有一个Ball类extends Entity负责了解它自己的参数(速度,大小,......),如何更新和绘制自己,......我留下了一Paddle门课让你完成。归根结底,这完全是关注点分离的问题。谁应该知道谁的事?然后传递变量。至于你的另一个问题:我是否需要为每个球创建多个 requestAnimationFrame 循环?这绝对是可能的,但我认为有一个集中的地方来处理lastUpdate,&nbsp;deltaTime,lastBallCreated让事情变得简单得多。在实践中,开发者倾向于为此尝试使用单个动画帧循环。class Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; this.x = x&nbsp; &nbsp; &nbsp; &nbsp; this.y = y&nbsp; &nbsp; }&nbsp; &nbsp; update() { console.warn(`${this.constructor.name} needs an update() function`) }&nbsp; &nbsp; draw() { console.warn(`${this.constructor.name} needs a draw() function`) }&nbsp; &nbsp; isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }}class Ball extends Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; super(x, y)&nbsp; &nbsp; &nbsp; &nbsp; this.speed = 100 // px per second&nbsp; &nbsp; &nbsp; &nbsp; this.size = 10 // radius in px&nbsp; &nbsp; }&nbsp; &nbsp; update(deltaTime) {&nbsp; &nbsp; &nbsp; &nbsp; this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000&nbsp; &nbsp; }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.arc(this.x, this.y, this.size, 0, 2 * Math.PI)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead() {&nbsp; &nbsp; &nbsp; &nbsp; return this.y < 0 - this.size&nbsp; &nbsp; }}class Paddle extends Entity {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; super(0, 0)&nbsp; &nbsp; }&nbsp; &nbsp; update() { /**/ }&nbsp; &nbsp; draw() { /**/ }&nbsp; &nbsp; isDead() { return false }}class Game {&nbsp; &nbsp; /** @param {HTMLCanvasElement} canvas */&nbsp; &nbsp; constructor(canvas) {&nbsp; &nbsp; &nbsp; &nbsp; this.entities = [] // contains all game entities (Balls, Paddles, ...)&nbsp; &nbsp; &nbsp; &nbsp; this.context = canvas.getContext('2d')&nbsp; &nbsp; &nbsp; &nbsp; this.newBallInterval = 1000 // ms between each ball&nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = 0 // timestamp of last time a ball was launched&nbsp; &nbsp; }&nbsp; &nbsp; start() {&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = performance.now()&nbsp; &nbsp; &nbsp; &nbsp; const paddle = new Paddle()&nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(paddle)&nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; }&nbsp; &nbsp; update() {&nbsp; &nbsp; &nbsp; &nbsp; // calculate time elapsed&nbsp; &nbsp; &nbsp; &nbsp; const newTime = performance.now()&nbsp; &nbsp; &nbsp; &nbsp; const deltaTime = newTime - this.lastUpdate&nbsp; &nbsp; &nbsp; &nbsp; // update every entity&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.update(deltaTime))&nbsp; &nbsp; &nbsp; &nbsp; // other update logic (here, create new entities)&nbsp; &nbsp; &nbsp; &nbsp; if(this.lastBallCreated + this.newBallInterval < newTime) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ball = new Ball(100, 300) // this is quick and dirty, you should put some more thought into `x` and `y` here&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(ball)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = newTime&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; // remember current time for next update&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = newTime&nbsp; &nbsp; }&nbsp; &nbsp; draw() {&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.draw(this.context))&nbsp; &nbsp; }&nbsp; &nbsp; cleanup() {&nbsp; &nbsp; &nbsp; &nbsp; // to prevent memory leak, don't forget to cleanup dead entities&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(entity.isDead()) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const index = this.entities.indexOf(entity)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.splice(index, 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }&nbsp; &nbsp; loop() {&nbsp; &nbsp; &nbsp; &nbsp; requestAnimationFrame(() => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.update()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.draw()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.cleanup()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }}const canvas = document.querySelector('canvas')const game = new Game(canvas)game.start()<canvas height="300" width="300"></canvas>管理玩家输入现在假设您要将键盘输入添加到您的游戏中。在那种情况下,我实际上会创建一个单独的类,因为根据您要支持的“按钮”数量,它会很快变得非常复杂。所以首先,让我们画一个基本的桨,这样我们就可以看到发生了什么:class Paddle extends Entity {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; // we just add a default initial x,y and height,width&nbsp; &nbsp; &nbsp; &nbsp; super(150, 20)&nbsp; &nbsp; &nbsp; &nbsp; this.width = 50&nbsp; &nbsp; &nbsp; &nbsp; this.height = 10&nbsp; &nbsp; }&nbsp; &nbsp; update() { /**/ }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; // we just draw a simple rectangle centered on x,y&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead() { return false }}现在我们添加一个基本InputsManager类,您可以根据需要将其复杂化。仅针对两个键,处理keydown和keyup可以同时按下两个键的事实已经有几行代码,因此最好将事情分开,以免弄乱我们的Game对象。class InputsManager {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0 // this is the value we actually need in out Game object&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keydown', this.onKeydown.bind(this))&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keyup', this.onKeyup.bind(this))&nbsp; &nbsp; }&nbsp; &nbsp; onKeydown(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = -1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; onKeyup(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === -1) // make sure the direction was set by this key before resetting it&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === 1) // make sure the direction was set by this key before resetting it&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}现在,我们可以更新我们的Game类来利用这个新的InputsManagerclass Game {&nbsp; &nbsp; // ...&nbsp; &nbsp; start() {&nbsp; &nbsp; &nbsp; &nbsp; // ...&nbsp; &nbsp; &nbsp; &nbsp; this.inputsManager = new InputsManager()&nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; }&nbsp; &nbsp; update() {&nbsp; &nbsp; &nbsp; &nbsp; // update every entity&nbsp; &nbsp; &nbsp; &nbsp; const frameData = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; deltaTime,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inputs: this.inputsManager,&nbsp; &nbsp; &nbsp; &nbsp; } // we now pass more data to the update method so that entities that need to can also read from our InputsManager&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.update(frameData))&nbsp; &nbsp; }&nbsp; &nbsp; // ...}update在更新实体方法的代码以实际使用 new 之后InputsManager,结果如下:class Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; this.x = x&nbsp; &nbsp; &nbsp; &nbsp; this.y = y&nbsp; &nbsp; }&nbsp; &nbsp; update() { console.warn(`${this.constructor.name} needs an update() function`) }&nbsp; &nbsp; draw() { console.warn(`${this.constructor.name} needs a draw() function`) }&nbsp; &nbsp; isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }}class Ball extends Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; super(x, y)&nbsp; &nbsp; &nbsp; &nbsp; this.speed = 300 // px per second&nbsp; &nbsp; &nbsp; &nbsp; this.radius = 10 // radius in px&nbsp; &nbsp; }&nbsp; &nbsp; update({deltaTime}) {&nbsp; &nbsp; // Ball still only needs deltaTime to calculate its update&nbsp; &nbsp; &nbsp; &nbsp; this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000&nbsp; &nbsp; }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead() {&nbsp; &nbsp; &nbsp; &nbsp; return this.y < 0 - this.radius&nbsp; &nbsp; }}class Paddle extends Entity {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; super(150, 50)&nbsp; &nbsp; &nbsp; &nbsp; this.speed = 200&nbsp; &nbsp; &nbsp; &nbsp; this.width = 50&nbsp; &nbsp; &nbsp; &nbsp; this.height = 10&nbsp; &nbsp; }&nbsp; &nbsp; update({deltaTime, inputs}) {&nbsp; &nbsp; // Paddle needs to read both deltaTime and inputs&nbsp; &nbsp; &nbsp; &nbsp; this.x += this.speed * deltaTime / 1000 * inputs.direction&nbsp; &nbsp; }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead() { return false }}class InputsManager {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keydown', this.onKeydown.bind(this))&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keyup', this.onKeyup.bind(this))&nbsp; &nbsp; }&nbsp; &nbsp; onKeydown(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = -1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; onKeyup(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === -1)&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}class Game {&nbsp; &nbsp; /** @param {HTMLCanvasElement} canvas */&nbsp; &nbsp; constructor(canvas) {&nbsp; &nbsp; &nbsp; &nbsp; this.entities = [] // contains all game entities (Balls, Paddles, ...)&nbsp; &nbsp; &nbsp; &nbsp; this.context = canvas.getContext('2d')&nbsp; &nbsp; &nbsp; &nbsp; this.newBallInterval = 500 // ms between each ball&nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = -Infinity // timestamp of last time a ball was launched&nbsp; &nbsp; }&nbsp; &nbsp; start() {&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = performance.now()&nbsp; &nbsp; // we store the new Paddle in this.player so we can read from it later&nbsp; &nbsp; &nbsp; &nbsp; this.player = new Paddle()&nbsp; &nbsp; // but we still add it to the entities list so it gets updated like every other Entity&nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(this.player)&nbsp; &nbsp; &nbsp; &nbsp; this.inputsManager = new InputsManager()&nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; }&nbsp; &nbsp; update() {&nbsp; &nbsp; &nbsp; &nbsp; // calculate time elapsed&nbsp; &nbsp; &nbsp; &nbsp; const newTime = performance.now()&nbsp; &nbsp; &nbsp; &nbsp; const deltaTime = newTime - this.lastUpdate&nbsp; &nbsp; &nbsp; &nbsp; // update every entity&nbsp; &nbsp; &nbsp; &nbsp; const frameData = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; deltaTime,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inputs: this.inputsManager,&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.update(frameData))&nbsp; &nbsp; &nbsp; &nbsp; // other update logic (here, create new entities)&nbsp; &nbsp; &nbsp; &nbsp; if(this.lastBallCreated + this.newBallInterval < newTime) {&nbsp; &nbsp; &nbsp; &nbsp; // we can now read from this.player to the the position of where to fire a Ball&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ball = new Ball(this.player.x, 300)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(ball)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = newTime&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; // remember current time for next update&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = newTime&nbsp; &nbsp; }&nbsp; &nbsp; draw() {&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.draw(this.context))&nbsp; &nbsp; }&nbsp; &nbsp; cleanup() {&nbsp; &nbsp; &nbsp; &nbsp; // to prevent memory leak, don't forget to cleanup dead entities&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(entity.isDead()) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const index = this.entities.indexOf(entity)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.splice(index, 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }&nbsp; &nbsp; loop() {&nbsp; &nbsp; &nbsp; &nbsp; requestAnimationFrame(() => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.update()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.draw()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.cleanup()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }}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 {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'none'&nbsp; &nbsp; &nbsp; &nbsp; this.x = x&nbsp; &nbsp; &nbsp; &nbsp; this.y = y&nbsp; &nbsp; }&nbsp; &nbsp; update() { console.warn(`${this.constructor.name} needs an update() function`) }&nbsp; &nbsp; draw() { console.warn(`${this.constructor.name} needs a draw() function`) }&nbsp; &nbsp; isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }&nbsp; &nbsp; static testCollision(a, b) {&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'none') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.warn(`${a.constructor.name} needs a collision type`)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(b.collision === 'none') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.warn(`${b.constructor.name} needs a collision type`)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'circle' && b.collision === 'circle') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let circle = a.collision === 'circle' ? a : b&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let rect = a.collision === 'rect' ? a : b&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)&nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; }}现在有很多种 2D 碰撞,所以代码有点冗长,但要点是:这是我在这里做出的设计决定。我可以成为通才和未来证明这一点,但它看起来像上面......我必须.collision向我的所有游戏对象添加一个属性,以便它们知道它们是否应该在上述算法中被视为 a'circle'或 ' 。rect'class Ball extends Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; super(x, y)&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'circle'&nbsp; &nbsp; }&nbsp; &nbsp; // ...}class Paddle extends Entity {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; super(150, 50)&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'rect'&nbsp; &nbsp; }&nbsp; &nbsp; // ...}或者我可以极简主义,只添加我需要的东西,在这种情况下,将代码实际放入实体中可能更有意义Paddle:class Paddle extends Entity {&nbsp; &nbsp; testBallCollision(ball) {&nbsp; &nbsp; &nbsp; &nbsp; const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2&nbsp; &nbsp; &nbsp; &nbsp; const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2&nbsp; &nbsp; &nbsp; &nbsp; const ballIsRightOfRectLeftSide = ball.x + ball.radius >= this.x - this.width / 2&nbsp; &nbsp; &nbsp; &nbsp; const ballIsLeftOfRectRightSide = ball.x - ball.radius <= this.x + this.width / 2&nbsp; &nbsp; &nbsp; &nbsp; return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide&nbsp; &nbsp; }}cleanup无论哪种方式,我现在都可以从循环函数Game(我选择放置删除死实体的逻辑的地方)访问碰撞信息。对于我的第一个通才解决方案,我会这样使用它:class Game {&nbsp; &nbsp; cleanup() {&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // I'm passing this.player so all entities can test for collision with the player&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(entity.isDead(this.player)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const index = this.entities.indexOf(entity)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.splice(index, 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }}class Ball extends Entity {&nbsp; &nbsp; isDead(player) {&nbsp; &nbsp; &nbsp; &nbsp; // this is the "out of bounds" test we already had&nbsp; &nbsp; &nbsp; &nbsp; const outOfBounds = this.y < 0 - this.radius&nbsp; &nbsp; &nbsp; &nbsp; // this is the new "collision with player paddle"&nbsp; &nbsp; &nbsp; &nbsp; const collidesWithPlayer = Entity.testCollision(player, this)&nbsp; &nbsp; &nbsp; &nbsp; return outOfBounds || collidesWithPlayer&nbsp; &nbsp; }}使用第二种极简主义方法,我仍然需要通过播放器进行测试:class Game {&nbsp; &nbsp; cleanup() {&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // I'm passing this.player so all entities can test for collision with the player&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(entity.isDead(this.player)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const index = this.entities.indexOf(entity)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.splice(index, 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }}class Ball extends Entity {&nbsp; &nbsp; isDead(player) {&nbsp; &nbsp; &nbsp; &nbsp; // this is the "out of bounds" test we already had&nbsp; &nbsp; &nbsp; &nbsp; const outOfBounds = this.y < 0 - this.radius&nbsp; &nbsp; &nbsp; &nbsp; // this is the new "collision with player paddle"&nbsp; &nbsp; &nbsp; &nbsp; const collidesWithPlayer = player.testBallCollision(this)&nbsp; &nbsp; &nbsp; &nbsp; return outOfBounds || collidesWithPlayer&nbsp; &nbsp; }}最后结果我希望你学到了一些东西。同时,这是这篇很长的回答帖子的最终结果:class Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'none'&nbsp; &nbsp; &nbsp; &nbsp; this.x = x&nbsp; &nbsp; &nbsp; &nbsp; this.y = y&nbsp; &nbsp; }&nbsp; &nbsp; update() { console.warn(`${this.constructor.name} needs an update() function`) }&nbsp; &nbsp; draw() { console.warn(`${this.constructor.name} needs a draw() function`) }&nbsp; &nbsp; isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }&nbsp; &nbsp; static testCollision(a, b) {&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'none') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.warn(`${a.constructor.name} needs a collision type`)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(b.collision === 'none') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.warn(`${b.constructor.name} needs a collision type`)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'circle' && b.collision === 'circle') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let circle = a.collision === 'circle' ? a : b&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let rect = a.collision === 'rect' ? a : b&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)&nbsp; &nbsp; &nbsp; &nbsp; return undefined&nbsp; &nbsp; }}class Ball extends Entity {&nbsp; &nbsp; constructor(x, y) {&nbsp; &nbsp; &nbsp; &nbsp; super(x, y)&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'circle'&nbsp; &nbsp; &nbsp; &nbsp; this.speed = 300 // px per second&nbsp; &nbsp; &nbsp; &nbsp; this.radius = 10 // radius in px&nbsp; &nbsp; }&nbsp; &nbsp; update({deltaTime}) {&nbsp; &nbsp; &nbsp; &nbsp; this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000&nbsp; &nbsp; }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead(player) {&nbsp; &nbsp; &nbsp; &nbsp; const outOfBounds = this.y < 0 - this.radius&nbsp; &nbsp; &nbsp; &nbsp; const collidesWithPlayer = Entity.testCollision(player, this)&nbsp; &nbsp; &nbsp; &nbsp; return outOfBounds || collidesWithPlayer&nbsp; &nbsp; }}class Paddle extends Entity {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; super(150, 50)&nbsp; &nbsp; &nbsp; &nbsp; this.collision = 'rect'&nbsp; &nbsp; &nbsp; &nbsp; this.speed = 200&nbsp; &nbsp; &nbsp; &nbsp; this.width = 50&nbsp; &nbsp; &nbsp; &nbsp; this.height = 10&nbsp; &nbsp; }&nbsp; &nbsp; update({deltaTime, inputs}) {&nbsp; &nbsp; &nbsp; &nbsp; this.x += this.speed * deltaTime / 1000 * inputs.direction&nbsp; &nbsp; }&nbsp; &nbsp; /** @param {CanvasRenderingContext2D} context */&nbsp; &nbsp; draw(context) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; context.beginPath()&nbsp; &nbsp; &nbsp; &nbsp; context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)&nbsp; &nbsp; &nbsp; &nbsp; context.fill()&nbsp; &nbsp; }&nbsp; &nbsp; isDead() { return false }}class InputsManager {&nbsp; &nbsp; constructor() {&nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keydown', this.onKeydown.bind(this))&nbsp; &nbsp; &nbsp; &nbsp; window.addEventListener('keyup', this.onKeyup.bind(this))&nbsp; &nbsp; }&nbsp; &nbsp; onKeydown(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = -1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; onKeyup(event) {&nbsp; &nbsp; &nbsp; &nbsp; switch (event.key) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowLeft':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === -1)&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'ArrowRight':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(this.direction === 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.direction = 0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}class Game {&nbsp; &nbsp; /** @param {HTMLCanvasElement} canvas */&nbsp; &nbsp; constructor(canvas) {&nbsp; &nbsp; &nbsp; &nbsp; this.entities = [] // contains all game entities (Balls, Paddles, ...)&nbsp; &nbsp; &nbsp; &nbsp; this.context = canvas.getContext('2d')&nbsp; &nbsp; &nbsp; &nbsp; this.newBallInterval = 500 // ms between each ball&nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = -Infinity // timestamp of last time a ball was launched&nbsp; &nbsp; }&nbsp; &nbsp; start() {&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = performance.now()&nbsp; &nbsp; &nbsp; &nbsp; this.player = new Paddle()&nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(this.player)&nbsp; &nbsp; &nbsp; &nbsp; this.inputsManager = new InputsManager()&nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; }&nbsp; &nbsp; update() {&nbsp; &nbsp; &nbsp; &nbsp; // calculate time elapsed&nbsp; &nbsp; &nbsp; &nbsp; const newTime = performance.now()&nbsp; &nbsp; &nbsp; &nbsp; const deltaTime = newTime - this.lastUpdate&nbsp; &nbsp; &nbsp; &nbsp; // update every entity&nbsp; &nbsp; &nbsp; &nbsp; const frameData = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; deltaTime,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; inputs: this.inputsManager,&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.update(frameData))&nbsp; &nbsp; &nbsp; &nbsp; // other update logic (here, create new entities)&nbsp; &nbsp; &nbsp; &nbsp; if(this.lastBallCreated + this.newBallInterval < newTime) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const ball = new Ball(this.player.x, 300)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.push(ball)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.lastBallCreated = newTime&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; // remember current time for next update&nbsp; &nbsp; &nbsp; &nbsp; this.lastUpdate = newTime&nbsp; &nbsp; }&nbsp; &nbsp; draw() {&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => entity.draw(this.context))&nbsp; &nbsp; }&nbsp; &nbsp; cleanup() {&nbsp; &nbsp; &nbsp; &nbsp; // to prevent memory leak, don't forget to cleanup dead entities&nbsp; &nbsp; &nbsp; &nbsp; this.entities.forEach(entity => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if(entity.isDead(this.player)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const index = this.entities.indexOf(entity)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.entities.splice(index, 1)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }&nbsp; &nbsp; loop() {&nbsp; &nbsp; &nbsp; &nbsp; requestAnimationFrame(() => {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.update()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.draw()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.cleanup()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.loop()&nbsp; &nbsp; &nbsp; &nbsp; })&nbsp; &nbsp; }}const canvas = document.querySelector('canvas')const game = new Game(canvas)game.start()<canvas height="300" width="300"></canvas><script src="script.js"></script>单击“运行代码片段”后,您必须单击 iframe 以使其聚焦,以便它可以侦听键盘输入(左箭头,右箭头)。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

JavaScript