适用于 HTML5 游戏的简单素材资源管理

Seth Ladd

简介

HTML5 提供了许多实用的 API,用于在浏览器中构建现代、响应迅速且功能强大的 Web 应用。太棒了,但您确实想构建和玩游戏!幸运的是,HTML5 也开创了游戏开发的新时代,它使用画布等 API 和强大的 JavaScript 引擎,无需插件即可将游戏直接传送到浏览器中。

本文介绍了如何为 HTML5 游戏构建简单的资源管理组件。如果没有素材资源管理器,您的游戏将很难针对未知的下载时间和异步图片加载进行补偿。下面通过一个示例来了解适用于 HTML5 游戏的简单素材资源管理器。

问题

HTML5 游戏不能假设图片或音频等资源位于玩家的本地计算机上,因为 HTML5 游戏暗示在网络浏览器中玩游戏,且资源通过 HTTP 下载。由于涉及到网络,因此浏览器不确定何时可以下载和提供游戏资源。

在网络浏览器中以编程方式加载图片的基本方法是以下代码:

var image = new Image();
image.addEventListener("success", function(e) {
  // do stuff with the image
});
image.src = "/some/image.png";

现在,假设有一百张图片需要在游戏启动时加载并显示。如何知道 100 张图片全部准备好了?它们是否全部成功加载?比赛应该何时真正开始?

解决方法

让资产管理器处理资产队列,并在一切就绪后向游戏报告。素材资源管理器概括了通过网络加载素材资源的逻辑,并提供了一种查看状态的简单方法。

我们的简易资产管理器具有以下要求:

  • 将下载内容加入队列
  • 开始下载
  • 跟踪成功和失败
  • 在一切完成后发出信号
  • 资源轻松检索

正在排入队列

第一项要求是将下载内容加入队列。此设计可让您声明所需资源,而无需实际下载。例如,如果您想在配置文件中声明某个游戏关卡的所有资源,此方法会很有用。

构造函数和队列的代码如下所示:

function AssetManager() {
  this.downloadQueue = [];
}

AssetManager.prototype.queueDownload = function(path) {
    this.downloadQueue.push(path);
}

开始下载

将所有要下载的资源排入队列后,您可以要求资源管理器开始下载所有内容。

幸运的是,网络浏览器可以并行进行下载,每个主机通常最多可以有 4 个连接。加快资源下载速度的一种方法是在托管资源时使用一系列域名。例如,您可以尝试使用 assets1.example.com、 assets2.example.com 和 assets3.example.com 等文件,而不要投放 assets.example.com 中的所有文件。即使其中每一个域名只是同一网络服务器的 CNAME,网络浏览器也会将其视为单独的服务器,并会增加用于下载资源的连接数。如需详细了解此技巧,请参阅跨网域拆分组件中的“加快网站速度的最佳做法”。

我们的下载初始化方法称为 downloadAll()。我们会逐渐积累更多数据。目前,这是第一个直接开始下载的逻辑。

AssetManager.prototype.downloadAll = function() {
    for (var i = 0; i < this.downloadQueue.length; i++) {
        var path = this.downloadQueue[i];
        var img = new Image();
        var that = this;
        img.addEventListener("load", function() {
            // coming soon
        }, false);
        img.src = path;
    }
}

如以上代码所示,downloadAll() 只是遍历 downloadQueue 并创建新的 Image 对象。系统将为加载事件添加事件监听器,并设置图片的 src,以触发实际下载。

使用此方法,您就可以开始下载了。

跟踪成功和失败

另一个要求是同时跟踪成功和失败,因为遗憾的是,有些事情并非总是完美无缺。到目前为止,该代码仅跟踪已成功下载的素材资源。通过为错误事件添加事件监听器,您可以捕获成功和失败场景。

AssetManager.prototype.downloadAll = function(downloadCallback) {
  for (var i = 0; i < this.downloadQueue.length; i++) {
    var path = this.downloadQueue[i];
    var img = new Image();
    var that = this;
    img.addEventListener("load", function() {
        // coming soon
    }, false);
    img.addEventListener("error", function() {
        // coming soon
    }, false);
    img.src = path;
  }
}

我们的资产管理器需要了解我们经历了多少成功和失败,否则就永远不知道游戏何时可以开始。

首先,我们将计数器添加到构造函数中的对象中,如下所示:

function AssetManager() {
<span class="highlight">    this.successCount = 0;
    this.errorCount = 0;</span>
    this.downloadQueue = [];
}

接下来,在事件监听器中递增计数器,代码现在如下所示:

img.addEventListener("load", function() {
    <span class="highlight">that.successCount += 1;</span>
}, false);
img.addEventListener("error", function() {
    <span class="highlight">that.errorCount += 1;</span>
}, false);

资产管理器现在可同时跟踪成功加载和失败的资产。

完成时发出信号

在游戏将资源排入下载队列并要求资源管理器下载所有资源后,需要通知游戏何时下载所有资源。资源管理器可以向游戏发回信号,而无需游戏反复询问资源是否已下载。

素材资源管理器首先需要知道每项素材资源的完成时间。我们现在添加一个 isDone 方法:

AssetManager.prototype.isDone = function() {
    return (this.downloadQueue.length == this.successCount + this.errorCount);
}

通过将 successCount + errorCount 与 downloadQueue 的大小进行比较,素材资源管理器可以了解每项资源是成功完成,还是出现了某种错误。

当然,知道是否已完成这一过程只是成功的一半;资产管理者也需要对此方法进行检查。我们将在两个事件处理脚本中添加此检查,如以下代码所示:

img.addEventListener("load", function() {
    console.log(this.src + ' is loaded');
    that.successCount += 1;
    if (that.isDone()) {
        // ???
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
if (that.isDone()) {
        // ???
    }
}, false);

在计数器递增后,我们将查看这是否是队列中的最后一个资产。如果素材资源管理器确实已经下载完毕,我们该怎么做?

如果资源管理器下载完了所有资源,当然,我们会调用回调方法!让我们更改 downloadAll() 并为回调添加一个形参:

AssetManager.prototype.downloadAll = function(downloadCallback) {
    ...

我们将在事件监听器内调用 downloadCallback 方法:

img.addEventListener("load", function() {
    that.successCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);

素材资源管理器终于可以满足最后一个要求了。

轻松检索资产

一旦收到游戏可以开始的信号,游戏便会开始呈现图片。资产管理器不仅负责下载和跟踪资产,还负责将其提供给游戏。

我们的最终要求暗示某种 getAsset 方法,因此我们现在添加它:

AssetManager.prototype.getAsset = function(path) {
    return this.cache[path];
}

此缓存对象在构造函数中进行初始化,如下所示:

function AssetManager() {
    this.successCount = 0;
    this.errorCount = 0;
    this.cache = {};
    this.downloadQueue = [];
}

缓存在 downloadAll() 末尾填充,如下所示:

AssetManager.prototype.downloadAll = function(downloadCallback) {
  ...
      img.addEventListener("error", function() {
          that.errorCount += 1;
          if (that.isDone()) {
              downloadCallback();
          }
      }, false);
      img.src = path;
      <span class="highlight">this.cache[path] = img;</span>
  }
}

额外知识点:问题修复

您发现错误了吗?如上文所述,只有在触发加载或错误事件时,系统才会调用 isDone 方法。但是,如果资产管理器没有任何排队等待下载的资产,该怎么办?isDone 方法永远不会被触发,游戏也永远不会开始。

您可以通过向 downloadAll() 添加以下代码来适应此情况:

AssetManager.prototype.downloadAll = function(downloadCallback) {
    if (this.downloadQueue.length === 0) {
      downloadCallback();
  }
 ...

如果没有资源加入队列,则立即调用回调函数。错误已修复!

用法示例

在 HTML5 游戏中使用此素材资源管理器非常简单。以下是使用该库的最基本方法:

var ASSET_MANAGER = new AssetManager();

ASSET_MANAGER.queueDownload('img/earth.png');

ASSET_MANAGER.downloadAll(function() {
    var sprite = ASSET_MANAGER.getAsset('img/earth.png');
    ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});

上面的代码说明了:

  1. 创建新的素材资源管理器
  2. 将要下载的资产加入队列
  3. 使用 downloadAll() 开始下载
  4. 通过调用回调函数在资源准备就绪时发出信号
  5. 使用 getAsset() 检索资产

有待改进的方面

毫无疑问,随着您开发游戏的这一简单资产管理器,您一定会发现,它能帮助您打下坚实的基础。未来的功能可能包括:

  • 指示哪个素材资源出错了
  • 用于指示进度的回调
  • 从 File System API 中检索资源

请在下面的评论中发表改进内容、分支和代码链接。

完整源代码

此资产管理器的源代码以及从其衍生的游戏作品,根据 Apache 许可作为开源内容,可以在 Bad Aliens GitHub 帐号中找到。您可在兼容 HTML5 的浏览器中玩 Bad Aliens 游戏。该游戏是我的 Google IO 大会演讲的主题,主题为“Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development”(幻灯片视频)。

摘要

大多数游戏都有某种资产管理器,但 HTML5 游戏则需要使用资产管理器来通过网络加载资产并处理故障。本文概要介绍了一个简单的素材资源管理器,它应该易于使用,以便您在开发下一款 HTML5 游戏中进行相应调整。祝你玩得开心,并在下面的评论区告诉我们你的想法。谢谢!