“游戏循环”是一种用于渲染动画和游戏的技术的名称,该技术会随着时间变化而变化。 它的核心是尽可能多次运行的功能,需要用户输入,在经过的时间中更新状态,然后绘制框架。
在这篇简短的文章中,您将学习这种基本技术的工作原理,并且能够开始制作自己的基于浏览器的游戏和动画。
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元素上的width
和height
属性都设置了画布的大小。 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(带有练习文件)