如何通過 JavaScript 編寫一個游戲主循環

GraigVry 8年前發布 | 8K 次閱讀 游戲開發 JavaScript開發 JavaScript

如何通過 JavaScript 編寫一個游戲主循環

“游戲主循環”是一種能夠隨時間改變狀態的用于渲染動畫和游戲的技術。它的核心是一個盡可能頻繁地運行的方法,來接收用戶輸入,更新隨時間改變的狀態,然后繪制當前幀。

在這篇短文中你將了解這些基礎技術是如何工作的,并且可以自己制作出基于瀏覽器的游戲和動畫。

JavaScript 中的“游戲主循環”看起來像這樣:

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 軸位置的增量。當到達邊界時我們可以減掉畫布的寬度來讓它回到起點。

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 的紅方塊。

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 會追蹤用戶按下了哪一個鍵。

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 為下。

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軸 的值,并保證對象在邊界以內。

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 值來標記飛船的方向。

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)
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 方法在繪制箭頭之前會移動并轉動畫布的原點。

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 。

添加行星、子彈和碰撞監測的工作就交給你了~

 

 

來自:http://web.jobbole.com/89125/

 

 本文由用戶 GraigVry 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!