如何用 html5 制作手机上的动画

2015年02月26日 11:08 0 点赞 0 评论 更新于 2025-11-21 16:27

很多朋友都想知道怎样用 HTML5 制作手机上的动画,下面将详细说明。

子画面基本原理

我一直很喜欢网页游戏,因为大多数网页游戏容易制作且容易玩,只要点击一个链接就可以开始。不过,Ajax 和移动 DOM 元素虽然有一定趣味性,但会限制可制作的游戏类型。对于游戏开发者来说,技术的发展日新月异,HTML5 为网页游戏开发不断提供大量新选择,各浏览器供应商也为成为新标准的最佳平台而展开激烈竞争。

从游戏开发者的角度看,一切正朝着好的方向发展: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 命令来绘制小箱子:

<!doctype html>
<html>
<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()">
<canvas id="gameCanvas"></canvas>
</body>
</html>

在这个例子中,在 body 标签中添加了一个 onload 事件,执行该事件时会获取画布元素并绘制几个箱子,操作较为简单。

不过,你会注意到画布没有铺满整个浏览器窗口。为解决这个问题,可根据画布所包含的文件元素的大小来灵活调整画布尺寸:

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);
}

同时,在 body 标签中添加 onresize 命令:

<body onresize="resize()">

现在,当你调整浏览器的大小时,矩形会相应地进行调整。

载入图像

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

首先,需要准备图像资源。由于要用 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));
}

此时显示的图像包含一只类似吸血鬼猫的 6 个奔跑帧。为了将这个子画面做成动画,需要每次绘制一个帧。

子画面动画

可以使用 drawImage 命令的源参数来绘制一个帧,实际上是只绘制源图像的一部分。为了绘制第一帧,使用允许指定源图像中矩形的 drawImage 拓展版。由于猫动画由 6 个 96 x 54 像素大小的帧组成,可添加如下代码:

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);
}

此时,猫的动画就会动起来,但速度可能较快。

requestAnimFrame

setTimeout 是一个不错的方法,几乎在所有浏览器上都能正常运行,但还有一个更好的选择——requestAnimFrame。

requestAnimFrame 的作用与 setTimeout 类似,但浏览器知道你正在渲染帧,因此可以优化绘制循环以及与页面其他部分的回流。它甚至会检测标签是否可见,如果隐藏则不进行绘制,从而节省电池(以 60fps 的速率循环的网页游戏非常耗电)。此外,浏览器还有可能以其他未知的方式进行优化。根据我的经验,使用 requestAnimFrame 可以显著提高性能,特别是在现代浏览器中。

不过,在某些情况下,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 本身不能计时,没有与设置 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();
}

载入页面后,小猫的移动速度会更加合理。

缩放和旋转

当图像渲染后,可以使用 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();
}
}

注意,在缩放命令前添加了 ctx.save(),并在最后添加了 ctx.restore()。如果不这样做,缩放命令会累积,猫的图像会很快缩小到看不见。

使用负值可以颠倒图像。将缩放值从 (0.5, 0.5) 改为 (-1, 1),猫图像会水平翻转,往相反的方向跑:

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 位置来抵消这个影响。

不过,缩放和旋转操作会影响渲染性能。在制作游戏时,可以使用预渲染技巧来解决这个问题以及其他可能遇到的渲染性能问题。

预渲染

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

在 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();
}

注意,这里创建的画面对象不会显示出来。为了设置预渲染,可以从 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, 54);
ctx.restore();
}
}

通过预渲染,可以提高渲染性能,尤其是在处理复杂动画时。

作者信息

boke

boke

共发布了 3994 篇文章