HTML5 Canvas Game: Animowane tło

A starry background in space used for scrolling

Tak jak obiecałem jakiś czas temu, wreszcie skończyłem Galaxian Style HTML5 game i jestem gotowy do napisania tutoriala o tym, składającego się z pięciu części. Gra została napisana w całości od zera, bez użycia zewnętrznych bibliotek więc będziesz mógł zobaczyć jak stworzyć działającą grę w canvas. Więc bez dalszych wstępów, zaczynajmy!

Poziom trudności: Średni
Języki programowania: HTML5, JavaScript
Kod: https://github.com/straker/galaxian-canvas-game/tree/master/part1
Tłumaczenie: Kamil “Eluzive” Pyszczek

Zanim zaczniemy ten tutorial chciałbym pokazać Ci finalny produkt. Pewnie już się zaczęło, więc kliknij “Restart” żeby zacząć od początku. Upewnij się, że kliknąłeś na obszar gry jeśli sterowanie nie działa (sprawdź jak długo wytrzymasz i dodaj wynik poniżej :) ).

Sterowanie: Poruszanie się – strzałki (↓→)
Strzelanie – Spacja

Jak pewnie zauważysz jest tu jeszcze troche do zrobienia ale zajmiemy się tym w części piątej. Naszym głównym celem jest sprawienie, że gra będzie działać poprawnie w 60FPS.

Teraz kiedy już wiesz co mamy zamiar zrobić, zabierzmy się za tutorial.

Pierwszym krokiem do napisania naszej gry w HTML5 jest zrozumienie jak to działa. Zacznijmy od powiedzenia kilku rzeczy o canvas i jak to powinno prosperować abyśmy otrzymali jak najlepsze wyniki.

Pierwszą rzeczą do zapamiętania jest fakt, że rysowanie na canvas jest kosztowną operacją. Dlatego postaramy się zmniejszyć ilość rysowania aby jak najbardziej zoptymalizować naszą aplikację. Jest na to wiele, sposobów ale my skupimy się na paru.

Na potrzeby tego kursu będziemy używać techniki wielu canvasów na stronie do narysowania wszystkich fragmentów naszej gry. Zyskamy na tym sporo wydajności. Będziemy używać ich trzech jako warstw, jednej głównej i reszty aby sprawić wrażenie jednolitej gry. Wyjaśnię czemu akurat trzech warstw w późniejszej części kursu. Teraz natomiast użyjemy jednej aby narysować „uciekające tło” czyli animacje ciągłego poruszania się tła z wykorzystaniem jednego obrazka.

Kolejną rzeczą do zapamiętania o HTML5 jest fakt, że słaby kod w JavaScript może sprawić wiele problemów. Słaba jakość kodu to normalnie i tak nic dobrego ale w sieci ma to jeszcze większe znaczenie. Dlatego użyjemy kilku zaawansowanych wzorców i technik aby nasz kod był tak szybki jak to tylko możliwe.

Teraz kiedy już to wszystko wiemy możemy zabrać się do kodowania, w tej części skupimy się na samej stronie i animacji tła.

Na początku zacznijmy z naszą stroną.

<!DOCTYPE html>
<html>
<head>
<title>Space Shooter Demo</title>
<style>
canvas {
position: absolute;
top: 0px;
left: 0px;
background: transparent;
}
</style>
</head>
<body onload="init()">
<!-- Canvas dla animomwanego tła -->
<canvas id="background" width="600" height="360">
Twoja przegladarka nie wspiera elementu canvas prosimy sprobowac z inna.
</canvas>
<script src="space_shooter_part_one.js"></script>
</body>
</html>

Jak widzimy jest to bardzo proste. Tworzymy obiekt canvas i wrzucamy do środka jakiś tekst w przypadku gdyby przeglądarka nie obsługiwała tej technologii. Dzięki CSS wszystkie canvasy będą na sobie. Na koniec ładujemy skrypt JavaScript.

Zaczniemy od stworzenia obiektu który będzie trzymał wszystkie zdjęcia w grze. Jest to dobra technika, gdyż możemy używać obiektu jednego zdjęcia kilka razy nie musząc go definiować za każdym pojedynczym razem.

/**
* Definiowanie obiektu który będzie trzymał wszystkie zdjęcia
* potrzebne do naszej gry, będą one tworzone i wczytywane
* tylko raz.
*/
var imageRepository = new function() {
// Definiowanie zdjec
this.background = new Image();

// Ustawienie sciezek do obrazkow
this.background.src = "imgs/bg.png";
}

Teraz skupimy się na animacji tła. Stworzyliśmy już obiekt który będzie trzymał nasze obrazki i ustawiliśmy mu ścieżkę dostępu do nich. Od teraz możemy się odwoływać do zbioru obrazków, za każdym razem kiedy będziemy jakiegoś potrzebować.

Następnym co zdefiniujemy będzie klasa Drawable.

/**
* Stworzenie klasy Drawable ktora bedzie podstawowa dla wszystkich
* rysowanych obiektow w grze. Ustawianie domyslnych wartosci do
* Zmiennych ktore wszystkie klasy potomne odziedzicza, tak jak i
* podstawowe funkcje
*/
function Drawable() {
this.init = function(x, y) {
// Domyslne zmienne
this.x = x;
this.y = y;
}

this.speed = 0;
this.canvasWidth = 0;
this.canvasHeight = 0;

// Definicja funkcji abstrakcyjnej która zostanie zaimplementowana w klasach potomnych
this.draw = function() {
};
}

lasa ta jest specjalna ze względu na to, że to z niej będą dziedziczyć inne klasy w grze. W terminologii programistów nazywamy to klasą abstrakcyją. Klasa taka pozwala nam na jednorazowe zdefiniowanie wszystkich zmiennych i funkcji z jakich będziemy korzystać i daje nam dostęp do nich we wszystkich klasach które będą od niej dziedziczyć bez potrzeby powielania kodu. Dodatkowo jeśli dokonamy kiedyś jakichś zmian, musimy je zastosować jedynie w klasie Drawable bez potrzeby edycji każdego pojedynczego obiektu w grze.

Klasa Drawable posiada metodę init() która pozwala nam ustawić pozycję elementu w naszym canasie, a także prędkość obiektu (ile pikseli pokona w trakcie jednej klatki), oraz szerokość i wysokość naszego canvasa. Ostatnia metoda, draw(), nie wymagała definiowania, gdyż jest zwyczajnie pusta, ale zrobiłem to mimo wszystko, będzie mi to przypominać, że jest to klasa abstrakcyjna i nie powinno się od niej tworzyć obiektów.

Teraz kiedy mamy klase podstawową, czas stworzyć klase tła która będzie odpowiedzialna za przesuwające się zdjęcie.

/**
* Stworzenie klasy Background ktora jest dzieckiem klasy Drawable
* Rysowana jest na canvasie “background” i stwarza ilzuje ruchu
* dzieki animacji obrazka.
*/
function Background() {
this.speed = 1; // Przedefiniowanie predkosci animacji dla tla

// Implementacja funkcji abstrakcyjnej
this.draw = function() {
// “Ucieczka” tłą
this.y += this.speed;
this.context.drawImage(imageRepository.background, this.x, this.y);

// Narysowanie innego obrazka nad pierwszym co daje wrazenie nieskonczonosci
this.context.drawImage(imageRepository.background, this.x, this.y - this.canvasHeight);

// Jesli obrazek wyjedzie poza ekran, reset
if (this.y >= this.canvasHeight)
this.y = 0;
};
}
// Ustawienie dziedziczenia Background z Drawable
Background.prototype = new Drawable();

Klasa Background ustawia prędkość animacji na 1px na klatkę, W metodzie draw() aktualizujemy pozycje Y tła (nasze „ucieka” z góry do dołu) a następnie rysujemy je na canvasie. Oprócz tego rysujemy taki sam obrazek powyżej pierwszego aby uzyskać efekt nieskończoności animacji. Na koniec funkcja sprawdza pozycje Y obrazka i w odpowiednim momencie ją resetuje tak, żeby sama animacja mogła się zapętlać i trwać nadal.

Aby ustawić dziedziczenie obiektu Background z Drawable musimy posłużyć się funkcją prototype. Na początku może być lekko myląca ale nie jest trudna do pojęcia. Zasadniczo to mówimy Background aby skopiowało wszystko z Drawable. Tak właśnie dziedziczymy w JavaScript.

Podstawowa struktura gry kompletna więc czas aby napisać klase gry która będzie trzymała całą jej zwartość.

/**
* Stworzenie klasy Game która będzie przechowywała wszystkie obiekty dla naszej gry
*/
function Game() {
/**
* Pobranie informacji o canvasie i wstawienie jej do obiektów gry
* Zwraca true jeśli canvas jest wspierany lub false w przeciwnym razie
* Zatrzymuje skrypt animacji dla przeglądarek które nie wspirają canvas
*/
this.init = function() {
// Pobranie elementu canvas
this.bgCanvas = document.getElementById('background');

// Sprawdzanie wsparcia dla canvas
if (this.bgCanvas.getContext) {
this.bgContext = this.bgCanvas.getContext('2d');

// Wstawienie do obiektów informacji o ich canvasach
Background.prototype.context = this.bgContext;
Background.prototype.canvasWidth = this.bgCanvas.width;
Background.prototype.canvasHeight = this.bgCanvas.height;

// Inicjalizacja obiektu background
this.background = new Background();
this.background.init(0,0); // Set draw point to 0,0
return true;
} else {
return false;
}
};

// Rozpoczecie petli animacji
this.start = function() {
animate();
};
}

Klasa Game posiada tylko dwie metody. init() na początku zbiera wszystkie elementy canvas ze strony. Następnie sprawdza czy obiekt canvas jest wspierany poprzez szukanie funkcji canvas.getContext(). Metoda zwraca prawdę jeśli przeglądarka wspiera technologie canvas lub fałsz gdy tego nie robi.

Game wstawia dane o canvasie do klasy Background, teraz Background wie którego canvasa ma użyć a takżę jakie ma on wymary. Następnie tworzymy obiekt background i ustawiamy mu początkowe koordynaty. Wreszcie, Game zwraca prawdę, gdyż canvas rzeczywiście jest wspierany i gra może być kontynuowana.

Metoda start() zaczyna pętle animacji, jest wywołana tylko jeśli init() zwróci prawdę. Jeśli canvas nie jest wspierany, starsze przeglądarki nigdy jej nie wywołają marnując cenne zasoby komputera które mogłyby być wykorzystane do innych celów.

Kiedy klasa Game jest już skończona nie pozostaje nam nic poza napisaniem pętli do animacji.

/**
* Petla animacji. Wywoluje requestAnimFrame dla optymalizacji gry i narysowania wszystkich
* elementow
* Funkcja musi byc globalna i nie moze byc zwiazana z jakimkolwiek obiektem
*/
function animate() {
requestAnimFrame( animate );
game.background.draw();
}

/**
* requestAnim shim layer by Paul Irish
* Znalezienie pierwszego API ktore zadziala aby zoptymalizowac petle animacji
* W przypadku niepowodzenia uzyc setTimeout()
*/
window.requestAnimFrame = (function(){
return window.requestAnimationFrame   ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback, /* DOMElement */ element){
window.setTimeout(callback, 1000 / 60);
};
})();

Funkcja animate() jest pętlą naszej gry która rysuje tło. Do animacji gry wykorzystujemy requestAnimFrame shim autorstwa Paul’a Irish. Pętla gry nie powinna być stworzona z pomocą setTimeout() gdyż nie jest ona przystosowana do 60FPS. Nowoczesne przeglądarki posiadają własne, wysoce zoptymalizowane pętle do gier. setTimeout() zostanie użyty dopiero wtedy kiedy nic innego nie zadziała.

Ostatni kawałek kodu tworzy obiekt game i rozpoczyna gre.

/**
* Zainicjowanie gry i rozpoczecie jej
*/
var game = new Game();

function init() {
if(game.init())
game.start();
}

I to jest to! Powinieneś teraz mieć panoramowanie gwiaździste tło na płótnie.

Obiekt canvas w HTML5 ma wielki potencjał na rynku gier. Ale aby tworzyć gry przy jego pomocy, najpierw musimy poznać słabości jego jak i samych przeglądarek aby odpowiednio rozplanować naszą grę. Kiedy mamy to już za sobą możemy zacząć pisać podstawy naszej gry korzystając z tego czego się tu nauczyliśmy aby uniknąć wszelkich pułapek w kodzie.