如何在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 width = 800 
var height = 200
  var state = { 
x: width / 2,
y: height / 2
}
  function update(progress) { 
state.x += progress
  if (state.x > width) { 
state.x -= width
}
}

绘制新框架

此示例使用元素呈现图形,但是游戏循环也可以与其他输出(例如HTML或SVG文档)一起使用。

draw函数只是呈现世界的当前状态。 在每一帧上,我们将清除画布,然后绘制一个10px的红色正方形,其中心位于state对象中存储的位置。

 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) 
}

我们有运动!

注意:在演示中,您可能会注意到CSS以及HTML元素上的widthheight属性都设置了画布的大小。 CSS样式设置将绘制到页面的canvas元素的实际大小,HTML属性设置canvas API将使用的坐标系或“网格”的大小。 有关更多信息,请参见此堆栈溢出问题。

响应用户输入

接下来,我们将获得键盘输入来控制对象的位置, state.pressedKeys将跟踪所按下的键。

 var state = { 
x: (width / 2),
y: (height / 2),
pressedKeys: {
left: false,
right: false,
up: false,
down: false
}
}

让我们听所有的keydown和keyup事件,并相应地更新state.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
}
}

我们有用户输入!

小行星

既然我们掌握了基础知识,我们可以做一些更有趣的事情。

制造像经典游戏《小行星》中所见的船并没有那么复杂。

我们的state需要存储一个附加的矢量(x,y对)以进行运动以及船舶方向的旋转。

 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功能需要更新三件事:

  • 根据左/右按键旋转
  • 基于上/下键和旋转的运动
  • 基于运动矢量和画布边界的位置
 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代表下)。

我将留给您添加小行星,子弹和碰撞检测😉

升级

如果您觉得这篇文章有趣,您将喜欢从零开始观看Mary Rose Cook的现场代码《太空侵略者》,这是一个更复杂的示例,它已有数年历史,但却是在浏览器中构建游戏的绝佳入门。 请享用!

建议

☞编写您的第一个游戏:在Canvas上使用JavaScript制作的Arcade Classic

☞将JavaScript升级到ES6

☞ES6 Javascript:完整的开发人员指南

☞JavaScript的承诺:ES6和AngularJS中的应用

☞学习ECMAScript 6:转向新的JavaScript

☞ES6 Javascript Essentials(带有练习文件)