Blog (9)
Komentarze (25)
Recenzje (2)
@lukasz.developerHTML5 w tworzeniu gier (cz. 2)

HTML5 w tworzeniu gier (cz. 2)

18.01.2011 01:37, aktualizacja: 18.01.2011 21:38

Hej,

przepraszam za dłuższą przerwę, ale ostatnio miałem trochę mniej czasu na pisanie niniejszego tutoriala. W dzisiejszej części cyklu o HTML5 i JS w tworzeniu interaktywnych treści pozostaniemy w zimowym klimacie, ale nieco przekształcimy stworzoną wcześniej aplikację w bardziej interaktywną :).

Tło

Na początku zajmiemy się graficzną stroną naszego dzieła ;). Zamiast nijakiego, jednokolorowego tła wstawimy piękny gradient :).

Wartości poszczególnych kolorów będziemy podawać w 24‑bitowym formacie #rrggbb (tzw. Hex, format heksadecymalny lub szesnastkowy), gdzie rr, gg, bb to odpowiednio czerwona, zielona i niebieska składowa koloru. Wszystkie wartości są wyrażane za pomocą dwucyfrowych liczb w systemie szesnastkowym, co umożliwia podanie wartości z przedziału [0;255].

Uwaga  Można również spotkać się z zapisem 32‑bitowym, który zawiera o dwa znaki więcej na początku, oznaczające wartość przezroczystości piksela (kanału alfa).

Jeśli nie podoba ci się wizja ręcznego rozkładania koloru piksela na kanały kolorystyczne i przeliczania je do odpowiedniego systemu liczbowego, niewątpliwie ucieszy cię informacja, że niewiele osób chciałoby to robić :P. Z tego też względu prawie każdy, nawet prosty, program graficzny umożliwia wybranie koloru z palety barw i podaje jego reprezentację w pomocnej nam postaci :).

Dla potrzeb naszego tutoriala wybrałem kolory: czarny : #000000 niebieski (no, nie do końca, ale nie czepiajmy się niuansów kolorystycznego nazewnictwa :P): #70D8FF

Dla prostych kolorów możemy również posługiwać się ich angielskimi nazwami, więc w przypadku czarnego napiszemy po prostu black.

Nasz gradient nie będzie się zmieniał, więc policzymy go tylko raz, w funkcji inicjalizującej. Najpierw dodajmy do zmiennych globalnych naszego skryptu (na początku pliku, tam, gdzie jest zdefiniowana np.: zmienna canvasSzerokosc) definicję zmiennej z gradientem:


var gradientTla;

Następnie w funkcji inicjalizujGre, w ifie sprawdzającym poprawność canvasa, dodajmy tworzenie obiektu gradientu. Metoda będzie wówczas wyglądać mniej więcej tak (z dokładnością do wielokropków, które oznaczają kod bez zmian ;)):


function inicjalizujGre(glownyCanvas)
{
	...
	/* jeśli element canvas jest obsługiwany */
	if (canvasElement.getContext)
	{
		context = canvasElement.getContext("2d");
		
		gradientTla = context.createLinearGradient(0, 0, 0, wysokoscCanvas);
		gradientTla.addColorStop(0, "black");
		gradientTla.addColorStop(1, "#70D8FF");
		
		...
		
		/* uruchom glowna petle gry */
		setInterval(glownaPetla, dlugoscKlatki);
	}
	...
}

Na koniec zmieńmy kod czyszczenia tła planszy:


function glownaPetla()
{
	/* wyczysc ekran po poprzedniej klatce */
	//context.fillStyle = "rgba(0, 0, 0, 1)";
	context.fillStyle = gradientTla;
	context.fillRect(0, 0, szerokoscCanvas, wysokoscCanvas);
...
}

Zakomentowaliśmy poprzedni kod i dodaliśmy nowy, którzy rysuje nasz gradient :).

Obrazki i rotacje

Ja zapewne zauważyliście, płatki śniegu mają trochę bardziej skomplikowane kształty niż kółka, których używaliśmy do tej pory :P. Moglibyśmy stworzyć je implementując rysowanie odpowiednio złożonego kształtu w postaci ścieżki (za pomocą poleceń moveTo i lineTo), ale my posłużymy się innym sposobem :). Narysujcie płatek śniegu w dowolnym programie graficznym i zapiszcie go jako plik platek_sniegu.png w katalogu ze skryptem. Jeśli nie chcecie rysować, możecie się posłużyć obrazkiem płatka stworzonym na szybko przeze mnie.



[image=img1]

Mamy już plik graficzny, teraz pora umieścić go w naszej "grze" :).

Zmieńmy kod w następujący sposób:


function inicjalizujGre(glownyCanvas)
{
	...

	/* jeśli element canvas jest obsługiwany */
	if (canvasElement.getContext)
	{
		context = canvasElement.getContext("2d");
		
		platekImg = new Image();
		platekImg.src = "platek_sniegu.png";
		platki[0] = new PlatekSniegu(100, 0);
		...
	}
...
}

Następnie znajdźmy w głównej pętli wcześniejszy kod rysujący płatki:


function glownaPetla()
{
...
for(var j = 0; j < platki.length; j++)
{
...
//rysuj platek
		context.fillStyle = "#fff";
		context.beginPath();
		context.arc(platki[j].x, platki[j].y, platki[j].r, 0, 2*Math.PI, true);
		context.closePath();
		context.fill();
}
...
}

i zamieńmy go na następujący:


function glownaPetla()
{
...
for(var j = 0; j < platki.length; j++)
{
...
	//rysuj platek
	context.shadowBlur = 10;
	context.shadowColor = "white";
	//oblicz skladowa pozioma polozenia platka tak, aby skalowanie wygladalo jak obrot wokol pionowej osi symetrii
	var xPlatka = platki[j].x - platki[j].aktualneR/2;
	//oblicz skladowa pionowa polozenia platka
	var yPlatka = platki[j].y - platki[j].r;
	context.drawImage(platekImg, xPlatka, yPlatka, platki[j].aktualneR, platki[j].r);
}

Przy okazji dodaliśmy też efekt rozmytego białego cienia, który sprawi, że płatki będą się lepiej prezentować na ekranie. Zauważmy też, że użyte zostało nowe pole aktualneR. Będziemy je wykorzystywać do obrotu, który w przybliżeniu ma polegać na ściskaniu i rozciąganiu obrazka z poziomie. Do obsłużenia efektu dodajmy też klasy płatka pole deltaR informujące czy obrazek należy aktualnie rozciągać (wartość 1) czy ściskać (wartość -1).

Uwaga Nazwy r i aktualneR zostały przyjęte ze względu na zmniejszenie liczby potrzebnych zmian w kodzie względem poprzedniej części tutoriala. Oznaczają one odpowiednio wysokość i szerokość obrazku płatka i nie należy ich mylić z oznaczeniem promienia.

Dodajmy więc odpowiednią inicjalizację w w metodzie tworzącej obiekty płatków:


function PlatekSniegu(startX, startY)
{
	this.x = startX;
	this.y = startY;
	this.r = 20;
	this.aktualneR = this.r;
	this.deltaR = -1;
	
	this.kierunek = 0; /* wartosc ze zbioru {-1, 0, 1} */
	this.dlugosc = 0;
}

Ze względu na zmianę znaczenia pola r, zwiększyłem również jego domyślną wartość.

Następnie w głównej pętli gry dodajmy kod zmieniający rozmiar płatka:


function glownaPetla()
{
...
for(var j = 0; j < platki.length; j++)
{
...

//obrot platka wokol osi Y

		//jesli platek został ściśnięty do szerokosci 0
		if (platki[j].aktualneR <= 0)
		{
		//zmien flage na rozciaganie
			platki[j].deltaR = 1;
		}
		//jesli platek zostal rozciagniety ponad swoj oryginalny rozmiar
		else if (platki[j].aktualneR >= platki[j].r)
		{
		//zmien flage na sciskanie
			platki[j].deltaR = -1;
		}
		//zmodyfikuj aktualna szerokosc zgodnie z ustawiona flaga
		platki[j].aktualneR += platki[j].deltaR;

		//rysuj platek
...
}
...

Zmodyfikujmy też fragment odpowiadający za tworzenie nowych płatków:


function glownaPetla()
{
...
//co 10 klatek dodaj nowy platek sniegu az do osiagniecia maksymalnej ich liczby
if (licznikKlatek++ == 10 && platki.length < liczbaPlatkowMax)
{
	//dodaj nowy platek
	var nowyPlatek = new PlatekSniegu(Math.floor(Math.random()*szerokoscCanvas), 0);
	nowyPlatek.r = Math.floor(Math.random()*rozmiarPlatkaMax) + 10;
	//losuj liczbe od 1 do r, dzieki czemu platki beda roznic sie faza "obrotu"
	nowyPlatek.aktualneR = Math.floor(Math.random()*nowyPlatek.r) + 1;
	nowyPlatek.dlugosc = Math.floor(Math.random()*dlugoscProstegoToruMax) + 2;
	platki[platki.length] = nowyPlatek;
	licznikKlatek = 0;
...
}
...

W celu lepszego dopasowania rozmiarów płatków zwiększyłem również wartość zmiennej globalnej rozmiarPlatkaMax do 30.

Reakcja na zdarzenia użytkownika (klawiatura)

W grze zwykle gracz ma jakiś wpływ na to co się dzieje, u nas jednak może póki co kibicować opadającym płatkom :P. Dlatego też, aby zwiększyć jego możliwości interakcji, dodamy obsługę klawiatury. Koncepcja jest następująca: gracz kieruje śniegową kulą, która toczy się po dolnej krawędzi planszy. Za pomocą strzałek w lewo i w prawo może zmieniać jej kierunek. Aktualnie nie będzie to miało wpływu na płatki, ale będzie już pewną interakcją. Wiedząc co chcemy oprogramować, zabierzmy się do pisania :).

Dodajmy zmienną wcisnietyKlawisz do deklaracji na początku pliku. Będzie ona przechowywała kod ostatnio wciśniętego klawisza.


var wcisnietyKlawisz = 0;

Zdefiniujmy następnie "klasę" gracza. Potrzebna nam będzie tylko jedna jej instancja, więc możemy zrobić to w nieco odmienny sposób niż w przypadku płatków śniegu. Dodajmy zatem zaprezentowaną definicję zmiennej gracz do zmiennych globalnych.


var gracz = {
				x : 10,
				y : 10, 
				r : 5, 
				szybkosc : 5
			};

Powyższy kawałek kodu przypisuje zmiennej gracz obiekt o podanych polach x, y, r oraz szybkosc. Taka metoda powoduje, że nie możemy zdefiniować kolejnych obiektów klasy takiej jak zmienna gracz, a więc mamy do czynienia z tzw. singletonem. Nie jest to jednak dla nas przeszkodą, bo gracz jest jeden :). Jeśli chcecie poczytać więcej o definiowaniu klas w JS, polecam artykuł 3 ways to define a JavaScript class.

W pętli inicjalizującej oprogramujmy następnie reakcję na wciśnięcie klawisza:


function inicjalizujGre(glownyCanvas)
{
...
	/* jeśli element canvas jest obsługiwany */
	if (canvasElement.getContext)
	{
		...
		gracz.y = wysokoscCanvas - gracz.r;
		document.onkeydown = function(e)
								{
									var e = window.event || e;
									
									wcisnietyKlawisz = e.keyCode;
								};
		
		/* uruchom glowna petle gry */
		setInterval(glownaPetla, dlugoscKlatki);
	}
...
}

To wystarczy, aby w pętli głównej posiadać informację o klawiszu ostatnio naciśniętym przez użytkownika. Zdarzeń, które możemy obsłużyć jest dużo i mogą one dotyczyć nie tylko obsługi myszki i klawiatury :). Więcej o zdarzeniach w Java Script możecie poczytać na JavaScript Events (strona w języku angielskim), na Zdarzenia i ich obsługa - Kurs języka JavaScript (artykuł po polsku :)) lub użyć Google'a. Opisów jest sporo :).

Stwórzmy też nową funkcję implementującą reakcje na obsługiwane zdarzenia:


function ruchGracza()
{
if ((wcisnietyKlawisz == 37) && (gracz.x > gracz.szybkosc))
	{
		gracz.x -= gracz.szybkosc;
	}
	else if ((wcisnietyKlawisz == 39) && (gracz.x < (szerokoscCanvas - gracz.szybkosc)))
	{
		gracz.x += gracz.szybkosc;
	}
}

i dodajmy jej wywołanie w funkcji glownaPetla() (w pętli głównej). Warto zwrócić uwagę na liczby 37 i 39, które są kodami klawiszy ze strzałkami odpowiednio w lewo i w prawo.

Pozostało jeszcze wyświetlenie samej kuli, którą kieruje gracz. Wykorzystamy do tego kod analogiczny jak w poprzedniej części do rysowania śniegowych kulek.


function glownaPetla()
{
	...
	
	ruchGracza();

	for(var j = 0; i < platki.length; j++)
	{
		...
	}
	...
	//rysuj postac gracza
	context.fillStyle = "#fff";
	context.beginPath();
	context.arc(gracz.x, gracz.y, gracz.r, 0, 2*Math.PI, true);
	context.closePath();
	context.fill();
}

W ten sposób upodobniliśmy nasz zimowy przykład do czegoś co niedługo można będzie już szumnie nazwać grą :). W kolejnej części rozszerzymy interakcję gracza tak, aby nasze demo przypominało grę, która ma określony cel ;). Pobawimy się też dźwiękiem i innymi elementami na stronie. Następna część tutoriala wkrótce :).

Pozdrawiam,

Łukasz

P.S.

Jeśli macie wybrane przez siebie funkcjonalności, o których chcielibyście przeczytać w kolejnych częściach, przyślijcie wiadomość albo napiszcie komentarz :).

Przydatne linki

Przygotowując pierwsze dwie części minitutoriala korzystałem między innymi z podanych poniżej linków. Zawierają one więcej informacji o możliwościach elementu Canvas. Jeśli chcecie poszerzyć swoją wiedzę w tym temacie, mogą one stanowić dobre źródło informacji :). Canvas 2D API Specification 1.0 Drawing Graphics with Canvas Let’s Call It A Draw(ing Surface)

A artykule ponadto pojawiły się inne przydatne linki: JavaScript Events Zdarzenia i ich obsługa - Kurs języka JavaScript Artykuł o singletonie na WIkipedii 3 ways to define a JavaScript class

Wybrane dla Ciebie
Komentarze (3)