如何通过 JavaScript 编写一个游戏主循环

GraigVry 8年前
   <h2>如何通过 JavaScript 编写一个游戏主循环</h2>    <p>“游戏主循环”是一种能够随时间改变状态的用于渲染动画和游戏的技术。它的核心是一个尽可能频繁地运行的方法,来接收用户输入,更新随时间改变的状态,然后绘制当前帧。</p>    <p>在这篇短文中你将了解这些基础技术是如何工作的,并且可以自己制作出基于浏览器的游戏和动画。</p>    <p>JavaScript 中的“游戏主循环”看起来像这样:</p>    <pre>  <code class="language-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)  </code></pre>    <p>requestAnimationFrame 方法请求浏览器在下一次重绘之前尽可能快地调用特定的方法。它是渲染动画专用的 API,但你也可以用 setTimeout 方法设置一个短的超时时间来达到相似的效果。当回调函数开始触发时,requestAnimationFrame 传入一个时间戳作为参数,它包含从窗口加载到现在的毫秒数,等价于 <a href="/misc/goto?guid=4958991886792869773" rel="nofollow,noindex">performance.now()</a> 。</p>    <p>progress 值,或者说每次渲染的时间间隔对于创建流畅的动画是至关重要的。我们通过它来调整 update 方法中的 x 轴和 y 轴的位置,保证动画以稳定的速度运动。</p>    <h3>更新位置</h3>    <p>我们的第一个动画简单到不行。一个红色的方块向右移动直到碰到画布的边缘,然后回到起始位置。</p>    <p>我们需要存储方块的位置,以及在 update 方法中 x 轴位置的增量。当到达边界时我们可以减掉画布的宽度来让它回到起点。</p>    <pre>  <code class="language-javascript">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)  }  </code></pre>    <h3>绘制新一帧</h3>    <p>本例使用 <canvas> 元素来渲染图像,不过游戏主循环也可以结合其他输出,比如 HTML 或者 SVG 来使用。</p>    <p>draw 方法简单地渲染游戏世界的当前状态。每一帧我们都要清空画布,然后在state 对象中保存的位置上重新画一个 10px 的红方块。</p>    <pre>  <code class="language-javascript">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)  }。  </code></pre>    <p>然后我们就发现它动起来了!</p>    <p>在 SitePoint 的 CodePen 可以查看示例: Game Loop in JavaScript: Basic Movement 。</p>    <p>注:在这个例子中你可能会注意到画布的大小是通过 CSS 和 HTML 元素的 width, height 属设置的。CSS 样式设置了画布在页面绘画的真实尺寸,而 HTML 属性则设置了画布 API 需要用到的坐标系或者网格的大小。看看 Stack Overflow 上的这个问题 来了解更多。</p>    <h3>响应用户输入</h3>    <p>下面我们要获取键盘输入来控制对象的位置,state.pressedKeys 会追踪用户按下了哪一个键。</p>    <pre>  <code class="language-javascript">var state = {    x: (width / 2),    y: (height / 2),    pressedKeys: {      left: false,      right: false,      up: false,      down: false    }  }  </code></pre>    <p>我们监听所有的 keydown 和 keyup 事件,并且同步更新 update.pressedKeys。我用 D 键作为向右方向,A 为左,W 为上,S 为下。</p>    <pre>  <code class="language-javascript">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)  </code></pre>    <p>然后我们就只需要根据按下的键来更新 x轴 和 y轴 的值,并保证对象在边界以内。</p>    <pre>  <code class="language-javascript">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    }  }  </code></pre>    <p>现在我们就可以响应用户输入了!</p>    <p>在 SitePoint的 CodePen 可以查看示例: Game Loop in Javascript: Dealing with User Input 。</p>    <h3>行星游戏</h3>    <p>既然现在我们已经掌握了基本原理,那么就可以做些更有意思的事了。</p>    <p>做一艘看起来像经典游戏“ 行星 ”里的飞船其实一点都不复杂。</p>    <p>state 对象需要额外存储一个向量(一个 x、y 对)用来移动,还要保存一个 rotation 值来标记飞船的方向。</p>    <pre>  <code class="language-javascript">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    }  }  </code></pre>    <p>update 方法需要做三件事:</p>    <ul>     <li>根据左右键更新方向(rotation)</li>     <li>根据上下键和方向更新移动向量(movement)</li>     <li>根据移动向量和画布边界更新对象位置(position)</li>    </ul>    <pre>  <code class="language-javascript">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    }  }  </code></pre>    <p>draw 方法在绘制箭头之前会移动并转动画布的原点。</p>    <pre>  <code class="language-javascript">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()  }  </code></pre>    <p>这就是我们需要重建类似“行星”游戏飞船的所有代码。本例的操作按键和前面那个完全一样(D键向右,A 向左,W向上,S 向下)</p>    <p>在 SitePoint的 CodePen 可以查看示例: Game Loop in JavaScript: Recreating Asteroids 。</p>    <p>添加行星、子弹和碰撞监测的工作就交给你了~</p>    <h3> </h3>    <p> </p>    <p>来自:http://web.jobbole.com/89125/</p>    <p> </p>