Proste zarządzanie zasobami w grach HTML5

Wstęp

HTML5 zapewnia wiele przydatnych interfejsów API do tworzenia nowoczesnych, elastycznych i zaawansowanych aplikacji internetowych w przeglądarce. To świetnie, ale naprawdę chcesz tworzyć i w nie grać. Na szczęście HTML5 rozpoczął nową erę w tworzeniu gier, w której interfejsy API takie jak Canvas i potężne mechanizmy JavaScript umożliwiają wyświetlanie gier bezpośrednio w przeglądarce bez konieczności instalowania wtyczek.

Z tego artykułu dowiesz się, jak utworzyć prosty komponent do zarządzania zasobami w grze HTML5. Jeśli nie będziesz mieć menedżera zasobów, gra będzie mieć problemy z rekompensacją nieznanego czasu pobierania i asynchronicznego wczytywania obrazów. Poniżej znajdziesz przykład prostego menedżera zasobów do wykorzystania w grach HTML5.

Problem

Gry w formacie HTML5 nie mogą zakładać, że ich zasoby, takie jak obrazy czy dźwięk, będą znajdować się na lokalnym komputerze gracza, ponieważ gry w formacie HTML5 sugerują, że gry są uruchamiane w przeglądarce z zasobami pobranymi przez HTTP. Zaangażowana jest sieć, więc przeglądarka nie jest pewna, kiedy zasoby gry zostaną pobrane i dostępne.

Podstawowy sposób programowego wczytywania obrazu w przeglądarce to następujący kod:

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

Teraz wyobraź sobie, że masz sto obrazów, które trzeba wczytać i wyświetlić po uruchomieniu gry. Skąd wiadomo, że wszystkie 100 obrazów jest gotowych? Czy wszystkie udało się załadować? Kiedy właściwie ma się rozpocząć mecz?

Rozwiązanie

Pozwól menedżerowi zasobów zająć się kolejkowaniem zasobów i raportować grę, gdy wszystko będzie gotowe. Menedżer komponentów uogólnia zasady ładowania zasobów przez sieć i ułatwia sprawdzanie ich stanu.

Nasz prosty menedżer zasobów musi spełniać następujące wymagania:

  • dodaj pobrane pliki do kolejki
  • rozpocznij pobieranie
  • śledzenie sukcesów i niepowodzeń
  • sygnał, że wszystko będzie gotowe
  • łatwe pobieranie zasobów

Umieszczam w kolejce

Pierwszym wymaganiem jest dodanie pobranych plików do kolejki. Umożliwia on deklarowanie potrzebnych zasobów bez konieczności ich pobierania. Może się to okazać przydatne, gdy na przykład chcesz zadeklarować w pliku konfiguracji wszystkie zasoby związane z poziomem gry.

Kod konstruktora i kolejkowania wygląda tak:

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

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

Rozpocznij pobieranie

Gdy wszystkie zasoby są już umieszczone w kolejce do pobrania, możesz poprosić menedżera zasobów, by wszystko zaczął pobierać.

Przeglądarka może równolegle pobrać pliki – zwykle jest to maksymalnie 4 połączenia na hosta. Jednym ze sposobów przyspieszenia pobierania zasobów jest użycie różnych nazw domen na potrzeby hostingu zasobów. Na przykład zamiast wyświetlać wszystko, co znajduje się w witrynie zasoby.example.com, spróbuj użyć zasobów1.example.com, zasobów2.example.com, zasobów3.example.com itd. Nawet jeśli każda z tych nazw domen jest po prostu rekordem CNAME tego samego serwera WWW, przeglądarka traktuje je jako oddzielne serwery i zwiększa liczbę połączeń używanych do pobierania zasobów. Więcej informacji o tej metodzie znajdziesz w artykule o dzieleniu komponentów między domenami w artykule o sprawdzonych metodach przyspieszania działania witryny.

Nasza metoda inicjowania pobierania nosi nazwę downloadAll(). Z czasem będziemy ją rozbudowywać. Na razie jest to pierwsza logika, która powoduje rozpoczęcie pobierania.

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

Jak widać w kodzie powyżej, downloadAll() po prostu powtarza iteracje w kolejce pobierania i tworzy nowy obiekt Image. Zostanie dodany detektor zdarzeń wczytywania i ustawiona jest wartość src obrazu, która uruchamia właściwe pobieranie.

W ten sposób możesz rozpocząć pobieranie.

Śledzenie sukcesów i niepowodzeń

Kolejnym wymaganiem jest śledzenie zarówno sukcesów, jak i porażek, ponieważ niestety nie zawsze wszystko działa idealnie. Obecnie kod śledzi tylko pobrane zasoby. Dodanie odbiornika zdarzenia błędu pozwoli Ci rejestrować zarówno scenariusze powodzenia, jak i niepowodzenia.

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

Nasz menedżer zasobów potrzebuje informacji o liczbie naszych sukcesów i niepowodzeń, ponieważ w przeciwnym razie nigdy nie dowie się, kiedy gra się rozpocznie.

Na początek do obiektu dodajemy w konstruktorze liczniki, które teraz wyglądają tak:

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

Następnie zwiększ liczbę liczników w detektorach zdarzeń, co teraz wygląda tak:

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

Menedżer komponentów śledzi teraz zarówno udane, jak i nieudane zasoby.

Po zakończeniu sygnalizacji

Gdy gra umieści zasoby w kolejce do pobrania i poprosi menedżera zasobów o pobranie wszystkich zasobów, musi otrzymać powiadomienie o pobraniu wszystkich zasobów. Zamiast za każdym razem, gdy gra będzie pytać o pobranie zasobów, menedżer zasobów może przekazać grze ponownie.

Menedżer zasobów musi wiedzieć, kiedy dany zasób jest gotowy. Teraz dodamy metodę isDone:

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

Porównując wartości successCount + errorCount z rozmiarem kolejki pobierania, menedżer zasobów wie, czy wszystkie zasoby zakończyły się powodzeniem, czy też nie wystąpiły jakieś błędy.

Oczywiście wiedza o tym, czy proces się zakończy, to tylko połowa sukcesu – menedżer zasobów musi również sprawdzić tę metodę. Dodamy ten test do obu modułów obsługi zdarzeń w takiej postaci:

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

Po zwiększeniu liczby liczników sprawdzimy, czy to był ostatni zasób w kolejce. Jeśli menedżer zasobów rzeczywiście zakończył pobieranie, co dokładnie zrobić?

Gdy menedżer zasobów skończy pobierać wszystkie zasoby, oczywiście wywołamy metodę wywołania zwrotnego. Zmodyfikujmy parametr downloadAll() i dodajmy parametr wywołania zwrotnego:

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

W naszych detektorach zdarzeń wywołamy metodę 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);

Menedżer zasobów jest gotowy na spełnienie ostatniego wymogu.

Łatwe pobieranie zasobów

Gdy sygnalizuje, że gra może się rozpocząć, zacznie ona renderować obrazy. Menedżer zasobów odpowiada nie tylko za pobieranie i śledzenie zasobów, ale także za dostarczanie ich do gry.

Ostatni wymóg zakłada użycie metody getAsset, więc dodamy ją teraz:

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

Obiekt pamięci podręcznej został zainicjowany w konstruktorze, który teraz wygląda tak:

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

Pamięć podręczna jest zapełniana na końcu downloadAll() w ten sposób:

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

Dodatkowo: poprawione błędy

Czy udało Ci się zauważyć błąd? Jak wspomnieliśmy powyżej, metoda isDone jest wywoływana tylko po wywołaniu zdarzeń wczytywania lub błędu. Co jednak, jeśli w menedżerze zasobów nie ma żadnych zasobów oczekujących na pobranie? Metoda isDone nigdy nie jest wywoływana, a gra nigdy się nie uruchamia.

Aby dostosować się do tej sytuacji, dodaj do downloadAll() ten kod:

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

Jeśli żadne zasoby nie znajdują się w kolejce, wywołanie zwrotne jest wywoływane natychmiast. Błąd został naprawiony.

Przykład użycia

Korzystanie z tego menedżera zasobów w grze HTML5 jest całkiem proste. Oto najprostszy sposób korzystania z biblioteki:

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

Powyższy kod ilustruje:

  1. Tworzy nowego menedżera zasobów
  2. Dodaj zasoby do kolejki do pobrania
  3. Rozpocznij pobieranie w aplikacji downloadAll()
  4. Sygnalizuje gotowość zasobów, wywołując funkcję wywołania zwrotnego
  5. Pobierz zasoby za pomocą usługi getAsset()

Obszary do poprawy

Z pewnością zabraknie Ci tego prostego menedżera zasobów podczas tworzenia gry. Mam nadzieję, że był to dobry początek. Możliwe funkcje w przyszłości:

  • sygnalizowanie, w którym zasobie wystąpił błąd
  • wywołań zwrotnych wskazujących postęp.
  • pobieranie zasobów z interfejsu File System API

Publikujcie poprawki, rozwidlenia i linki do kodu w komentarzach poniżej.

Pełne źródło

Źródło tego menedżera zasobów i gra, z której on pochodzi, to oprogramowanie open source objęte licencją Apache i dostępne na koncie Bad Aliens na GitHubie. Grę Bad Aliens można grać w przeglądarce zgodnej z HTML5. Ta gra była tematem mojej prezentacji Google IO zatytułowanej Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (slides, video).

Podsumowanie

Większość gier ma swojego menedżera zasobów, ale gry w formacie HTML5 wymagają menedżera zasobów, który wczytuje zasoby przez sieć i obsługuje awarie. W tym artykule opisujemy prostego menedżera zasobów, który powinien być łatwy w użyciu i dostosowaniu do następnej gry w HTML5. Baw się dobrze i podziel się z nami swoją opinią w komentarzach poniżej. Dziękujemy!