我遇到的问题是,在开发HTML5画布应用程序时,需要使用大量变换(如平移、旋转、缩放),因此需要调用许多context.save()和context.restore()方法。即使只绘制很少的内容,性能也会快速下降(因为在循环中尽可能多地调用save()和restore()方法)。是否有替代方法可以使用这些转换但仍然能够提高性能?谢谢!
我遇到的问题是,在开发HTML5画布应用程序时,需要使用大量变换(如平移、旋转、缩放),因此需要调用许多context.save()和context.restore()方法。即使只绘制很少的内容,性能也会快速下降(因为在循环中尽可能多地调用save()和restore()方法)。是否有替代方法可以使用这些转换但仍然能够提高性能?谢谢!
避免保存和还原状态
使用setTransform可以避免需要进行保存和还原状态。
有很多原因会导致保存和还原状态会导致速度变慢,这取决于当前的GPU和2D上下文状态。如果您将当前的填充和/或描边样式设置为大型图案,或者使用复杂的字体/渐变,或者使用过滤器(如果可用),那么保存和还原过程可能比呈现图像需要更长时间。
当编写动画和游戏时,性能是一切,对我来说,它与精灵计数有关。每帧(60分之一秒)我可以绘制的精灵越多,我就可以添加更多的特效,环境越详细,游戏就越好。
我让状态保持开放式,即我不保持对当前2D上下文状态的详细跟踪。这样我就永远不必使用保存和还原状态。
ctx.setTransform而不是ctx.transform
由于变换函数transform、rotate、scale、translate会使当前变换矩阵相乘,它们很少被使用,因为我不知道变换状态是什么。
为了处理未知状态,我使用setTransform来完全替换当前的变换矩阵。这也允许我在一次调用中设置比例和平移,而无需知道当前状态是什么。
ctx.setTransform(scaleX,0,0,scaleY,posX,posY); // scale and translate in one call
我也可以添加旋转功能,但是用于查找x、y轴向量(setTransform中的前4个数字)的JavaScript代码比旋转慢。
精灵和其渲染
下面是一个扩展的精灵函数。它从精灵表绘制精灵,精灵具有x和y缩放、位置和中心,并且我总是使用alpha来设置透明度。
// image is the image. Must have an array of sprites
// image.sprites = [{x:0,y:0,w:10,h:10},{x:20,y:0,w:30,h:40},....]
// where the position and size of each sprite is kept
// spriteInd is the index of the sprite
// x,y position on sprite center
// cx,cy location of sprite center (I also have that in the sprite list for some situations)
// sx,sy x and y scales
// r rotation in radians
// a alpha value
function drawSprite(image, spriteInd, x, y, cx, cy, sx, sy, r, a){
var spr = image.sprites[spriteInd];
var w = spr.w;
var h = spr.h;
ctx.setTransform(sx,0,0,sy,x,y); // set scale and position
ctx.rotate(r);
ctx.globalAlpha = a;
ctx.drawImage(image,spr.x,spr.y,w,h,-cx,-cy,w,h); // render the subimage
}
在一般的计算机上,使用该函数可以以完整帧速率渲染1000+精灵。在Firefox(撰写时)中,我使用该函数获得了2000+(精灵是从1024 x 2048精灵表中随机选择的),最大精灵尺寸为256 x 256。function drawSprite(image, spriteInd, x, y, a){
var spr = image.sprites[spriteInd];
var w = spr.w;
var h = spr.h;
ctx.setTransform(1,0,0,1,x,y); // set scale and position
ctx.globalAlpha = a;
ctx.drawImage(image,spr.x,spr.y,w,h,0,0,w,h); // render the subimage
}
或者最简单的玩法精灵、粒子、子弹等。
function drawSprite(image, spriteInd, x, y,s,r,a){
var spr = image.sprites[spriteInd];
var w = spr.w;
var h = spr.h;
ctx.setTransform(s,0,0,s,x,y); // set scale and position
ctx.rotate(r);
ctx.globalAlpha = a;
ctx.drawImage(image,spr.x,spr.y,w,h,-w/2,-h/2,w,h); // render the subimage
}
如果它是一个背景图片
function drawSprite(image){
var s = Math.max(image.width / canvasWidth, image.height / canvasHeight); // canvasWidth and height are globals
ctx.setTransform(s,0,0,s,0,0); // set scale and position
ctx.globalAlpha = 1;
ctx.drawImage(image,0,0); // render the subimage
}
常见的情况是游戏场地可以进行缩放、平移和旋转。为此,我维护了一个闭包变换状态(上面的所有全局变量都是封闭的变量,并且是渲染对象的一部分)。
// all coords are relative to the global transfrom
function drawGlobalSprite(image, spriteInd, x, y, cx, cy, sx, sy, r, a){
var spr = image.sprites[spriteInd];
var w = spr.w;
var h = spr.h;
// m1 to m6 are the global transform
ctx.setTransform(m1,m2,m3,m4,m5,m6); // set playfield
ctx.transform(sx,0,0,sy,x,y); // set scale and position
ctx.rotate(r);
ctx.globalAlpha = a * globalAlpha; (a real global alpha)
ctx.drawImage(image,spr.x,spr.y,w,h,-cx,-cy,w,h); // render the subimage
}
以上所有内容都是关于实际游戏精灵渲染速度最快的技术。
一般提示
永远不要使用任何矢量类型的渲染方法(除非您有多余的帧时间),例如填充,描边,filltext,arc,rect,moveTo,lineTo,因为它们会立即减慢速度。如果需要呈现文本,请创建一个离屏画布,渲染一次,然后显示为精灵或图像。
图像尺寸和GPU RAM
创建内容时,始终使用图像尺寸的幂规则。GPU处理大小为2的幂的图像。 (2,4,8,16,32,64,128....)因此,宽度和高度必须是2的幂。即1024 x 512或2048 x 128是很好的大小。
如果您不使用这些大小,2D上下文并不在意,它所做的是将图像扩展到最接近的幂。因此,如果我有一个300x300的图像,为了适应GPU,图像必须扩展到最接近的幂,即512x512。因此,实际内存占用比您能够显示的像素大2.5倍以上。当GPU的本地内存用尽时,它将开始从主板RAM中切换内存,当这种情况发生时,帧速率会下降到无法使用。
确保您调整图像大小以便不浪费RAM,这意味着在撞到RAM壁之前,您可以将更多内容装入游戏中(对于较小的设备而言,RAM墙并不多)。
GC是主要的帧窃取者
最后一项优化是确保GC(垃圾收集器)几乎没有任何工作要做。在主循环中,避免使用new(重复使用对象而不是dereference它并创建另一个),避免从数组中推送和弹出(保持其长度不下降),保持活动项目的单独计数。创建自定义迭代器和推送函数,这些函数具有项目上下文感知性(知道数组项是否活动)。当您推送时,除非没有非活动项,否则不会推送新项,当项目变为非活动状态时,将其留在数组中,并稍后使用它(如果需要)。
有一个简单的策略,我称之为快速堆栈,超出了此答案的范围,但可以处理1000多个短暂的游戏对象,而不会产生任何GC负载。一些更好的游戏引擎使用类似的方法(池化数组提供非活动项目的池)。
GC应该不到您的游戏活动的5%,如果没有,请查找您在哪里不必要地创建和dereferencing。
ctx.drawSprite()
方法是什么?这应该是 ctx.drawImage()
吗? - nnnnnn
ctx.setTransform(1,0,0,1,0,0)
。请记住,每次调用ctx.save()
都会将所有上下文属性堆叠起来,并且只有在调用ctx.restore()
时才会释放它们,因此请确保对于每个save()
,都有一个相应的restore()
被调用。 - Kaiido