如何通過 JavaScript 編寫一個遊戲主迴圈
“遊戲主迴圈”是一種能夠隨時間改變狀態的用於渲染動畫和遊戲的技術。它的核心是一個儘可能頻繁地執行的方法,來接收使用者輸入,更新隨時間改變的狀態,然後繪製當前幀。
在這篇短文中你將瞭解這些基礎技術是如何工作的,並且可以自己製作出基於瀏覽器的遊戲和動畫。
JavaScript 中的“遊戲主迴圈”看起來像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function update(progress) { // Update the state of the world for the elapsed time since last render } function draw() { // Draw the state of the world } function loop(timestamp) { var progress = timestamp - lastRender update(progress) draw() lastRender = timestamp window.requestAnimationFrame(loop) } var lastRender = 0 window.requestAnimationFrame(loop) |
requestAnimationFrame 方法請求瀏覽器在下一次重繪之前儘可能快地呼叫特定的方法。它是渲染動畫專用的 API,但你也可以用 setTimeout 方法設定一個短的超時時間來達到相似的效果。當回撥函式開始觸發時,requestAnimationFrame 傳入一個時間戳作為引數,它包含從視窗載入到現在的毫秒數,等價於 performance.now()。
progress 值,或者說每次渲染的時間間隔對於建立流暢的動畫是至關重要的。我們通過它來調整 update 方法中的 x 軸和 y 軸的位置,保證動畫以穩定的速度運動。
更新位置
我們的第一個動畫簡單到不行。一個紅色的方塊向右移動直到碰到畫布的邊緣,然後回到起始位置。
我們需要儲存方塊的位置,以及在 update 方法中 x 軸位置的增量。當到達邊界時我們可以減掉畫布的寬度來讓它回到起點。
1 2 3 4 5 6 7 8 9 10 11 |
var canvas = document.getElementById("canvas") var width = canvas.width var height = canvas.height var ctx = canvas.getContext("2d") ctx.fillStyle = "red" function draw() { ctx.clearRect(0, 0, width, height) ctx.fillRect(state.x - 5, state.y - 5, 10, 10) } |
繪製新一幀
本例使用 <canvas> 元素來渲染影像,不過遊戲主迴圈也可以結合其他輸出,比如 HTML 或者 SVG 來使用。
draw 方法簡單地渲染遊戲世界的當前狀態。每一幀我們都要清空畫布,然後在state 物件中儲存的位置上重新畫一個 10px 的紅方塊。
1 2 3 4 5 6 7 8 9 10 11 |
var canvas = document.getElementById("canvas") var width = canvas.width var height = canvas.height var ctx = canvas.getContext("2d") ctx.fillStyle = "red" function draw() { ctx.clearRect(0, 0, width, height) ctx.fillRect(state.x - 5, state.y - 5, 10, 10) }。 |
然後我們就發現它動起來了!
在 SitePoint 的 CodePen 可以檢視示例:Game Loop in JavaScript: Basic Movement。
注:在這個例子中你可能會注意到畫布的大小是通過 CSS 和 HTML 元素的 width, height 屬設定的。CSS 樣式設定了畫布在頁面繪畫的真實尺寸,而 HTML 屬性則設定了畫布 API 需要用到的座標系或者網格的大小。看看 Stack Overflow 上的這個問題來了解更多。
響應使用者輸入
下面我們要獲取鍵盤輸入來控制物件的位置,state.pressedKeys 會追蹤使用者按下了哪一個鍵。
1 2 3 4 5 6 7 8 9 10 |
var state = { x: (width / 2), y: (height / 2), pressedKeys: { left: false, right: false, up: false, down: false } } |
我們監聽所有的 keydown 和 keyup 事件,並且同步更新 update.pressedKeys。我用 D 鍵作為向右方向,A 為左,W 為上,S 為下。你可以在這裡找到鍵盤碼列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var keyMap = { 68: 'right', 65: 'left', 87: 'up', 83: 'down' } function keydown(event) { var key = keyMap[event.keyCode] state.pressedKeys[key] = true } function keyup(event) { var key = keyMap[event.keyCode] state.pressedKeys[key] = false } window.addEventListener("keydown", keydown, false) window.addEventListener("keyup", keyup, false) |
然後我們就只需要根據按下的鍵來更新 x軸 和 y軸 的值,並保證物件在邊界以內。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function update(progress) { if (state.pressedKeys.left) { state.x -= progress } if (state.pressedKeys.right) { state.x += progress } if (state.pressedKeys.up) { state.y -= progress } if (state.pressedKeys.down) { state.y += progress } // Flip position at boundaries if (state.x > width) { state.x -= width } else if (state.x < 0) { state.x += width } if (state.y > height) { state.y -= height } else if (state.y < 0) { state.y += height } } |
現在我們就可以響應使用者輸入了!
在 SitePoint的 CodePen 可以檢視示例:Game Loop in Javascript: Dealing with User Input。
行星遊戲
既然現在我們已經掌握了基本原理,那麼就可以做些更有意思的事了。
做一艘看起來像經典遊戲“行星”裡的飛船其實一點都不復雜。
state 物件需要額外儲存一個向量(一個 x、y 對)用來移動,還要儲存一個 rotation 值來標記飛船的方向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var state = { position: { x: (width / 2), y: (height / 2) }, movement: { x: 0, y: 0 }, rotation: 0, pressedKeys: { left: false, right: false, up: false, down: false } } |
update 方法需要做三件事:
- 根據左右鍵更新方向(rotation)
- 根據上下鍵和方向更新移動向量(movement)
- 根據移動向量和畫布邊界更新物件位置(position)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
function update(progress) { // Make a smaller time value that's easier to work with var p = progress / 16 updateRotation(p) updateMovement(p) updatePosition(p) } function updateRotation(p) { if (state.pressedKeys.left) { state.rotation -= p * 5 } else if (state.pressedKeys.right) { state.rotation += p * 5 } } function updateMovement(p) { // Behold! Mathematics for mapping a rotation to it's x, y components var accelerationVector = { x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)), y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180)) } if (state.pressedKeys.up) { state.movement.x += accelerationVector.x state.movement.y += accelerationVector.y } else if (state.pressedKeys.down) { state.movement.x -= accelerationVector.x state.movement.y -= accelerationVector.y } // Limit movement speed if (state.movement.x > 40) { state.movement.x = 40 } else if (state.movement.x < -40) { state.movement.x = -40 } if (state.movement.y > 40) { state.movement.y = 40 } else if (state.movement.y < -40) { state.movement.y = -40 } } function updatePosition(p) { state.position.x += state.movement.x state.position.y += state.movement.y // Detect boundaries if (state.position.x > width) { state.position.x -= width } else if (state.position.x < 0) { state.position.x += width } if (state.position.y > height) { state.position.y -= height } else if (state.position.y < 0) { state.position.y += height } } |
draw 方法在繪製箭頭之前會移動並轉動畫布的原點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function draw() { ctx.clearRect(0, 0, width, height) ctx.save() ctx.translate(state.position.x, state.position.y) ctx.rotate((Math.PI/180) * state.rotation) ctx.strokeStyle = 'white' ctx.lineWidth = 2 ctx.beginPath () ctx.moveTo(0, 0) ctx.lineTo(10, 10) ctx.lineTo(0, -20) ctx.lineTo(-10, 10) ctx.lineTo(0, 0) ctx.closePath() ctx.stroke() ctx.restore() } |
這就是我們需要重建類似“行星”遊戲飛船的所有程式碼。本例的操作按鍵和前面那個完全一樣(D鍵向右,A 向左,W向上,S 向下)
在 SitePoint的 CodePen 可以檢視示例:Game Loop in JavaScript: Recreating Asteroids。
新增行星、子彈和碰撞監測的工作就交給你了~
升級
如果你對本文很感興趣,那你肯定會喜歡閱讀這篇《Mary Rose Cook live-code Space Invaders form scrach》來看一個更復雜的例子。雖然是發表於幾年前,但它是一篇介紹開發瀏覽器遊戲的非常棒的文章。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式