当前位置:首页 >教程首页 > 游戏设计 > 游戏动作设计师班 >如何用HTML5 Canvas制作子画面动画

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

发布时间:2018-11-17 19:57:24
作者:Martin Wells
 
子面画基本原理
 
我一直很喜欢网页游戏,因为大多数都容易制作,而且容易玩(只要点击一个链接就可以开始玩了)。

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

sprite-animations
 
所以,从游戏开发者的角度看,一切都朝着正确的方向发展: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
 
这个箱子不错,但你会注意到,画布没有铺满整个浏览器窗口。为了解决这个问题,我们可以增加画布的宽度和高度。我是指根据画布所包含的文件元素的大小来灵活地调整画布尺寸。

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
 
载入图像
 
大部分游戏都需要动画的子画面,所以我来添加一些图像吧。
 
首先,你需要图像资源。因为我们要用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

这个图像显示了一只吸血鬼猫(好吧,是我自己觉得像)的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

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

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

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
 
缩放和旋转
 
当图像渲染后,你还是可以使用这个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
 
你会发现我还在缩放命令前添加了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
 
真是一只天才的猫(不过吸血鬼本来就能爬墙)。
 
缩放和旋转效果很好。好是好,但它也很慢,会对渲染表现产生重大影响。在制作游戏时,还有另一个技巧——预渲染,可以解决这个问题以及你可能遇到了大量其他渲染表现问题。
 
预渲染
 
预渲染就是提前处理图像。你只做一次昂贵的渲染操作,然后循环使用已渲染好的结果。
 
在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
 
如果有需要,你可以把画面转换成图像,即设置它的来源为使用包含编码图像数据的数据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(最流畅)。另外,飞船的徽章和标志会根据你所在的队伍动态地重新着色。这再一次节省了渲染和资源,而且允许飞船颜色根据玩家选择的队伍动态地调整。
学员作品赏析
  • 2101期学员李思庭作品

    2101期学员李思庭作品

  • 2104期学员林雪茹作品

    2104期学员林雪茹作品

  • 2107期学员赵凌作品

    2107期学员赵凌作品

  • 2107期学员赵燃作品

    2107期学员赵燃作品

  • 2106期学员徐正浩作品

    2106期学员徐正浩作品

  • 2106期学员弓莉作品

    2106期学员弓莉作品

  • 2105期学员白羽新作品

    2105期学员白羽新作品

  • 2107期学员王佳蕊作品

    2107期学员王佳蕊作品

专业问题咨询

你担心的问题,火星帮你解答
×

确定