HTML5 游戏的网络字体预加载

在游戏开发中,有两种渲染文本的方法:通过位图字体矢量字体。位图字体本质上是包含给定字体所有字符的精灵图图像。精灵图使用常规的字体文件(传统上为.ttf)。这如何应用于 Web 和 HTML5 游戏的开发?

您可以像往常一样使用位图字体——毕竟它们只是图像,并且大多数 HTML 5 游戏引擎或库都直接支持它们。对于矢量字体渲染,我们可以依赖任何可以通过 CSS 访问的字体:这包括玩家计算机中已存在的系统字体(如 Arial 或 Times New Roman),或网络字体,如果它们不在系统中,则可以动态下载。

但是,并非所有游戏引擎或框架都包含将这些字体作为常规资源(如图像或音频文件)加载的机制——它们依赖于这些字体已经存在。这会导致一些问题,例如游戏尝试以未加载的字体渲染文本……相反,玩家将看不到任何文本,或者文本将使用备用或默认字体渲染。

在本文中,我们将探讨一些将网络字体预加载到我们的游戏中的技术,并描述如何将它们与一个流行的 2D 游戏框架集成:Phaser

网络字体加载的工作原理

有两种方法可以加载网络字体:通过 CSS(使用 @font-face)或通过 JavaScript(使用 字体加载 API)。CSS 解决方案已经存在一段时间了;而 JavaScript API 尚未被浏览器广泛采用。如果您现在想要发布游戏,我们建议您使用 CSS 方法,因为它具有可移植性。

使用 @font-face 声明

这只是在您的 CSS 代码中进行的声明,使您能够设置字体族并指向可以从中获取该字体的地址。在这个代码片段中,我们声明了一个名为 Amatica SC 的字体族,并假设我们有一个 TTF 文件作为资源。

@font-face {
  font-family: 'Amatica SC';
  font-style: normal;
  font-weight: 400;
  src: local('Amatica SC'),
       local('AmaticaSC-Regular'),
       url(fonts/amaticasc-regular.ttf) format('truetype');
}

注意:除了指向特定文件外,我们还可以指向可能安装在用户计算机中的字体名称(在这种情况下,为 Amatica SC 或 AmaticaSC-Regular)。

实际加载

重要的是要记住,通过 CSS 声明字体族不会加载字体!只有当浏览器首次检测到将要使用该字体时,才会加载字体。

这会导致视觉上的故障:文本要么使用默认字体渲染,然后更改为网络字体(这被称为 FOUT 或未样式文本闪烁);要么文本根本不渲染,并保持不可见,直到字体可用。在网站上,这通常不是什么大问题,但在游戏中(Canvas/WebGL),当字体可用时,我们不会获得自动的浏览器重新渲染!因此,如果我们尝试渲染文本而字体不可用,这就是一个大问题。

因此,我们需要在尝试在游戏中使用字体之前实际下载它……

如何强制下载网络字体

CSS 字体加载 API

JavaScript API 确实会强制加载字体。截至今天,它仅适用于 Firefox、Chrome 和 Opera(您可以在 caniuse.com 中查看最新的字体加载支持信息)。

请注意,使用FontFaceSet时,您仍然需要在某个地方声明您的字体——在这种情况下,在 CSS 中使用@font-face

Typekit 的网络字体加载器

这是一个由 TypeKit 和 Google 开发的开源加载器——您可以查看 Github 中的网络字体加载器存储库。它可以加载自托管字体,以及来自流行存储库(如 Typekit、Google Fonts 等)的字体。

在以下代码片段中,我们将直接从 Google Fonts 加载 Amatica SC,并指定一个回调函数——在 2D 画布中渲染文本——该函数将在字体加载完毕并可用于使用时调用。

FontFace Observer 库

FontFace Observer 是 另一个开源加载器,它不包含针对常见字体存储库的特定代码。如果您自托管字体,这可能比 Typekit 的更好选择,因为它是一个更轻量的文件大小。

此库使用Promise接口——但不用担心,如果您需要支持旧浏览器,还有一个带有 polyfill 的版本。在这里,您还需要通过 CSS 声明您的字体,以便库知道从哪里获取它们。

在 Phaser 中集成字体加载

现在我们已经了解了如何在 HTML5 中加载网络字体,让我们讨论一下如何将这些字体与游戏引擎集成。这个过程会因引擎或框架而异。我选择 Phaser 作为示例,因为它被广泛用于 2D 游戏开发。您可以在此处查看一些在线示例

当然,还有一个 带有完整源代码的 Github 存储库,这样您就可以更仔细地查看我创建的内容。

Phaser 的工作原理如下:游戏被划分为游戏状态,每个游戏状态都会执行一系列阶段。最重要的阶段是:initpreloadcreaterenderupdate。预加载阶段是我们必须加载游戏资源(如图像、声音等)的地方,但不幸的是,Phaser 的加载器没有提供加载字体的方法。

有几种方法可以绕过或解决此问题。

延迟字体渲染

我们可以在预加载阶段使用字体加载 API 或库来强制下载字体。但是,这会产生一个问题。Phaser 的加载器不允许我们在所有加载完成时告诉它。这意味着我们无法暂停加载器并阻止预加载阶段结束,以便我们可以切换到创建——这就是我们想要设置游戏世界的地方。

第一个方法是延迟文本渲染,直到字体加载完毕。毕竟,我们有一个在 promise 中可用的回调,对吧?

function preload() {
  // load other assets here
  // ...

  let font = new FontFaceObserver('Amatica SC');
  font.load().then(function () {
    game.add.text(0, 0, 'Lorem ipsum', {
      font: '12px Amatica SC',
      fill: '#fff'
    });
  }
}

这种方法有一个问题:如果回调在preload阶段结束之前被调用会发生什么?一旦我们切换到create,我们的 Phaser.Text 对象就会被清除。

我们可以做的是在两个标志下保护文本的创建:一个标志表示字体已加载,另一个标志表示创建阶段已开始。

var fontLoaded = false;
var gameCreated = false;

function createText() {
  if (!fontLoaded || !gameCreated) return;
  game.add.text(0, 0, 'Lorem ipsum', {
      font: '12px Amatica SC',
      fill: '#fff'
  });
}

function preload() {
  let font = new FontFaceObserver('Amatica SC');
  font.load().then(function () {
    fontLoaded = true;
    createText();
  });
}

function create() {
  gameCreated = true;
  createText();
}

这种方法的主要缺点是我们完全忽略了 Phaser 的加载器。由于这不会将字体排队作为资源,因此游戏将启动并且字体将不存在——这可能会导致闪烁效果或故障。另一个问题是,“加载”屏幕或栏将忽略字体,将显示为已加载 100%,并将切换到游戏,即使我们的字体资源尚未加载。

使用自定义加载器

如果我们可以修改 Phaser 的加载器并添加我们需要的任何内容怎么办?我们可以!我们可以扩展 Phaser.Loader 并向原型添加一个方法,该方法将排队一个资源——网络字体!问题是我们需要修改一个内部(用于私有使用)Phaser.Loader 方法loadFile,以便我们可以告诉加载器如何加载字体,以及何时加载完成。

// We create our own custom loader class extending Phaser.Loader.
// This new loader will support web fonts
function CustomLoader(game) {
    Phaser.Loader.call(this, game);
}

CustomLoader.prototype = Object.create(Phaser.Loader.prototype);
CustomLoader.prototype.constructor = CustomLoader;

// new method to load web fonts
// this follows the structure of all of the file assets loading methods
CustomLoader.prototype.webfont = function (key, fontName, overwrite) {
    if (typeof overwrite === 'undefined') { overwrite = false; }

    // here fontName will be stored in file's `url` property
    // after being added to the file list
    this.addToFileList('webfont', key, fontName);
    return this;
};

CustomLoader.prototype.loadFile = function (file) {
    Phaser.Loader.prototype.loadFile.call(this, file);

    // we need to call asyncComplete once the file has loaded
    if (file.type === 'webfont') {
        var _this = this;
        // note: file.url contains font name
        var font = new FontFaceObserver(file.url);
        font.load(null, 10000).then(function () {
            _this.asyncComplete(file);
        }, function ()  {
            _this.asyncComplete(file, 'Error loading font ' + file.url);
        });
    }
};

此代码到位后,我们需要创建一个实例并将其交换到game.load中。此交换必须尽快完成:在执行的第一个游戏状态的init阶段。


function init() {
    // swap Phaser.Loader for our custom one
    game.load = new CustomLoader(game);
}

function preload() {
    // now we can load our font like a normal asset
    game.load.webfont('fancy', 'Amatica SC');
}

这种方法的优点是真正地与加载器集成,因此如果我们有加载栏,它将不会完成,直到字体完全下载(或超时)。当然,缺点是,我们正在覆盖 Phaser 的内部方法,因此我们无法保证我们的代码在框架的未来版本中将继续工作。

一个愚蠢的解决方法……

我在游戏创作中使用的一种方法是,直到我知道字体已准备好,才启动游戏。由于大多数浏览器在网络字体加载完毕之前不会渲染文本,我只是使用网络字体创建一个带有播放按钮的启动画面……这样,我知道按钮在该字体加载完毕后将可见,因此可以安全地启动游戏。

明显的缺点是我们直到玩家按下该按钮才会开始加载资源……但这确实有效,并且非常易于实现。以下是其中一个启动画面的屏幕截图示例,它使用常规的 HTML5 DOM 元素和 CSS 动画创建

Screen Shot 2016-06-28 at 15.23.24


就是这样,在 HTML5 游戏中渲染网络字体!未来,一旦字体加载 API 更加成熟,HTML5 游戏引擎和框架将开始将其集成到其代码中,希望我们不再需要自己动手或寻找可用的解决方法。

在此之前,祝您编码愉快!:)

关于 Belén Albeza

Belén 是一位在 Mozilla 开发者关系部门工作的工程师和游戏开发者。她关心网络标准、高质量代码、可访问性和游戏开发。

更多 Belén Albeza 的文章……


一条评论

  1. CreativeSpark

    不错的解决方案 - 我希望这将在未来更新中实现到默认的 Phaser 加载程序中。

    2016 年 7 月 11 日 下午 2:07

本文的评论已关闭。