游戏开发

适用于 Web 游戏的 2D 绘制技术和库

Michael Oneppo

在长时间内,确实只有一种方式来制作交互式 Web 游戏:Flash。不管喜欢与否,Flash 拥有快速绘制系统。每个人都可以用它来创建动画、指点式冒险活动和各种其他体验。

当浏览器将 Web 标准调整到符合 HTML5 时,会有大量选项可用于开发快速、高质量的图形 — 而无需插件。本文将介绍绘制方法的一个小示例,以及基础技术和一些使这些技术更易于使用的库。我将不会介绍专门适用于游戏的库。适用于游戏的库有很多,我将把这些库留到另一篇文章中进行讨论。

绘制标准

从 HTML5 开始,出现了三种进行 2D 绘制的常见方法:文档对象模型 (DOM)、画布和可缩放的向量图形 (SVG) 格式。在跳转到使用这些技术的库调查之前,我将介绍每种方法的工作方式以便更好地了解每种方法的取舍。

毫无疑问,在 HTML 中绘制图形的最基本方法就是使用 HTML。通过创建大量图像或背景元素和使用类似 jQuery 的库,您无需重绘场景就可以快速创建来回移动的 sprite。浏览器将为您完成这一操作。这种结构通常称为场景图。对于 HTML,场景图就是 DOM。由于您将使用 CSS 来设置您 sprite 样式,则还可以使用 CSS 过渡和动画将某些平滑运动添加到您的场景。

使用此方法的主要问题是它依赖于 DOM 呈现器。当您要处理复杂场景时,这可以降低运行速度。我不建议使用超过几百个元素。因为任何比匹配三个更复杂的操作或平台游戏都可能具有性能问题。并且,突然增加元素的数量(例如在粒子系统中)就可能会导致动画中的停顿。

此方法的另一个问题是您需要使用 CSS 来设置元素样式。根据您编写 CSS 的方式,这一操作可能相当快速,也可能非常缓慢。最后,针对 HTML 编写的代码可能难以移到不同的系统,如本机 C++。如果您想要将您的游戏移植到一个类似控制台的地方,则这一问题非常重要。以下就是对优点的总结:

  • 在网页的基本结构上进行构建
  • jQuery 和其他库可以轻松地移动内容
  • Sprite 相对较易于设置
  • 使用 CSS 过渡和动画的内置动画系统

缺点的总结:

  • 许多小型元素会妨碍您的开发进度
  • 需要使用 CSS 来设置元素的样式
  • 没有矢量图像
  • 很难移植到其他平台

HTML5 Canvas

Canvas 元素弥补了许多缺点。它提供了即时模式呈现环境 — 大量平面像素。您告诉它要在 JavaScript 中绘制的内容,它会立即进行绘制。因为它将您的绘制命令转换为像素,所以您可以快速堆积长的绘制命令列表,而不会使系统停止响应。您可以绘制几何图形、文本、图像、渐变和其他元素。若要了解有关使用游戏画布的详细信息,请查看 David Catuhe 文章,网址为 bit.ly/1fquBuo

那么,它的缺点是什么呢?由于画布会在它完成绘制的那一刻忘记绘制内容,所以您每次想要进行更改时都必须自己重绘场景。并且,如果您想要以复杂的方式修改形状,如弯曲或设置动画效果,则必须进行计算并重绘该项。这意味着您需要在您自己的数据结构中维护场景的大量数据。考虑到有许多库可以简化这一过程,这个缺点实际上也没什么大不了的。如果您确实想要执行某些自定义操作,请注意画布不会为您保留信息。最后,画布不包括动画。您必须在后续步骤中绘制您的场景才能制作一个流畅的动画。以下就是对优点的总结:

  • 直接绘制 — 场景可能会更复杂
  • 支持很多不同的可视元素

缺点的总结:

  • 缺乏场景的内部存储,需要自行构建
  • 必须手动完成复杂转换和动画效果
  • 缺乏动画系统

SVG:可缩放的向量图形

作为用于描述 2D 视觉效果的基于 XML 的标记,SVG 与 HTML 类似。关键的区别在于,SVG 是针对绘制,而 HTML 主要用于文本和布局。因此,SVG 具有一些功能强大的绘制功能,如平滑形状、复杂动画、变形和甚至是类似模糊处理的映像筛选器。与 HTML 一样,SVG 具有场景图形结构,因此您可以细读 SVG 元素、添加形状、更改它们的属性,而不用担心需要重绘所有内容。浏览器将为您完成这一操作。来自第 9 频道的视频“使用 HTML5 中的 SVG” (bit.ly/1DEAWmh) 进行了更详细的说明。

与 HTML 一样,复杂场景会使 SVG 停滞不前。SVG 可以处理某种程度的复杂性,但它不能完全匹配通过使用画布所提供的复杂性。此外,用于处理 SVG 的工具可能很复杂,尽管有其他工具来简化这一过程。以下就是对优点的总结:

  • 具有许多绘制选项,如曲面和复杂形状
  • 结构化,无需重绘

缺点的总结:

  • 复杂性会让 SVG 停滞不前
  • 难以操作

2D 绘制库

现在,您了解了有关可用于在 Web 上进行绘制的标准,我将介绍一些可以简化绘制和动画的库。需要指出的是,即使您很少绘制,也无需执行绘制以外的其他操作。例如,您经常需要图形对输入作出反应。库有助于简化与绘制相关联的常见任务。

KineticJS 想要一个画布的场景图?KineticJS 是功能非常强大的画布库,以场景图开始并可以添加更多的功能。基准上,KineticJS 可以让您定义包含要绘制形状的画布中的图层。例如,图 1 显示如何使用 KineticJS 绘制一个简单的红色圆圈。

图 1 使用 KineticJS 绘制一个圆圈

// Points to a canvas element in your HTML with id "myCanvas"
var myCanvas = $('#myCanvas'); 
var stage = new Kinetic.Stage({
  // get(0) returns the first element found by jQuery,
  // which should be the only canvas element
  container: myCanvas.get(0),
    width: 800,
    height: 500
  });
   
var myLayer = new Kinetic.Layer({id: “myLayer”});
stage.add(myLayer);
var circle = new Kinetic.Ellipse({
  // Set the position of the circle
  x: 100,                                            
  y: 100,
   
  // Set the size of the circle
  radius: {x: 200, y: 200},
   
  // Set the color to red
  fill: '#FF0000'  
});
 
myLayer.add(circle);
stage.draw();

每次您想要重绘场景时,都将需要调用图 1 的最后一行。KineticJS 通过记住场景布局并确保正确绘制所有内容来完成其余的工作。

KineticJS 中的一些有趣事情使其功能相当强大。例如,某个对象的 fill 属性可以实现大量操作,包括渐变:

fill: {
  start: {x: 0, y: 0},
  end: {x: 0, y: 200},
  colorStops: [0, '#FF0000', 1, '#00FF00']
},

或图像:

// The "Image" object is built into JavaScript and
// Kinetic knows how to use it
fillPatternImage: new Image('path/to/an/awesome/image.png'),

KineticJS 还具有一个动画系统,这样就可以通过创建 Animation 对象或者在场景的形状上使用 Tween 对象过渡属性来四处移动内容。图 2 显示这两种动画类型。

图 2 使用 KineticJS 的动画

// Slowly move the circle to the right forever
var myAnimation = new Kinetic.Animation(
  function(frame) {
    circle.setX(myCircle.getX() + 1);
  },
  myLayer);
 
// The animation can be started and stopped whenever
myAnimation.start();
// Increase the size of the circle by 3x over 3 seconds
var myTween = new Kinetic.Tween({
  node: circle,
  duration: 3,
  scaleX: 3.0,
  scaleY: 3.0
});
 
// You also have to initiate tweens
myTween.play();

KineticJS 功能强大且使用广泛,尤其适用于游戏。请在 kineticjs.com 上查看代码、示例和文档。

Paper.js Paper.js 提供多个库,用于简化画布的绘制。它提供称为 PaperScript 的略有修改的 JavaScript 版本来简化常见的绘制任务。当您的项目中包括 PaperScript 时,链接到它,就像链接到常规脚本一样,只是使用不同的代码类型:

<script type=“text/paperscript" src=“mypaperscript.js”>

这样使 Paper.js 对该代码的解释略有不同。实际上,Paper.js 只有两个组成部分。首先,PaperScript 拥有称为 Point 和 Size 的两个内置对象。PaperScript 包括这些对象以使用其功能的常见用法,并提供直接加上、减去和乘以这些类型的功能。例如,若要在 PaperScript 中移动对象,您可以执行以下操作:

var offset = new Point(10, 10);
 
var myCircle = new Path.Circle({
  center: new Point(300, 300),
  radius: 60
});
 
// Direct addition of Point objects!
myCircle.position += offset;

Paper.js 会以不同方式解释的第二件事就对事件作出响应。请考虑用 JavaScript 编写以下代码:

function onMouseDown(event) {
  alert("Hello!");
}

这将不执行任何操作,因为该函数不绑定到任何元素的事件。但是,在 PaperScript 中编写相同的代码时,Paper.js 将自动检测此函数并将其绑定到鼠标按下事件。可以在 paperjs.org 了解更多相关信息。

Fabric.js Fabric.js 是一个功能包式的画布库,具有将多种高级效果和形状融入没有大量代码的网页的功能。一些值得注意的功能包括诸如背景删除的映像筛选器、构成您自己的复合对象的自定义类以及您可以以多种样式在画布上进行绘制的“自由绘制”支持。Fabric.js 类似于 KineticJS,因为 Fabric.js 也有场景图,但它采用有些人更喜欢的更为简洁的结构。例如,您从来无需重绘场景:

var canvas = new fabric.Canvas('myCanvas');
var circle = new fabric.Circle({
  radius: 200,
    fill: '#FF0000',
    left: 100,
    top: 100
});
 
// The circle will become immediately visible
canvas.add(circle);

这并不是巨大的差异,但 Fabric.js 提供混合自动重绘和手动重绘的精细呈现控件。为了举例说明,请在 Fabric.js 中增加一个圆圈,如下所示:

circle.animate(
  // Property to animate
  'scale',
  // Amount to change it to
  3,
  {
    // Time to animate in milliseconds
    duration: 3000,
    // What's this?
    onChange: canvas.renderAll.bind(canvas)
  });

当为 Fabric.js 中的某些内容设置动画时,一旦更改某个值,您必须告诉它所要执行的操作。大多数情况下,您希望它重绘场景。这就是 canvas.renderAll.bind(canvas) 引用的内容。该代码会返回一个呈现整个场景的函数。但是,如果您正在以这种方式为大量对象设置动画,则它会不必要地为每个对象重绘一次场景。您可以转而取消重绘整个场景,并自己重绘动画。图 3 演示这种方法。

图 3 Fabric.js 中更严格的重绘控制

var needRedraw = true;
 
// Do things like this a lot, say hundreds of times
circle.animate(
  'scale',
  3,
  {
    duration: 3000,
       
    // This function will be called when the animation is complete
    onComplete: function() {
      needRedraw = false;
    }
  });
 
// This function will redraw the whole scene, and schedule the
// next redraw only if there are animations going
function drawAnimations() {
  canvas.renderAll();
  if (needRedraw) {
    requestAnimationFrame(drawAnimations);
  }
}
 
// Now draw the scene to show the animations
requestAnimationFrame(drawAnimations);

Fabric.js 提供大量自定义项,以便您可以仅在需要时优化绘制。对于某些人来说,这可能很难处理。不过对于很多复杂游戏来说,这可能是一项重要功能。请在 fabricjs.com 上查看详细信息。

Raphaël Raphaël 是一种有用的 SVG 库,消除了处理 SVG 的大部分复杂性。在适用时,Raphaël 使用 SVG。在不适用时,Raphaël 使用浏览器中可用的任何技术以在 JavaScript 中实现 SVG。在 Raphaël 中创建的每个图形对象也是具备 DOM 对象所有功能的 DOM 对象,例如绑定事件处理程序和 jQuery 访问。Raphaël 还拥有一个动画系统,允许您定义独立于绘制对象并启用大量重用的动画:

var raphael = Raphael(0, 0, 800, 600);
 
var circle = raphael.circle(100, 100, 200);
circle.attr("fill", "red");
circle.animate({r: 600}, 3000);
 
// Or make a custom animation
var myAnimation = Raphael.animation(
  {r: 600},
  3000);
circle.animate(myAnimation);

在这段代码中,Raphaël 将 SVG 文档放置在带有 circle 元素的页面上,而不是绘制一个圆圈。奇怪的是,Raphaël 本身并不支持加载 SVG 文件。Raphaël 没有丰富的社区,因此可从 bit.ly/1AX9n7q 获取一个插件执行此操作。

Snap.svg Snap.svg 看起来与 Raphaël 相似:

var snap = Snap("#myCanvas"); // Add an SVG area to the myCanvas element
var circle = snap.circle(100, 100, 200);
circle.attr("fill", "#FF0000");
circle.animate({r: 600}, 1000);

主要区别之一是,Snap.svg 包括无缝 SVG 导入:

Snap.load("myAwesomeSVG.svg");

第二个关键的区别是,如果您知道您正在使用的 SVG 的结构,Snap.svg 会提供功能强大的内置工具来搜索和编辑准备就绪的 SVG 结构。例如,假设您想要使您的 SVG 中的所有组(“g”标记)不可见。在 SVG 加载后,您必须将回调中的此功能添加到 load 方法中:

Snap.load("myAwesomeSVG.svg", function(mySVG) {
  mySVG.select("g").attr("opacity", 0);
});

Select 方法的工作方式与 jQuery“$”选择器非常类似,但它的功能非常强大。请在 snapsvg.io 上查看 Snap.svg。

更多:p5.js

很多这些库提供一些额外的常见任务。这将创建一系列技术来满足各种应用程序,从简单绘制到交互式媒体和复杂的游戏体验。这一系列之中还有些什么 — 不仅仅是简单的绘制解决方案,也不完全是游戏引擎?

值得注意的一个项目是 p5.js,它用流行的处理编程语言构建(参见 processing.org)。此 JavaScript 库通过在浏览器中实现处理来提供交互式媒体环境。p5.js 将绝大多数常见任务合并到一组函数,您必须将这组函数定义为响应系统中的事件,如重绘场景或鼠标输入。它与 Paper.js 很类似,但它还具备多媒体库。下面是一个演示这种方法如何得到更简洁图形代码的示例:

float size = 20;
function setup() {
  createCanvas(600, 600);
}
 
function draw() {
  ellipse(300, 300, size, size);
  size = size + .1;
}

此程序使圆圈增加大小,直到它填满屏幕。请在 p5js.org 上查看 p5.js。

那么使用哪种方法呢?

画布和 SVG 有一些明显的优点和缺点。也存在很多库,可用于显著地减少了每种方法的缺点。那么,您该使用哪种方法呢?我通常不建议使用传统的 HTML。新型游戏很有可能会超过它可以支持的图形复杂性。因此,接下来就是在 SVG 和画布之间进行选择,这难以抉择。

对于非常独特的游戏类型,答案就变得容易一些。如果您正在构建带有成千上万个粒子的游戏,则可以使用画布。如果您正在构建一个漫画连载样式的指点式冒险游戏,则可以考虑 SVG。

对于大多数游戏而言,这一选择不会涉及到性能,因为有许多方面会让您相信是那样的。您可以花费数小时反复讨论要使用的库。不过最后,这些都是您可以用来编写游戏的时间。

我的建议是,根据您的艺术资产做出选择。如果您要在 Adobe Illustrator 或 Inkscape 中制作字符动画,为什么将动画的每帧转换成像素呢?以本机方式使用矢量艺术。不要浪费气力将您的作品塞进画布。

相反,如果您的作品主要是基于像素的或者您要在逐像素的基础上生成复杂效果,则画布将是一个完美的选择。

另一个选择

如果您正在寻找可能实现的最佳性能,并且您愿意处理稍多一点的复杂性以实现这一性能,我强烈建议您考虑 Pixi.js。不同于我在本文中介绍的任何其他方法,Pixi.js 使用 WebGL 实现 2D 呈现。这提供了一些主要的性能改进。

该 API 不像本文所述的其他 API 那么简单,但它并没有巨大的差异。此外,WebGL 不像其他技术那样在那么多的浏览器上受到支持。因此在较旧的系统上,Pixi.js 没有任何性能优势。无论哪种方式,做出您的选择,并享受该过程。


Michael Oneppo 是富有创造性思维的技术专家并且以前是 Microsoft Direct3D 团队的项目经理。他最近的工作包括担任 Library For All(非盈利技术)的 CTO 以及致力于探索获得 NYU 交互式电信计划方面的硕士学位。​

衷心感谢以下 Microsoft 技术专家对本文的审阅:Shai Hinitz