Quản lý tài sản một cách đơn giản cho trò chơi HTML5

Giới thiệu

HTML5 đã cung cấp nhiều API hữu ích để xây dựng các ứng dụng web hiện đại, thích ứng và mạnh mẽ trong trình duyệt. Điều này thật tuyệt, nhưng bạn thực sự muốn tạo và chơi trò chơi! May mắn thay, HTML5 cũng đã mở ra một kỷ nguyên mới trong phát triển trò chơi, sử dụng các API như Canvas và các công cụ JavaScript mạnh mẽ để phân phối trò chơi ngay trong trình duyệt mà không cần trình bổ trợ.

Bài viết này sẽ hướng dẫn bạn tạo một thành phần Quản lý tài sản đơn giản cho trò chơi HTML5. Nếu không có trình quản lý tài sản, trò chơi của bạn sẽ khó bù đắp cho thời gian tải xuống không xác định và tải hình ảnh không đồng bộ. Hãy theo dõi để xem ví dụ về một trình quản lý tài sản đơn giản cho trò chơi HTML5.

Vấn đề

Trò chơi HTML5 không thể giả định rằng các thành phần của trò chơi như hình ảnh hoặc âm thanh sẽ có trên máy cục bộ của người chơi, vì trò chơi HTML5 ngụ ý được chơi trong trình duyệt web với các thành phần được tải xuống qua HTTP. Vì liên quan đến mạng, trình duyệt không chắc chắn thời điểm tải xuống và cung cấp tài sản cho trò chơi.

Cách cơ bản để tải hình ảnh trong trình duyệt web theo phương thức lập trình là mã sau:

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

Bây giờ, hãy tưởng tượng bạn có hàng trăm hình ảnh cần tải và hiển thị khi trò chơi khởi động. Làm cách nào để biết khi nào tất cả 100 hình ảnh đã sẵn sàng? Tất cả các tệp đó có tải thành công không? Trò chơi thực sự bắt đầu khi nào?

Giải pháp

Cho phép trình quản lý tài sản xử lý việc xếp hàng tài sản và báo cáo lại cho trò chơi khi mọi thứ đã sẵn sàng. Trình quản lý tài sản tổng quát hoá logic tải tài sản qua mạng và cung cấp một cách dễ dàng để kiểm tra trạng thái.

Trình quản lý tài sản đơn giản của chúng tôi có các yêu cầu sau:

  • thêm video vào hàng đợi tải xuống
  • bắt đầu tải xuống
  • theo dõi lượt thành công và không thành công
  • tín hiệu khi mọi thứ đã hoàn tất
  • dễ dàng truy xuất thành phần

Đang đưa vào hàng đợi

Yêu cầu đầu tiên là xếp hàng tải xuống. Thiết kế này cho phép bạn khai báo các thành phần cần thiết mà không cần tải xuống. Điều này có thể hữu ích nếu bạn muốn khai báo tất cả tài sản cho một cấp độ trò chơi trong tệp cấu hình.

Mã cho hàm khởi tạo và xếp hàng sẽ có dạng như sau:

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

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

Bắt đầu tải xuống

Sau khi thêm tất cả thành phần cần tải xuống vào hàng đợi, bạn có thể yêu cầu trình quản lý thành phần bắt đầu tải mọi thành phần xuống.

Rất may, trình duyệt web có thể tải xuống song song, thường là tối đa 4 kết nối trên mỗi máy chủ lưu trữ. Một cách để tăng tốc độ tải tài sản xuống là sử dụng một loạt tên miền để lưu trữ tài sản. Ví dụ: thay vì phân phát mọi nội dung từ assets.example.com, hãy thử sử dụng assets1.example.com, assets2.example.com, assets3.example.com, v.v. Ngay cả khi mỗi tên miền đó chỉ là một CNAME đến cùng một máy chủ web, trình duyệt web vẫn xem chúng là các máy chủ riêng biệt và tăng số lượng kết nối dùng để tải tài sản xuống. Tìm hiểu thêm về kỹ thuật này trong phần Phân tách thành phần trên các miền tại bài viết Các phương pháp hay nhất để tăng tốc trang web.

Phương thức khởi chạy tải xuống của chúng ta được gọi là downloadAll(). Chúng tôi sẽ xây dựng nó theo thời gian. Hiện tại, đây là logic đầu tiên để bắt đầu tải xuống.

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

Như bạn có thể thấy trong mã ở trên, downloadAll() chỉ lặp lại qua downloadQueue và tạo một đối tượng Image mới. Trình nghe sự kiện cho sự kiện tải được thêm và src của hình ảnh được đặt, điều này sẽ kích hoạt quá trình tải xuống thực tế.

Với phương thức này, bạn có thể bắt đầu tải xuống.

Theo dõi lượt thành công và không thành công

Một yêu cầu khác là theo dõi cả thành công và thất bại, vì rất tiếc là không phải lúc nào mọi thứ cũng diễn ra suôn sẻ. Mã này cho đến nay chỉ theo dõi các thành phần đã tải xuống thành công. Bằng cách thêm trình nghe sự kiện cho sự kiện lỗi, bạn có thể ghi lại cả trường hợp thành công và không thành công.

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

Trình quản lý tài sản của chúng ta cần biết số lần thành công và thất bại mà chúng ta gặp phải, nếu không, trình quản lý sẽ không bao giờ biết được thời điểm trò chơi có thể bắt đầu.

Trước tiên, chúng ta sẽ thêm bộ đếm vào đối tượng trong hàm khởi tạo, hiện có dạng như sau:

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

Tiếp theo, hãy tăng bộ đếm trong trình nghe sự kiện. Bây giờ, bộ đếm sẽ có dạng như sau:

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

Trình quản lý thành phần hiện đang theo dõi cả thành phần đã tải thành công và không tải được.

Báo hiệu khi hoàn tất

Sau khi trò chơi đưa các tài sản vào hàng đợi tải xuống và yêu cầu trình quản lý tài sản tải tất cả tài sản xuống, trò chơi cần được thông báo khi tất cả tài sản được tải xuống. Thay vì trò chơi liên tục hỏi xem các tài sản đã được tải xuống hay chưa, trình quản lý tài sản có thể gửi tín hiệu trở lại trò chơi.

Trước tiên, trình quản lý tài sản cần biết thời điểm mọi tài sản hoàn tất. Bây giờ, chúng ta sẽ thêm một phương thức isDone:

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

Bằng cách so sánh successCount + errorCount với kích thước của downloadQueue, trình quản lý tài sản sẽ biết liệu mọi tài sản đã hoàn tất thành công hay gặp phải lỗi nào đó.

Tất nhiên, việc biết liệu việc này đã hoàn tất hay chưa chỉ là một nửa chặng đường; trình quản lý tài sản cũng cần kiểm tra phương thức này. Chúng ta sẽ thêm bước kiểm tra này vào cả hai trình xử lý sự kiện, như mã dưới đây cho thấy:

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

Sau khi bộ đếm tăng lên, chúng ta sẽ xem đó có phải là thành phần cuối cùng trong hàng đợi hay không. Nếu trình quản lý tài sản thực sự đã tải xong, chúng ta nên làm gì?

Tất nhiên, nếu trình quản lý tài sản đã tải xong tất cả tài sản, chúng ta sẽ gọi một phương thức gọi lại! Hãy thay đổi downloadAll() và thêm một tham số cho lệnh gọi lại:

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

Chúng ta sẽ gọi phương thức downloadCallback bên trong trình nghe sự kiện:

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

Cuối cùng, trình quản lý tài sản đã sẵn sàng cho yêu cầu cuối cùng.

Dễ dàng truy xuất thành phần

Sau khi nhận được tín hiệu cho biết có thể bắt đầu, trò chơi sẽ bắt đầu kết xuất hình ảnh. Trình quản lý tài sản không chỉ chịu trách nhiệm tải xuống và theo dõi các tài sản mà còn cung cấp các tài sản đó cho trò chơi.

Yêu cầu cuối cùng của chúng ta ngụ ý một số loại phương thức getAsset, vì vậy, chúng ta sẽ thêm phương thức này ngay:

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

Đối tượng bộ nhớ đệm này được khởi tạo trong hàm khởi tạo, hiện có dạng như sau:

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

Bộ nhớ đệm được điền vào cuối downloadAll(), như minh hoạ dưới đây:

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

Phần thưởng: Sửa lỗi

Bạn có phát hiện lỗi không? Như đã viết ở trên, phương thức isDone chỉ được gọi khi sự kiện tải hoặc lỗi được kích hoạt. Nhưng nếu trình quản lý thành phần không có thành phần nào được đưa vào hàng đợi tải xuống thì sao? Phương thức isDone không bao giờ được kích hoạt và trò chơi không bao giờ bắt đầu.

Bạn có thể điều chỉnh trường hợp này bằng cách thêm mã sau vào downloadAll():

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

Nếu không có thành phần nào được đưa vào hàng đợi, lệnh gọi lại sẽ được gọi ngay lập tức. Lỗi đã được khắc phục!

Ví dụ về cách sử dụng

Việc sử dụng trình quản lý tài sản này trong trò chơi HTML5 khá đơn giản. Sau đây là cách cơ bản nhất để sử dụng thư viện:

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

Mã trên minh hoạ:

  1. Tạo trình quản lý tài sản mới
  2. Thêm các thành phần vào hàng đợi để tải xuống
  3. Bắt đầu tải xuống bằng downloadAll()
  4. Gửi tín hiệu khi các thành phần đã sẵn sàng bằng cách gọi hàm gọi lại
  5. Truy xuất thành phần bằng getAsset()

Các khía cạnh cần cải thiện

Chắc chắn bạn sẽ phát triển vượt ra khỏi trình quản lý tài sản đơn giản này khi xây dựng trò chơi, mặc dù tôi hy vọng trình quản lý này đã cung cấp cho bạn một khởi đầu cơ bản. Các tính năng trong tương lai có thể bao gồm:

  • báo hiệu thành phần nào có lỗi
  • lệnh gọi lại để cho biết tiến trình
  • truy xuất thành phần từ API Hệ thống tệp

Vui lòng đăng nội dung cải tiến, các nhánh và đường liên kết đến mã trong phần bình luận bên dưới.

Nguồn đầy đủ

Nguồn của trình quản lý tài sản này và trò chơi được trừu tượng hoá từ nguồn đó là nguồn mở theo Giấy phép Apache và có thể tìm thấy trong tài khoản GitHub của Bad Aliens. Bạn có thể chơi trò chơi Bad Aliens trong trình duyệt tương thích với HTML5. Trò chơi này là chủ đề của bài nói chuyện của tôi tại Google IO có tên Super Browser 2 Turbo HD Remix: Giới thiệu về phát triển trò chơi HTML5 (bản trình bày, video).

Tóm tắt

Hầu hết các trò chơi đều có một số loại trình quản lý tài sản, nhưng trò chơi HTML5 yêu cầu trình quản lý tài sản tải tài sản qua mạng và xử lý các lỗi. Bài viết này đã trình bày một trình quản lý tài sản đơn giản mà bạn có thể dễ dàng sử dụng và điều chỉnh cho trò chơi HTML5 tiếp theo của mình. Chúc bạn vui vẻ và đừng quên chia sẻ ý kiến của bạn trong phần bình luận bên dưới. Cảm ơn bạn!