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, phản hồi nhanh 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 xây dựng và chơi trò chơi! May mắn thay, HTML5 cũng đã mở ra một kỷ nguyên phát triển trò chơi mớ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 trực tiếp trên 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 cách xây dựng một thành phần Quản lý tài sản đơn giản cho trò chơi HTML5 của mình. Nếu không có trình quản lý nội dung, trò chơi của bạn sẽ khó có thể 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 cùng xem ví dụ về một trình quản lý tài sản đơn giản cho trò chơi HTML5 của bạn.

Vấn đề

Trò chơi HTML5 không được giả định rằng các thành phần (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ụ ý việc chơi trong một trình duyệt web có các thành phần được tải xuống qua HTTP. Do liên quan đến mạng nên trình duyệt không chắc chắn khi nào các tài sản của trò chơi sẽ được tải xuống và có sẵn.

Cách cơ bản để tải hình ảnh theo phương thức lập trình trong trình duyệt web 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ó một trăm hình ảnh cần được tải và hiển thị khi trò chơi khởi động. Làm cách nào để bạn biết khi nào toàn bộ 100 hình ảnh đã sẵn sàng? Tất cả các định dạng đó có tải thành công không? Khi nào trò chơi thực sự bắt đầu?

Giải pháp

Cho phép người quản lý tài sản xử lý việc thêm tài sản vào hàng đợi và báo cáo lại cho trò chơi khi mọi thứ đã sẵn sàng. Trình quản lý thành phần khái quát hoá logic về việc tải các thành phần qua mạng, đồng thời giúp bạn 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 tệp đã tải xuống vào danh sách chờ
  • bắt đầu tải xuống
  • theo dõi thành công và không thành công
  • tín hiệu khi mọi việc hoàn tất
  • dễ dàng truy xuất tài sản

Đang xếp hàng

Yêu cầu đầu tiên là xếp hàng các tệp đã tải xuống. Thiết kế này cho phép bạn khai báo những thành phần cần thiết mà không cần tải các thành phần đó xuống. Điều này có thể hữu ích, chẳng hạn như khi 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à hàng đợi 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 đã xếp tất cả thành phần vào hàng đợi để tải xuống, bạn có thể yêu cầu người quản lý thành phần bắt đầu tải mọi thứ xuống.

Thật may là trình duyệt web có thể tải song song các tệp tải xuống — thường có tối đa 4 kết nối trên mỗi máy chủ. Một cách để tăng tốc độ tải tài sản xuống là sử dụng nhiều 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 với cùng một máy chủ web, trình duyệt web sẽ xem chúng là các máy chủ riêng biệt và tăng số lượng kết nối được sử dụng để tải nội dung xuống. Tìm hiểu thêm về kỹ thuật này trong bài viết Phân tách các thành phần trên các miền trong 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 tạo tệp tải xuống của chúng ta có tên là downloadAll(). Chúng tôi sẽ phát triển dầ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ỉ cần lặp lại thông qua downloadQueue và tạo một đối tượng Hình ảnh mới. Trình nghe sự kiện cho sự kiện tải được thêm vào và src của hình ảnh được đặt, sẽ kích hoạt quá trình tải xuống thực tế.

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

Theo dõi 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 lẫn thất bại, vì tiếc là không phải mọi thứ đều hoạt động hoàn hảo. Hiện tại, mã này chỉ theo dõi những tài sản được 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 sẽ có thể nắm bắt được 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;
  }
}

Người quản lý tài sản cần biết chúng tôi đã gặp bao nhiêu lần thành công và thất bại, nếu không họ sẽ không bao giờ biết khi nào trò chơi có thể bắt đầu.

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

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

Tiếp theo, hãy tăng số bộ đếm trong trình nghe sự kiện 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ý tài sản hiện đang theo dõi cả tài sản tải thành công và tài sản không thành công.

Báo hiệu khi hoàn thành

Sau khi trò chơi xếp tài sản vào hàng đợi để tải xuống và yêu cầu người 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 hỏi đi hỏi lại xem tài sản có được tải xuống hay không, trình quản lý tài sản có thể gửi tín hiệu quay lại trò chơi.

Trước tiên, người 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 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ý nội dung sẽ biết được liệu mọi nội dung đã hoàn tất thành công hay có một số loại lỗi nào đó hay không.

Tất nhiên là biết được liệu đã hoàn tất hay chưa thì chỉ là một nửa trận chiến; người quản lý tài sản cũng cần kiểm tra phương pháp 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 liệu đó 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 tệp xuống thì chính xác chúng ta nên làm gì?

Nếu trình quản lý tài sản hoàn tất việc tải tất cả tài sản xuống, dĩ nhiên chúng ta sẽ gọi một phương pháp gọi lại! Hãy thay đổi downloadAll() và thêm 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);

Trình quản lý tài sản cuối cùng đã sẵn sàng đáp ứng yêu cầu cuối cùng.

Dễ dàng truy xuất tài sản

Sau khi được báo hiệu rằng có thể bắt đầu, trò chơi sẽ bắt đầu kết xuất hình ảnh. Người quản lý nội dung không chỉ chịu trách nhiệm tải xuống và theo dõi nội dung mà còn cung cấp nội dung đó 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 ngay bây giờ:

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, giờ đây sẽ có dạng như sau:

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

Bộ nhớ đệm được điền ở 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>
  }
}

Bật mí thêm cho bạn: Sửa lỗi

Bạn có phát hiện ra 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 sự kiện lỗi được kích hoạt. Nhưng nếu trình quản lý tài sản không có tài sản nào trong hàng đợi để tải xuống? 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ể phù hợp với 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ó tài sả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. Đã sửa lỗi!

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

Việc sử dụng trình quản lý nội dung 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. Đưa tài sản vào hàng đợi để tải xuống
  3. Bắt đầu tải xuống bằng downloadAll()
  4. Ra tín hiệu khi thành phần đã sẵn sàng bằng cách gọi hàm callback
  5. Truy xuất thành phần bằng getAsset()

Những khía cạnh cần cải thiện

Chắc chắn bạn sẽ không còn nghi ngờ gì khi dùng 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 nó đã mang đến một khởi đầu cơ bản. Các tính năng trong tương lai có thể bao gồm:

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

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

Nguồn đầy đủ

Nguồn cho trình quản lý tài sản này và trò chơi được tóm tắt là nguồn mở theo Giấy phép Apache và có thể tìm thấy trong tài khoản Bad Aliens GitHub. 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ủ đề cho buổi trò chuyện trên Google IO của tôi có tiêu đề Siêu trình duyệt 2 Turbo HD Remix: Giới thiệu về phát triển trò chơi HTML5 (trang 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ý nội dung, nhưng trò chơi HTML5 yêu cầu trình quản lý nội dung tải nội dung qua mạng và xử lý lỗi. Bài viết này đã trình bày một trình quản lý nội dung đơ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à vui lòng cho chúng tôi biết suy nghĩ của bạn trong phần nhận xét bên dưới. Cảm ơn bạn!