在游戏中发挥HTML5 Canvas的潜能
参考:http://www.kuqin.com/gamedev/20120505/320360.html
目前, 支持HTML 5的浏览器和Windows 8 Metro成为了游戏开发的候选方案。
利用canvas(画布)可以实现硬件加速,你可以在上面绘制游戏的内容,通过一些技巧可以达到每分钟60帧的渲染速度。
在游戏中,fluidity(流畅性)这一概念非常重要,因为好的流畅性能带给玩家更好的体验。
这篇文章的主旨在于教你一些技巧,让你可以最大程度地发挥HTML5 canvas的潜能。
我将通过一个例子来表达我要讲的内容。这个例子是2D tunnel effect(2D隧道效应),它是我为Coding4Fun 会议写的(我参加了在法国举办的TechDays 2012)。
(http://video.fr.msn.com/watch/video/techdays-2012-session-technique-coding4fun/zqy7cm8l).
早在80年代,当我还是一个年轻的demomaker的时候曾受到一些Commodore AMIGA代码的启发,从而写了这个2D tunnel effect。
经过不断地改进,现在它仅使用了canvas和Javascript(最初的代码是基于68000汇编的):
完整的代码可以从这里获得:http://www.catuhe.com/msdn/canvas/tunnel.zip
这篇文章的目的不是讲解该程序的开发过程,而是让你通过优化已有的代码来达到一个实时性的效果。
使用off-screen canvas(离屏画布)来读取图片数据
我想讲的第一点是怎样使用canvas来读取图片数据。实际上,任何一个游戏都需要图形来显示游戏界面和背景。canvas有一个非常有用的画图方法:drawImage 。这个功能可以用来绘制游戏界面,通过它你可以定义起始和目的区域。
但是有时候光使用它是不够的,比如你想要在源图像上实现一些特效,或者源图像不是一个简单的位图而是一个复杂的资源(如地图)。
在这些情况下,你需要访问到图片的内部数据。但是Image标签无法读到这些数据,这时候就该canvas上场了。
事实上,每当你需要从图片中读取内容的时候,你都可以使用off-screen canvas。也就是说,当你导入一张图片的时候,你只需要将它渲染到canvas中(而不是DOM里),然后你就可以通过读取canvas的像素点来获得源图片的内容了(这个过程非常简单)。
有关部分的代码如下(2D tunnel effect中用来读取隧道的纹理数据的):
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();
});
使用硬件缩放功能
现代浏览器和Windows8都支持硬件加速的canvas,这意味着,你可以使用GPU来调整canvas里面内容的尺寸。
在2D tunnel effect里,该算法要求处理canvas的每一个像素点,因此一个1024×768的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对象的大小(实现一个漂亮免费的模糊滤镜效果)。
在这种情况下,本来只有300×200 的图像会被GPU扩展到跟你的窗口一样大。
所有的现代浏览器都支持该功能,因此你可以放心的使用。
优化rendering loop
制作游戏的时候,需要一个rendering loop用来绘制所有的组件(如背景,界面,分数等等)。这个loop是代码的核心,因此必须充分优化从而保证游戏的快速和流畅。
RequestAnimationFrame
HTML5一个有趣的功能是使用window.requestAnimationFrame. 代替window.setInterval 来创建定时器,从而实现每(1000/16) 毫秒渲染一次(以达到60fps),你可以通过requestAnimationFrame将该任务交给浏览器。调用这个方法表明你想要尽快的更新有关的图形。
浏览器会将你的请求放入内部渲染计划,并使之与其本身的渲染及动画代码(CSS, transitions等等)同步。这个方法另一个有趣的地方在于如果窗口不显示(minimized, fully occluded等等),你的代码就不会被调用。
这能改善性能,因为浏览器可以优化并发渲染从而提高动画的流畅性(如你的渲染周期太长的话浏览器会将它与其本身的渲染及动画周期同步)。
代码很清晰(别忘了window前缀):
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);
}
};
你只需要在rendering loop的结尾调用这个函数并在接下来的代码段里进行注册即可:
var renderingLoop = function () {
…
QueueNewFrame();
};
访问DOM(Document Object Model)
为了优化rendering loop,你必须遵循这条黄金准则:DO NOT ACCESS THE DOM(不要访问DOM)。即使现代浏览器在这方面做了优化,读取DOM对象属性还是太慢了。
例如,在我的代码里,我使用了Internet Explorer 10 profiler(IE10的提供的分析器,按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函数比较慢。事实上,该操作并不需要在运行时计算,你可以在JavaScript里面加一些代码预先计算出结果。
一般来说,预先计算一些较为费时的代码是一种好方法。这里,在运行rendering loop之前,我已经计算好了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时,你需要使用一些整数坐标。实际上,所有的计算都采用的浮点数,你需要将它们转换成整形。
JavaScript 提供了 Math.round, Math.floor 甚至 parseInt 来转换数值。但是这个方法做了一些额外的工作(比如检测数据是不是有效的数值,parseInt 甚至先将参数转换成了字符串!)。在我的rendering loop里面,我需要一个更快的转换方法。
在我的旧的汇编代码里面,我使用了一个小技巧:将数据右移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;
当然该方法要求你的数据是合法的数值。
最终结果
实现了上述优化之后得到的结果如下:
你可以看到做了这些基本的功能优化之后的表现。
原始的隧道渲染(没做任何优化):
做完上述优化之后:
下表展示了每项优化对帧速率的影响(在我的机器上):
更进一步
记住这些关键技巧,你就可以为现代浏览器或Windows8制作实时、快速、流畅的游戏了。