游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

如何用HTML5 Canvas制作子画面动画

发布时间:2013-08-08 13:52:41 Tags:,,,

作者:Martin Wells

子面画基本原理

我一直很喜欢网页游戏,因为大多数都容易制作,而且容易玩(只要点击一个链接就可以开始玩了)。

Ajax和移动DOM元素是有些意思,但制约了你能制作的游戏类型。对于游戏开发者,技术不仅一直在变化,而且是飞速变化。HTML5为网页游戏开发不断地提供大量新选择,浏览器供应商也为成为新标准的最佳平台而展开激烈竞争。

sprite-animations(from webappers.com)

sprite-animations(from webappers.com)

所以,从游戏开发者的角度看,一切都朝着正确的方向发展:2D和3D硬件运算速度越来越快、javascript引擎的表现性能越来越好、排错和分析工具高度集成,以及可能最重要的,浏览器供应商正在积极地角逐最佳网页游戏平台。

所以工具实用了,浏览器强大了,供应商重视了,我们就可以制作出优秀的游戏了,对吧?基本上。

HTML5/Javascript游戏开发仍然处于发展初期,会遇到许多误区和技术选择。

在本文中,我将介绍一些开发2D游戏的选择,但愿能让读者对开发HTML5游戏有所了解。

基础

你要回答的第一个问题是,是使用HTML5 Canvas来绘制图像(场景图像)还是通过修改DOM元素。

为了用DOM做2D游戏,你基本上要动态地调整元素风格,以便在页面上移动它。虽然有些时候DOM修改是很好的,但这一次我将重点介绍使用HTML5 Canvas来制作图像,因为对于现代浏览器,它是最灵活的。

页面设置

首先,你要创建一个HTML页面,其中包含如下canvas标签:

<!doctype html>
<html>
<head>
<title></title>
</head>
<body style=’position: absolute; padding:0; margin:0; height: 100%; width:100%’>
<canvas id=”gameCanvas”></canvas>
</body>
</html>

如果你载入以上代码,当然什么也不会出现。那是因为虽然我们有一个canvas标签,但我们还没在上面绘制任何东西。我们来添加一些简单的canvas命令来绘制小箱子吧。

<head>
<title></title>
<script type=’text/javascript’>
var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
var ctx = canvas.getContext(“2d”);
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = ‘#333333′;
ctx.fillRect(canvas.width / 3, canvas.height / 3, canvas.width / 3,
canvas.height / 3);
}
</script>
</head>
<body onload=’onload()’ …

在这个例子中,我已经在body标签中添加了一个onload事件,然后执行功能获得画布元素,并绘制几个箱子。非常简单。

result 1(from webappers.com)

result 1(from webappers.com)

这个箱子不错,但你会注意到,画布没有铺满整个浏览器窗口。为了解决这个问题,我们可以增加画布的宽度和高度。我是指根据画布所包含的文件元素的大小来灵活地调整画布尺寸。

var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight;

加载后,你会看到画布铺满整个屏幕了。太好了。

再进一步,如果浏览器窗口大小是由用户调整的,我们还要重置画布的尺寸。

var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
resize();
}
function resize() {
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight;
var ctx = canvas.getContext(“2d”);
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = ‘#333333′;
ctx.fillRect(canvas.width/3, canvas.height/3, canvas.width/3, canvas.height/3);
}

添加onresize命令到body标签。

<body onresize=’resize()’ …

现在,如果你调整浏览器的大小,矩形应该如下图所示。

result 2(from webappers.com)

result 2(from webappers.com)

载入图像

大部分游戏都需要动画的子画面,所以我来添加一些图像吧。

首先,你需要图像资源。因为我们要用javascript绘制它,所以我觉得先声明图像然后设置它的src属性为你想载入的图像的URL,比较合理。

var img = null;
function onload() {

img = new Image();
img.src = ‘simba.png’;
}

然后你可以通过添加这个到resize方法中来绘制图像:

ctx.drawImage(img, canvas.width/2 – (img.width/2), canvas.height/2 – (img.height/2));

如果你重新载入页面后,在大部分情况下,你会看到图像出现了。不过我说的是大部分情况下,因为这取决于你的机器跑得有多快、浏览器是否已经缓存了图像。那是因为resize方法的调用时间介于你开始载入图像(设置它的src属性)的时间到浏览器准备好的时间之间。对于一两张图像,这个方法可能不错,但当你的游戏开始变大时,你就必须等到所有图像加载完才能执行活动。

给图像添加一个通知监听器,这样当图像准备就绪时你就会收到回叫信号。我得重新整理一下,以下是更新过的代码:

var canvas = null;
var img = null; var ctx = null;
var imageReady = false;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
ctx = canvas.getContext(“2d”);
img = new Image();
img.src = ‘images/simba.png’;
img.onload = loaded();
resize();
}
function loaded() {
imageReady = true; redraw();
}
function resize() {
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight; redraw();
}
function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
ctx.drawImage(img, canvas.width/2 – (img.width/2), canvas.height/2 – (img.height/2));
}

结果应该是:

result 3(from webappers)

result 3(from webappers)

这个图像显示了一只吸血鬼猫(好吧,是我自己觉得像)的6个奔跑帧。为了把这个子画面做成动画,我们必须每次绘制一个帧。

子画面动画

你可以用drawImage命令的源参数绘制一个帧。事实上,是只绘制源图像的一部分。所以为了绘制这唯一的第一帧,使用允许你指定源图像中的矩形的drawImage的拓展版。因为我们的猫动画是由6个96 x 96象素大小的帧组成的,我们可以添加:

ctx.drawImage(img, 0, 0, 96, 54, canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);

这里的关键是起点(0, 0, 96, 54)。这限制被绘制图像为猫动画的第一帧。我还设置根据单帧来居中,而不是包含所有6帧的整个图像尺寸。

现在总算有点意思了。为了让图像动起来,我们必须追踪要绘制的帧,然后随着时间推进帧数。为此,我们必须把静止页面做成隔时循环的页面。

我们按照老方法来做。添加60帧每秒间隔计时器。为了保证只有图像加载后才开始循环动画,我们要在loaded功能中添加以下命令:

function loaded() {
imageReady = true;
setTimeout( update, 1000 / 60 );
}

添加更新后的函数,然后调用redraw:

var frame = 0;
function update() {
redraw(); frame++;
if (frame >= 6) frame = 0;
setTimeout( update, 1000 / 60 );
}

当绘制后且帧推进完,计时器就会重置。

下一步,调整绘制图像,使源窗口根据我们想要绘制的那一帧位置来移动(关键是给帧设置的源X位置,是帧乘上帧的大小)。

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
}

结果如下:

result 4(from webappers)

result 4(from webappers)

我们邪恶的不死吸血猫活了!跑得太快了。

我们还要对动画做一些改进。

requestAnimFrame

setTimeout很好,几乎在所有浏览器上都运行得不错,但还有一个更好的方法,那就是requestAnimFrame。

requestAnimFrame的作用基本上就是setTimeout,但浏览器知道你正在渲染帧,所以它可以优化绘制循环,以及如何与剩下的页面回流。它甚至会检测标签是否可见,如果隐藏就不绘制,这样就节省了电池(是的,以60fps的速率循环的网页游戏是很烧电池的)。另外,浏览器还有机会以其他我们不知道的方式进行优化。根据我对更高级的帧加载的经验,这样可以大大提高表现,特别是在现在的浏览器中。

我要给读者提个醒,在某些情况下,setTimeout比requestAnimFrame更好用,特别是对于手机。测试一下,根据设备配置一下你的应用。

在不同的浏览器上调用requestAnimFrame的情况也不同,标准的检测方法如下:

window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();

如果requestAnimFrame支持不可用,还是可以用回内置的setTimeout。

然后你必须修改update方法,以便重复获得请求:

function update() {
requestAnimFrame(update);
redraw();
frame++;
if (frame >= 6) frame = 0;
}

在渲染/更新以前调用requestAnimFrame,往往能获得更连贯的效果。

另外,当我第一次使用requestAnimFrame时,我试图查找它如何计时的资料,但什么也没找到。那是因为它本来就是不能计时的。setTimeout没有什么与设置MS延时相当的东西,这意味着你不可能控制帧率。那你就做好你该做的事,其他的就让浏览器去处理吧。

另一件要注意的事是,如果你封闭使用requestAnimFrame,那么你必须做一个本地交换来调用它,如:

my.requestAnimFrame = (function () {
var func = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function (callback, element)
{
window.setTimeout(callback, 1000 / this.fps);
};
// apply to our window global to avoid illegal invocations (it’s a native) return function (callback, element) { func.apply(window, [callback, element]);
};
})();

基于时间的动画

接下来我们要设置一下猫的奔跑速度。现在,动画帧根据帧率播放,不同的设备情况有所不同。那就不妙了,因为如果角色移动的同时又有动画,结果就会看起很来很怪很不协调。你可以试一下控制帧率,但根据真正的定时做出的动画从各方面看都更好些。

你还会发现,游戏中的定时通常运用于你所做的一切东西:燃烧率、转弯速度、加速、跳跃,使用合适的定时,都会有更好的效果。

为了让猫以规定的速度奔跑,我们必须追踪已经经过的时间,然后根据分配给每帧的时间播放帧。基本步骤是:

1、按每秒几帧设置动画速度(msPerFrame)。

2、当你循环游戏时,计算一下自最后一帧以后已经经过了多少时间(delta)。

3、如果已经经过的时间足够把动画帧播完,那么播放这一帧并设置累积delta为0。

4、如果已经经过的时间不够,那么记住(累积)delta时间(acDelta)。

以下是代码:

var frame = 0;
var lastUpdateTime = 0;
var acDelta = 0;
var msPerFrame = 100;
function update() {
requestAnimFrame(update);
var delta = Date.now() – lastUpdateTime;
if (acDelta > msPerFrame)
{
acDelta = 0;
redraw();
frame++; if
(frame >= 6) frame = 0;
} else {

acDelta += delta;
}
lastUpdateTime = Date.now();
}

载入后,小猫的移动速度会更合理一些。

result 5(from webappers)

result 5(from webappers)

缩放和旋转

当图像渲染后,你还是可以使用这个2D画布来执行各种操作,如旋转和缩放。

例如,把图像缩小一半。你可以通过添加ctx.scale(0.5, 0.5)来达到效果:

function redraw()
{
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
{
ctx.save();
ctx.scale(0.5,0.5);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
ctx.restore();
}
}

result 6(from webappers)

result 6(from webappers)

你会发现我还在缩放命令前添加了ctx.save(),以及在最后添加了ctx.restore()。没有这个,缩放命令就会累积,而可怜的小猫就会很快缩小到看不见(试一下,很有意思)。

使用负值还可以达到颠倒图像的效果。如果你把缩放值从(0.5, 0.5)变成(-1, 1),那么猫图像就会水平翻转,这样它就会往相反的方向跑。注意,这个转变是用翻转起点X位置达到反转图像的效果。

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) { ctx.save();
ctx.translate(img.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
ctx.restore();
}
}

你可以尝试一下。以下是猫爬墙的动画(其实是竖直旋转了动画):

ctx.rotate( 270*Math.PI/180 );
ctx.drawImage(img, frame*96, 0, 96, 54,
-(canvas.width/2 – 48), (canvas.height/2 – 48), 96, 54);

在这个例子中,通过旋转内容,不只是图像旋转了,连坐标也旋转了,所以drawImage命令通过反转猫绘制的X位置来抵消这个。

result 7(from webappers)

result 7(from webappers)

真是一只天才的猫(不过吸血鬼本来就能爬墙)。

缩放和旋转效果很好。好是好,但它也很慢,会对渲染表现产生重大影响。在制作游戏时,还有另一个技巧——预渲染,可以解决这个问题以及你可能遇到了大量其他渲染表现问题。

预渲染

预渲染就是提前处理图像。你只做一次昂贵的渲染操作,然后循环使用已渲染好的结果。

在HTML5中,你必须在分开的不可见画布上绘制,然后不是绘制图像,而是把其他画布绘制在图像的位置上。

以下是预渲染猫的代码例子:

var reverseCanvas = null;
function prerender() {
reverseCanvas = document.createElement(‘canvas’);
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext(“2d”);
rctx.save(); rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
rctx.restore();
}

注意,画面对象是创建的,不是添加到文件占的,所以它不会显示出来。高度和宽度设置到原来的子画面表格中,然后原图像会使用渲染器的2D环境绘制图像。

为了设置预渲染,你可以从loaded功能中调用它。

function loaded() {
imageReady = true;
prerender();
requestAnimFrame(update);
}

然后当你制作定期重绘制命令时,使用reverseCanvas而不是原来的画布:

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) {
ctx.save();
ctx.drawImage(reverseCanvas, frame*96, 0, 96, 96,
(canvas.width/2 – 48), (canvas.height/2 – 48), 96, 96);
ctx.restore();
}
}

不幸地是,当我们颠倒图像,动画也会往后播放,所以你必须把动画顺序也颠倒一下:

function update() {
requestAnimFrame(update);
var delta = Date.now() – lastUpdateTime;
if (acDelta > msPerFrame) {
acDelta = 0;
redraw();
frame–;
if (frame < 0) frame = 5;
} else {
acDelta += delta;
}
lastUpdateTime = Date.now();
}

result 8(from webappers)

result 8(from webappers)

如果有需要,你可以把画面转换成图像,即设置它的来源为使用包含编码图像数据的数据URL。画布有方法可以达到这个效果,所以代码很简单:

newImage = new Image();
newImage.src = reverseCanvas.toDataURL(“image/png”);

另一个有意思的图像操作是使用真正的象素数据。HTML5画布元素把图像数据当作RGBA格式的象素集合来显示。代码如下:

var imageData = ctx.getImageData(0, 0, width, height);

上述代码会返回一个包含宽度、高度和数据成员的ImageData结构。这个数据元素就是一个象素的集合。

这个数据组是由所有象素点组成的,每个象素点都表现为4个实体,红、绿、蓝和alpha通道层,色彩范围是0-255。因此一张宽和高都是512的图像形成的数组就包含1048576个元素,也就是512×512等于262144个象素点再乘上4(每个象素点是4个实体)。

使用这个数据组,这里有一个例子:图像的特殊红色成分增加而红色和蓝色成分减少,因此形成我们的2级怪物——地狱恶魔猫。

function prerender() {
reverseCanvas = document.createElement(‘canvas’);
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext(“2d”);
rctx.save();
rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
// modify the colors var imageData = rctx.getImageData(0, 0, reverseCanvas.width, reverseCanvas.height);
for (var i=0, il = imageData.data.length;
i < il; i+=4) {
if (imageData.data[i] != 0) imageData.data[i] = imageData.data[i] + 100;
// red
if (imageData.data[i+1] != 0) imageData.data[i+1] = imageData.data[i+1] – 50;
// green
if (imageData.data[i+1] != 0) imageData.data[i+2] = imageData.data[i+2] – 50;
// blue
}
rctx.putImageData(imageData, 0, 0);
rctx.restore();
}

这个for循环有4次,每一次都修改这3个主色。第4个通道,alpha保持不变,但如果你希望可以使用它变化某些象素的透明度。(游戏邦注:在下面的例子中,我们给图像数据使用一个dataURL,主要是为了避免直接修改象素产生交叉域名问题。你不必在自己的服务器上做这个。)

因为使用象素组修改图象需要重制所有元素,在这个地狱猫的例子中,超过100万次,你应该尽量提前计算,尽量不要制作变量/对象和跳过象素。

结论

将画布绘制、缩放、旋转、转换和象素修改相结合,再加上预渲染,制作出来的游戏的动态效果非常棒。

我最近在一款2D四方向横版太空射击的游戏《Playcraft》的DEMO中也使用了这些技术。美工只给每种飞船(玩家和敌人)制作一个帧,之后我再根据我们希望飞船转向的角度、流畅程度来旋转和预渲染飞船。我可以在运行时根据飞船类型修改角度——殖家飞船的默认转向角度是36度(非常流畅),而敌人和对手飞船的是16度(比较卡)。我还添加了一个选项,允许电脑性能比较好的玩家把角度提高到72(最流畅)。另外,飞船的徽章和标志会根据你所在的队伍动态地重新着色。这再一次节省了渲染和资源,而且允许飞船颜色根据玩家选择的队伍动态地调整。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How to Make Sprite Animations with HTML5 Canvas

by Martin Wells

Sprite Fundamentals

I’ve always loved web games; they’re just fun to make, easy to code (mostly), and there’s something really nice about how accessible a game is when the user just has to click a link to start playing.

Ajax and moving dom elements around made for some fun, but limited in what kind of experience you could create. For game developers, things are changing, and quickly. HTML5 is introducing a bunch of new options for game development purely in the browser, and the browser vendors are competing hard to be the best platform for the new standards.

So from a game developer’s perspective everything is going in the right direction: 2D  and 3D hardware-acceleration, high-performance javascript engines, integrated debuggers and profilers, and, probably most importantly, browser vendors who are actively racing to be the best for serious game development.

So the tools are becoming usable, the browsers capable, and the vendors are listening, we can just go make awesome games right? Well, mostly.

HTML5/Javascript game development is still early, and there’s pitfalls to avoid, as well as choices to make on which technology to deploy.

In this article I’ll run through some of the choices to be made developing 2D games, and hopefully give you some ideas for developing your own games using HTML5.

The Basics

First question you’ll have to answer is whether to use the HTML5 Canvas tag for drawing images (a scene-graph), or by manipulating DOM elements.

To do 2D games using the DOM, you basically adjust element styles dynamically in order to move it around the page. Whilst there are some cases where DOM manipulation is good, I’m going to focus on using the HTML5 canvas for graphics since it’s the most flexible for games in a modern browser.

If you’re worried about compatible for older browsers and canvas check out excanvas (http://excanvas.sourceforge.net/).

Page Setup

To get going you’ll need to create an HTML page that contains the canvas tag:

<!doctype html>
<html>
<head>
<title></title>
</head>
<body style=’position: absolute; padding:0; margin:0; height: 100%; width:100%’>

<canvas id=”gameCanvas”></canvas>

</body>
</html>
If you load this up, you’ll be rewarded with, well, nothing much. That’s because whilst we have a canvas tag, we haven’t drawn anything on it. Let’s add some simple canvas calls to draw some boxes.

<head>
<title></title>
<script type=’text/javascript’>
var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
var ctx = canvas.getContext(“2d”);
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = ‘#333333′;
ctx.fillRect(canvas.width / 3, canvas.height / 3,
canvas.width / 3, canvas.height / 3);
}
</script>
</head>
<body onload=’onload()’ …

In this example I’ve added an onload event binding to the body tag, and then implemented the function to grab the canvas element and draw some boxes. Simple enough so far.

The boxes are nice, but you’ll notice the canvas doesn’t take up the complete area of the browser window. To accommodate that we can set it’s size by adding a width and height style to the canvas tag. I prefer to keep things dynamic by adjusting the size based on the size of the document element the canvas is contained within.

var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight;

Reload and you’ll see the canvas taking up the entire screen. Sweet.

Taking things a little further, let’s handle resizing of the canvas if the browser window is resized by the user.

var canvas = null;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
resize();
}
function resize() {
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight;
var ctx = canvas.getContext(“2d”);
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = ‘#333333′;
ctx.fillRect(canvas.width/3, canvas.height/3, canvas.width/3, canvas.height/3);
}

And add the onresize call to the body tag.

<body onresize=’resize()’ …

Now if you resize the browser the rectangles will follow along nicely.

Loading Graphics

Most games are going to need animated sprites, so let’s add some graphics.

First up you’ll need get to an image resource. Since we’re going to be drawing it from within javascript, I find it makes sense to declare the image there and then set its src attribute to be the url of the image you want to load. Please download this image file, which is adapted from SpriteLib GPL:

simba.png var img = null;
function onload() {

img = new Image();
img.src = ‘simba.png’;
}

You can then draw the image by adding this to the resize method:

ctx.drawImage(img, canvas.width/2 – (img.width/2), canvas.height/2 – (img.height/2));

If you then reload the page, in most cases, you’ll see an image appear. I say most cases, because it depends on how fast your machine is, and whether the browser has cached the image already. That’s because the resize method is being called in between when you’ve started loading the image (setting its src attribute) and when the browser has it ready to go. With one or two images you might get away with it, but as soon as your game expands you’ll need to wait till all the images are loaded before taking action.

To wait, add a notification listener to the image so you’ll get a callback when the image is ready. I’ve had to rearrange things a little to make it all work, so here’s the complete updated code:

var canvas = null;
var img = null;
var ctx = null;
var imageReady = false;
function onload() {
canvas = document.getElementById(‘gameCanvas’);
ctx = canvas.getContext(“2d”);
img = new Image();
img.src = ‘images/simba.png’;
img.onload = loaded();
resize();
}
function loaded() {
imageReady = true;
redraw();
}
function resize() {
canvas.width = canvas.parentNode.clientWidth;
canvas.height = canvas.parentNode.clientHeight;
redraw();
}
function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
ctx.drawImage(img, canvas.width/2 – (img.width/2),
canvas.height/2 – (img.height/2));
}

And the results should be:

This image shows 6 running frames of a little vampire kitty (well, that’s what I think it looks like). To animate the sprite we need to draw each of the frames one at a time.

Sprite Animation

You can draw a single frame using the source parameters of the drawImage call. In effect,only drawing a constrained portion of the source image. So to draw only the first frame use
the expanded version of drawImage that let’s you specify a rectangle in the source image.

Since our cat animation is made up from 6 frames each 96 x 96 pixels in size, we can do:

ctx.drawImage(img, 0, 0, 96, 54, canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);The key thing here is the starting 0, 0, 96, 54. That limits the image being drawn to just the first frame of our cat animation. I’ve also adjust the centering to be based on a single frame as well (the 48’s) rather than the entire image size containing all six frames.

Now the fun bit. To make the animation work we need to track which frame to draw, then as time progresses advance the frame number. To do this we’ll need to go from a static page to one that is cycling on a timed basis.

Let’s start by doing things the old fashioned way. Add an interval timer with a cycle time equivalent to 60 frames per second (1000ms divided by 60). To make sure we only start cycling the animation after the image has loaded, put the call in the loaded function:

function loaded() {
imageReady = true;
setTimeout( update, 1000 / 60 );
}

Adding an update function can then step forward the frame, and call for the redraw:

var frame = 0;

function update() {
redraw();
frame++;
if (frame >= 6) frame = 0;
setTimeout( update, 1000 / 60 );
}

After the draw and frame has been advance the timeout is set again.

Next, modify the draw image to move the source window according to which frame we want to draw (the key piece being the source X position being set to frame multiplied by the size of the frame (in this case frame * 96):

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
}

And the result:

Our evil undead-vampire-kitty lives! At super-cat speeds even.

Now we have an animation going let’s do some improvements.

requestAnimFrame

setTimeout is good, and it works well in just about every browser, but there’s an even better method, requestAnimFrame.

requestAnimFrame basically acts as a setTimeout, but the browser knows you’re rendering a frame so it can optimize the draw cycle, as well as how that interacts with the rest of the page reflow. It will even detect if the tab is visible and not bother drawing if it’s hidden, which saves battery (and yes, web games cycling at 60fps will burn battery). Under the hood, the browsers also get the opportunity to optimize in other mysterious ways they don’t tell us much about. In my experience with heavier frame loads (hundreds of sprites especially) there can be substantial gains in performance; especially on recent browser builds.

One caveat I’d add is that in some cases setTimeout will outperform requestAnimFrame, notably on mobile. Test it out and config your app based on the device.

The call to use requestAnimFrame is distinct across different browsers so the standard shim (thanks to Paul Irish) to detect this is:

window.requestAnimFrame = (function(){
return  window.requestAnimationFrame       ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame    ||
window.oRequestAnimationFrame      ||
window.msRequestAnimationFrame     ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();

There’s also a built-in fall back to plain old setTimeout if requestAnimFrame support is not available.

You then need to modify the update method to repeatedly make the request:

function update() {
requestAnimFrame(update);
redraw();
frame++;
if (frame >= 6) frame = 0;
}

Calling the requestAnimFrame before you actually carry out the render/update tends to provide a more consistent result.

On a side note, when I first started using requestAnimFrame I searched around for how it would be timed, but couldn’t find anyting. That’s because it isn’t. There’s no equivalent to setting the MS delay you’ll find with setTimeout, which means you can’t actually control the frame rate. Just do your work, and let the browser take care of the rest.

Another thing to watch out for is if you are using requestAnimFrame from within your own closure, then you’ll need to do a native wrapping to call it, such as:

my.requestAnimFrame = (function () {
var func = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element)
{
window.setTimeout(callback, 1000 / this.fps);
};

// apply to our window global to avoid illegal invocations (it’s a native)
return function (callback, element) {
func.apply(window, [callback, element]);
};
})();

Time-based Animation

Next we need to solve the speed at which poor kitty has been running. Right now the animation frame advances according to the frame rate, which is going to jump around on different devices. That’s bad; if you’re moving a character and animating at the same time, things are going to look weird and inconsistent across different frame rates. You can try to control the frame rate but in the end basing animation on real timing is going to make for a better all round experience.

You’ll also find that timing in general in games is going to apply to everything you do: firing rate, turning speed, accerlation, jumping, they’ll all be better handled using proper timing.

To advance kitty at a regulated speed we need to track how much time has passed,and then advance the frames according to the time allocated to each one. The basics of this is:

Set an animation speed in terms of frames per second. (msPerFrame)

As you cycle the game, figure out how much time has passed since the last frame (delta).

If enough time has passed to move the animation frame forward, then advance the frame and set the accumulated delta to 0.

If enough time hasn’t passed, remember (accumulate) the delta time (acDelta).

Here’s this in our code:

var frame = 0;
var lastUpdateTime = 0;
var acDelta = 0;
var msPerFrame = 100;

function update() {
requestAnimFrame(update);

var delta = Date.now() – lastUpdateTime;
if (acDelta > msPerFrame)
{
acDelta = 0;
redraw();
frame++;
if (frame >= 6) frame = 0;
} else
{
acDelta += delta;
}

lastUpdateTime = Date.now();
}

If you load this up, our little kitty has calmed down to a more reasonable speed.

Scaling and Rotating

You can also use the 2D canvas to perform a variety of operations on the image as it’s rendered, such as rotation and scaling.

For example, let’s make some kittens by scaling the image down by half. You can do this by adding a ctx.scale(0.5, 0.5) to the draw call:

function redraw()
{
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady)
{
ctx.save();
ctx.scale(0.5,0.5);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
ctx.restore();
}
}

Since the scaling is changing, you’ll notice I also added a ctx.save() before the scale call, then a ctx.restore() at the end. Without this, the calls to scale will accumulate and poor kitty will quickly shrink into oblivion (try it, its fun).

Scaling also works using negative values in order to reverse and image. If you change the scale values from (0.5, 0.5) to (-1, 1) the cat image will be flipped horizontally, so he’ll run in the opposite direction. Notice that translate is used to flip the starting X position to offset the reversal of the image.

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) {
ctx.save();
ctx.translate(img.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, frame*96, 0, 96, 54,
canvas.width/2 – 48, canvas.height/2 – 48, 96, 54);
ctx.restore();
}
}

You can use rotate to do (duh) rotation. Here’s kitty climbing the walls:

ctx.rotate( 270*Math.PI/180 );

ctx.drawImage(img, frame*96, 0, 96, 54,
-(canvas.width/2 – 48), (canvas.height/2 – 48), 96, 54);

In this case, by rotating the context, the coordinates are rotated as well, not just the image, so the drawImage call offset for this by making the inverting the x position of where the kitty will be drawn.

Such a talented kitty (though vampires are supposed to be able to climb walls right).

The scaling and rotation is cool. Man I can do anything! Well, not really. It’s awesome, but it’s also slow and will have a pretty dramatic impact on rendering performance. In a production game there’s another trick to handling this, and a bunch of other rendering performance issues you might encounter: prerendering.

Prerendering

Pre-rendering is just taking images that you would have rendered during your regular draw cycle and assembling them or manipulating them before hand. You do the expensive rendering operation once, then draw the prerendered result in the regular draw cycle.

In HTML5, you need to draw on a separate invisible canvas, and then instead of drawing an image, you draw the other canvas in its place.

Here’s an example of a function that prerenders the kitty as a reversed image.

var reverseCanvas = null;

function prerender() {
reverseCanvas = document.createElement(‘canvas’);
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext(“2d”);
rctx.save();
rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
rctx.restore();
}

Notice a canvas object is created, but not added to the DOM, so it wont be displayed. The height and width is set to the original spritesheet, and then the original image is drawn using the render buffer’s 2D context.

To setup the prerender you can call it from the loaded function.

function loaded() {
imageReady = true;
prerender();
requestAnimFrame(update);
}

Then when you make the regular redraw call, use the reverseCanvas, instead of the original:

function redraw() {
ctx.fillStyle = ‘#000000′;
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imageReady) {
ctx.save();
ctx.drawImage(reverseCanvas, frame*96, 0, 96, 96,
(canvas.width/2 – 48), (canvas.height/2 – 48), 96, 96);
ctx.restore();
}
}

Unfortunately, when we reversed the image the animation now plays backwards as well, so you’ll need to reverse the animation sequence as well:

function update() {
requestAnimFrame(update);
var delta = Date.now() – lastUpdateTime;
if (acDelta > msPerFrame) {
acDelta = 0;
redraw();
frame–;
if (frame < 0) frame = 5;
} else {
acDelta += delta;
}
lastUpdateTime = Date.now();
}

If you need to, you can convert the canvas into an image by setting its source to use a data url containing the encoded image data. Canvas has a method to do this, so its as easy as:
newImage = new Image();

newImage.src = reverseCanvas.toDataURL(“image/png”);

Another nice image manipulation is to play with the actual pixel data. The HTML5 canvas elements exposes the image data as an array of pixels in RGBA format. You can gain access to the data array form a context using:

var imageData = ctx.getImageData(0, 0, width, height);

Which will return an ImageData structure containing width, height and data members. The data element is the array of pixels we’re after.

The data array is made up of all the pixels, with each pixel being represented by 4 entries, red, green, blue and the alpha level, all ranging from 0 to 255. Thus an image which is 512 wide by 512 high will result in an array that has 1048576 elements in it – 512×512 equals 262,144 pixels, multiplied by 4 entries per pixel.

Using this data array, here’s an example where the specific red component of image is increased, whilst the red and blue components are reduced, thus creating our level 2 monster, the hell-spawn-demon-kitty.

function prerender() {
reverseCanvas = document.createElement(‘canvas’);
reverseCanvas.width = img.width;
reverseCanvas.height = img.height;
var rctx = reverseCanvas.getContext(“2d”);
rctx.save();
rctx.translate(img.width, 0);
rctx.scale(-1, 1);
rctx.drawImage(img, 0, 0);
// modify the colors
var imageData = rctx.getImageData(0, 0, reverseCanvas.width, reverseCanvas.height);
for (var i=0, il = imageData.data.length; i < il; i+=4) {
if (imageData.data[i] != 0) imageData.data[i] = imageData.data[i] + 100;    // red
if (imageData.data[i+1] != 0) imageData.data[i+1] = imageData.data[i+1] – 50; // green
if (imageData.data[i+1] != 0) imageData.data[i+2] = imageData.data[i+2] – 50; // blue
}
rctx.putImageData(imageData, 0, 0);
rctx.restore();
}

The for loop is interating over the data array in steps of 4, each time modifying the three primary colors. The 4th channel, alpha, is left as is, but if you like you can use this to vary the transparency of certain pixels. (Note: in the JSFiddle example below, we use a dataURL for the image data, specifically to avoid cross-domain issues with direct pixel manipulation. You won’t need to do that on your own server.)

Here’s our level 2 boss kitty:

Since manipulating an image using the pixel array requires iterating over all the elements, this the case of hell kitty, that’s over a million times, you should keep things pretty optimized: precalulate as much as possible, don’t create variables/objects and skip pixels as much as possible.

Conclusion

The combination of canvas drawing, scaling, rotating, translating and pixel manipulation, along with the performance option of using prerendering gives a range of powers to make cool, dynamic games.

As an example, I used these techniques in one of Playcraft’s demo games recently, a 2D 4-way scrolling space shooter. The artists produced only a single frame of each ship (player and enemy fighters), which I would then rotate and prerender according to how many degrees, and thus how smooth, we wanted the ships to turn. I could adjust the number of angles based on the type of ship at run time – by default, player ships rendered with 36 turning angles (very smooth), whereas enemy and opponent ships at only 16 angles (choppy). I also added an option to let players on more powerful computers choose to increase the smoothness angles to 72 all round (super smooth). In addition, I dynamically recolor the emblems and markings on the ships (the cool big stripes along the wings) according to the team you’re on. This again saves on rendering and resources, but also allows the ship colors to be dynamically adjusted based on a user selected team color.

For more information on what you can do with canvas check out the Canvas Element API.(source:webappers)


上一篇:

下一篇: