释放HTML5 Canvas的游戏潜力





5.00/5 (2投票s)
HTML5 浏览器和适用于 Windows 8 metro 风格应用的 HTML5 现在是开发现代游戏的严肃候选者。
HTML5 浏览器和适用于 Windows 8 metro 风格应用的 HTML5 现在是开发现代游戏的严肃候选者。
通过 Canvas,您可以访问一个硬件加速空间,您可以在其中绘制游戏内容,并且通过一些技巧,您将能够实现出色的60 帧每秒渲染。流畅性对于游戏至关重要,因为游戏越流畅,玩家的体验就越好。
本文旨在为您提供一些关于如何充分利用 HTML5 Canvas 的关键。这种效果主要受到我年轻时(80 年代)作为演示制作者编写的一些 Commodore AMIGA 代码的启发。
现在,它只使用了Canvas和JavaScript(而原始代码仅基于 68000 汇编语言)。
<在此处插入 tunnel.zip 包,例如此页面>
完整代码可在以下网址获取:http://www.catuhe.com/msdn/canvas/tunnel.zip
本文的目的是解释如何从给定代码开始,对其进行优化以实现实时性能,而不是解释隧道是如何开发的。
使用离屏 Canvas 读取图像数据
我想谈论的第一个要点是如何使用 Canvas 来帮助您读取图像数据。确实,在每个游戏中,您都需要精灵或背景的图形。Canvas 有一个非常有用的方法来绘制图像:drawImage
。此函数可用于在 Canvas 中绘制精灵,因为您可以定义源矩形和目标矩形。
但有时这还不够。例如,当您想对源图像应用某些效果时,它就不够了。或者当源图像不是简单的位图,而是游戏更复杂的资源时(例如,您需要从中读取数据的地图)。
在这些情况下,您需要访问图像的内部数据。但是Image
标签无法读取其内容。这就是 Canvas 可以帮助您的地方!
确实,每次需要读取图片内容时,您都可以使用离屏 Canvas。这里的核心思想是加载一张图片,当图片加载完成后,您只需将其渲染到一个 Canvas 中(不包含在 DOM 中)。然后,您可以通过读取 Canvas 的像素(这非常简单)来获取源图像的每个像素。
此技术的代码如下(用于 2D 隧道效果读取隧道纹理数据)。
var loadTexture = function (name, then) {
var texture = new Image();
var textureData;
var textureWidth;
var textureHeight;
var result = {};
// on load
texture.addEventListener('load', function () {
var textureCanvas = document.createElement('canvas'); // off-screen canvas
// Setting the canvas to right size
textureCanvas.width = this.width; //<-- "this" is the image
textureCanvas.height = this.height;
result.width = this.width;
result.height = this.height;
var textureContext = textureCanvas.getContext('2d');
textureContext.drawImage(this, 0, 0);
result.data = textureContext.getImageData(0, 0, this.width, this.height).data;
then();
}, false);
// Loading
texture.src = name;
return result;
};
要使用此代码,您必须考虑到纹理的加载是异步的,因此您必须使用then
参数来传递一个函数以继续您的代码。
// Texture
var texture = loadTexture("soft.png", function () {
// Launching the render
QueueNewFrame();
});
使用硬件缩放功能
现代浏览器和 Windows 8 支持硬件加速的 Canvas。这意味着,例如,您可以使用GPU来缩放 Canvas 的内容。
在 2D 隧道效果的情况下,该算法需要处理 Canvas 的每个像素。因此,例如,对于 1024x768 的 Canvas,您需要处理 786432 个像素。为了流畅,您每秒需要执行 60 次,相当于每秒47185920个像素!
任何有助于您减少像素数量的解决方案都将大大提高整体性能,这一点显而易见。
再次,Canvas 有一个解决方案!以下代码向您展示了如何使用硬件加速将 Canvas 的内部工作缓冲区缩放到 DOM 对象的大小。
// Setting hardware scaling
canvas.width = 300;
canvas.style.width = window.innerWidth + 'px';
canvas.height = 200;
canvas.style.height = window.innerHeight + 'px';
值得注意的是 DOM 对象的大小(canvas.style.width
和canvas.style.height
)与 Canvas 工作缓冲区的大小(canvas.width
和canvas.height
)之间的区别。
当这两个尺寸之间存在差异时,硬件会用于缩放工作缓冲区,而在我们的例子中,这是一个很好的事情:我们可以处理较小的分辨率,让GPU将结果缩放到适合 DOM 对象(带有漂亮的免费滤镜来模糊结果)。
在这种情况下,渲染尺寸为 300x200,GPU 会将其缩放到窗口大小。
所有现代浏览器都广泛支持此功能,因此您可以依靠它。
优化您的渲染循环
在编写游戏时,您必须有一个渲染循环,您可以在其中绘制游戏的所有组件(背景、精灵、分数等)。此循环是您代码的骨干,必须进行过度优化,以确保您的游戏快速流畅。
RequestAnimationFrame
HTML 5 引入的一个有趣功能是window
.requestAnimationFrame
函数。而不是使用window
.setInterval
来创建定时器,该定时器每 (1000/16) 毫秒调用您的渲染循环(以实现良好的 60 fps),您可以使用requestAnimationFrame
将此责任委托给浏览器。调用此方法表示您希望尽快由浏览器调用以更新与图形相关的内容。
浏览器会将您的请求包含在其自己的渲染计划中,并与浏览器的渲染和动画代码(CSS、过渡等)同步。此解决方案也很有趣,因为如果窗口未显示(最小化、完全被遮挡等),您的代码将不会被调用。
这有助于提高性能,因为浏览器可以优化并发渲染(例如,如果您的渲染循环太慢),并因此产生更流畅的动画。
代码非常明显(请注意供应商特定前缀的使用)。
var intervalID = -1;
var QueueNewFrame = function () {
if (window.requestAnimationFrame)
window.requestAnimationFrame(renderingLoop);
else if (window.msRequestAnimationFrame)
window.msRequestAnimationFrame(renderingLoop);
else if (window.webkitRequestAnimationFrame)
window.webkitRequestAnimationFrame(renderingLoop);
else if (window.mozRequestAnimationFrame)
window.mozRequestAnimationFrame(renderingLoop);
else if (window.oRequestAnimationFrame)
window.oRequestAnimationFrame(renderingLoop);
else {
QueueNewFrame = function () {
};
intervalID = window.setInterval(renderingLoop, 16.7);
}
};
要使用此函数,您只需在渲染循环结束时调用它以注册下一帧。
var renderingLoop = function () {
...
QueueNewFrame();
};
访问 DOM(文档对象模型)
为了优化您的渲染循环,您必须遵循至少一条黄金法则:不要访问 DOM。即使现代浏览器在这方面已经优化,但对于渲染循环来说,读取 DOM 对象属性仍然太慢。
例如,在我的代码中,我使用了 Internet Explorer 10 性能分析器(可在 F12 开发人员工具栏中使用),结果很明显。
正如您所见,在我的渲染循环中访问 Canvas 的宽度和高度花费了大量时间!
原始代码是
var renderingLoop = function () {
for (var y = -canvas.height / 2; y < canvas.height / 2; y++) {
for (var x = -canvas.width / 2; x < canvas.width / 2; x++) {
...
}
}
};
您可以通过两个先前填充了正确值的变量来删除 canvas.width 和 canvas.height 属性。
var renderingLoop = function () {
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
...
}
}
};
很简单,不是吗?有时可能很难意识到,但相信我,这是值得尝试的!
预计算
根据性能分析器,Math
.atan2
函数有点慢。事实上,此操作不是直接硬编码在 CPU 中,因此 JavaScript 运行时必须添加一些代码来计算结果。
总的来说,如果您可以预先计算一些耗时较长的代码,这总是一个好主意。在这里,在运行我的渲染循环之前,我计算了Math.atan2
的结果。
// precompute arctangent
var atans = [];
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
atans[index++] = Math.atan2(y, x) / Math.PI;
}
}
然后可以在渲染循环中使用atans
数组来显着提高性能。
避免使用 Math.round、Math.floor 和 parseInt
最后一个相关点是parseInt
的使用。
当您使用 Canvas 时,您需要使用整数坐标(x 和 y)来引用像素。确实,您的所有计算都使用浮点数进行,最终您需要将其转换为整数。
JavaScript 提供了Math.round
、Math.floor
甚至parseInt
来将数字转换为整数。但是这些函数会做一些额外的工作(例如,检查范围或检查值是否确实是数字。parseInt
甚至首先将其参数转换为字符串!)。而在我的渲染循环中,我需要一种快速的方法来执行此转换。
回想起我旧的汇编代码,我用了一个小技巧:不要使用parseInt
,您只需要将数字右移 0 位。运行时会将浮点值从浮点寄存器移到整数寄存器并使用硬件转换。用 0 值右移此值将使其保持不变,因此您可以将值转换回整数。
原始代码是
u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u);
而新的代码如下所示:
u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u) >> 0;
当然,此解决方案要求您确定该值是正确的数字。
最终结果
应用所有优化后,您将获得以下报告:
您可以看到现在代码似乎已得到很好的优化,只有基本功能。
从隧道的原始渲染开始(没有任何优化)。
<在此处插入 tunnel.zip 包,例如此页面>
在应用了所有这些优化之后。
<在此处插入 tunnel.zip 包,例如此页面>
我们可以通过以下图表总结每种优化的影响,该图表显示了我自己的计算机上测得的帧率。
进一步
牢记这些要点,您就可以为IE10 等现代浏览器或 Windows 8 的 metro 应用制作实时、快速、流畅的游戏!