Irysy Tworzą Internet #3 – Gra w canvas – Piramida Ramzesa

Ten wpis jest krótkim tutorialem do napisania gry w JavaScript z pomocą html’owego elementu <canvas>. Czym jest canvas? Canvas, to jak sama nazwa wskazuje, “płótno” na którym możemy umieszczać różne grafiki. Do “komunikacji” z nim (rysowania na nim) będziemy używać funkcji z JavaScript. Zakładam podstawową znajomość html i js czytelnika. W ramach poradnika będę opisywał grę, którą nazwałem Piramida Ramzesa – kiedyś napisałem ją w Javie, a teraz przepisuję do JavaScript. Gra polega na przesuwaniu pudeł w labiryncie tak aby dotrzeć do skarbu.

Demo tutaj: https://jsfiddle.net/jansier/67523o4v/
Cały kod źródłowy Piramidy-Ramzesa jest dostępny na githubie: https://github.com/jansier/

A więc do dzieła! Na początku musimy stworzyć stronę html, na której będziemy wszystko wyświetlać.

Powyższy kod umieszczamy w pliku index.html w katalogu głównym projektu. Linijka 10 ze znacznikiem <canvas> tworzy płótno o id=“canvas” szerokości i wysokości 800 pikseli. To na nim będziemy wyświetlać nasz stan gry. W linijce niżej dołączamy plik pr.js, który znajduje się w tym samym katalogu co nasza strona. W pr.js będzie opisane całe zachowanie gry i wywoływane będą funkcje rysujące.

Przejdźmy teraz do nowego pliku o nazwie pr.js. Żeby przetestować czy wszystko działa, zamalujemy całe nasze płótno na złoty kolor.

W pierwszym wierszu pobieramy element html <canvas> do zmiennej canvas. Canvas będzie teraz dla nas obiektem w js z wieloma dostępnymi funkcjami (API tutaj https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). W drugim wierszu pobieramy z płótna dwuwymiarowy kontekst – oznacza to, że za pomocą obiektu ctx będziemy mogli rysować w 2d. Następnie ustalamy szerokość i wysokość naszego płótna. Ciekawe funkcje pojawiają się w 6 i 7 wierszu. Najpierw za pomocą fillStyle ustalamy kolor jakim będziemy malować. Następnie do fillRect przekazujemy cztery argumenty – współrzędne lewego górnego rogu prostokąta, który będziemy malować i jego szerokość i wysokość. W efekcie powinniśmy zamalować dokładnie całe nasze płótno na złoto. Poprawny efekt:

Rysowanie stanu gry

Jeśli już wszystko działa możemy w końcu przejść do pisania gry. Na początku zastanówmy się jak chcemy reprezentować mapę, po której będzie poruszać się nasz bohater. Będziemy musieli rozróżniać jakoś pola puste, od tych w których stoją skrzynki. Pierwszy pomysł jaki przychodzi mi do głowy to tablica kwadratowa, która w poszczególnych komórkach przechowuje wartość oznaczającą pole puste lub skrzynkę. Ale to nie wystarczy – musimy też rozróżniać pola gdzie stoi nasz bohater, pole ze skarbem i ściany których nie da się poruszyć (będą one stanowiły mur wokół labiryntu). Napiszmy to w pliku pr.js

Na początku mamy wszystkie potrzebne definicje id – tak aby w logice programu nie musieć zastanawiać się czy ścianę oznaczyliśmy 2 czy 3, ale odwoływać się do wallId. A następnie tworzymy przykładowy poziom. Widać, że reprezentuje on planszę ze ścianą na około, postacią w lewym górnym rogu i skarbem w prawym dolnym 😉 . W środku znajdują się też jakieś skrzynki i oczywiście też pola puste.

Gdy jesteśmy już w stanie przechowywać labirynt w pamięci, napiszmy odpowiednie funkcje do jego wyświetlania. Np. funkcja rysująca brązową skrzynkę może wyglądać tak:

Tutaj pojawia się nowa funkcja z canvas – strokeRect. Działa podobnie jak fillRect z tą różnicą, że rysuje jedynie kontur figury.

Patrząc na nasz poziom, to mogłaby być ona wywołana np. z x=2, y=3 (bo levelArray[3][2]=2, (2=skrzynka), gdzie y określa wiersz w tabeli, x kolumnę). Jednak ta funkcja jest w tej chwili dość słabo napisana – rysujemy coś o wymiarach 30px x 30px z odpowiednim przesunięciem od lewego górnego rogu. A co jeśli później zmienimy zdanie co do wymiarów i będziemy chcieli przeskalować naszą grę? W tym celu wprowadzimy dwie dodatkowe zmienne. cells – będzie określać jakie wymiary ma nasza siatka na płótnie (ile kratek może się maksymalnie zmieścić), cellPx – będzie określać szerokość pojedynczej komórki na siatce (czyli np. skrzyni, pola pustego).

Powiedzmy, że zarezerwujemy miejsce dla 20 komórek na płótnie, nasz kod możemy zmienić następująco:

Teraz jeśli np. zmienią się wymiary naszego płótna (zmienna h – jego wysokość) to gra automatycznie się wraz z nim przeskaluje. Analogicznie jeśli stwierdzimy, że nasze poziomy będą bardzo duże i potrzebujemy płótna o szerokości 30 kratek, wszystkie obiekty się zmniejszą, tak aby idealnie się zmieścić.

W podobny sposób tworzymy funkcje dla pozostałych obiektów, czyli ściany, bohatera i skarbu (w miejscu pola pustego po prostu nie będziemy niczego rysować)

Pojawiły się tutaj nowe funkcje – beginPath, arc, fill, closePath. W uproszczeniu, taka sekwencja tych metod pozwala nam rysować koła – w grze bohater i skarb będą kółkami odpowiednio niebieskiego i żółtego koloru.

Mamy już wszystkie elementarne funkcje rysowania – pozostało nam teraz napisać funkcję paintMap(), która je wszystkie połączy, tak aby wyświetlić cały labirynt, i funkcję paint(), która wyczyści całe płótno a następnie wywoła paintMap().

Potrzebujemy podwójnej pętli, tak aby x i y przeszły po wszystkich komórkach tablicy. W środku używamy instrukcji switch, aby dla komórki wywołać odpowiednią funkcję – gdy w tablicy trafimy na wartość 1 oznacza to, że chcemy w danej kratce (x, y) narysować bohatera (analogicznie dla pozostałych obiektów). Zwróć uwagę, że odwołując się do komórek tablicy y podajemy jako pierwszą współrzędną a x jako drugą, może to być na początku mylące, ale tak jak już wcześniej pisałem pierwsza współrzędna w tablicy określa dla nas numer wiersza a druga kolumny.

Na koniec wywołujemy paintMap(), żeby narysować stan gry. Uff, w kóncu możemy oglądać efekt 😀

Sterowanie

Teraz pozostaje tylko dodać sterowanie. Przemieszczać będziemy się za pomocą przycisków strzałek na klawiaturze. Będziemy potrzebować jakiegoś narzędzia – “nasłuchiwacza”, który zareaguje gdy naciśniemy przycisk na klawiaturze i wywoła odpowiednią funkcję przemieszczania. Chcemy w jakiś sposób dowiadywać się czy nasz gracz nacisnął jakąś strzałkę. Tutaj z pomocą przychodzi nam EventListener – umożliwia on nam przechwytywanie niektórych zdarzeń – jak np. ruch myszą, albo naciśnięcie jakiegoś przycisku – i uruchamianie odpowiedniej funkcji odpowiadającej na zdarzenie.

Przeanalizujmy najpierw poniższy kod:

Pojawia się najpierw sporo nowych zmiennych. Wszystkim przyciskom na klawiaturze odpowiadają pewne kody, dlatego najpierw tworzę zmienne pamiętające jakie kody oznaczają przyciski strzałek. Zmienna levelSize będzie przechowywać informację o wymiarze labiryntu – przyjmujemy na razie, że jest on kwadratowy. Następnie heroX i heroY przechowują informację o aktualnej pozycji bohatera. I w końcu dochodzimy do miejsca, w którym dodajemy EventListener – będzie on reagował na zdarzenie ‘keydown‘ czyli naciśnięcie przycisku na klawiaturze. Dzięki:

pobieramy kod przycisku, który został naciśnięty i w następnej linijce przyrównujemy go do kodu strzałki w dół (zmienna keyDown). Naciśnięcie strzałki w dół odpowiada zwiększeniu heroY o 1 i pozostawieniu heroX bez zmian, tak więc najpierw sprawdzamy czy bohater nie wyszedł poza planszę, a następnie czy pole, na które chce wejść jest puste. Jeśli tak to przechodzimy na nie. Ciekawszy jest natomiast drugi if – właściwie fundament naszej gry – sprawdzamy czy pole, na które chce wejść bohater jest zajęte przez skrzynkę boxId. Jeśli tak, to sprawdzamy czy pole za skrzynką jest puste i przesuwamy skrzynkę na to miejsce, a bohatera na poprzednie miejsce skrzynki. Dzięki temu mechanizmowi i temu, że bohater nie jest w stanie przesunąć dwóch skrzynek naraz, możemy stworzyć na prawdę ciekawe i trudne łamigłówki – im większa mapa i gęściej napakowane skrzynki tym lepsza zabawa 🙂

Nie możemy zapomnieć, aby po wykonaniu ruchu przemalować cały labityrn za pomocą paint() – inaczej nie zobaczylibyśmy żadnego efektu.

Udało się! Bohater potrafi już przemieszczać się w dół. Musimy dodać jeszcze przemieszczanie w resztę kierunków i reagowanie na dojście do skarbu. Ale nasz kod już teraz staje się dość nieczytelny i jeśli będzie go ponad cztery razy tyle to już w niczym się nie połapiemy. Dlatego zmienimy trochę podejście i nie będziemy myśleć o każdym ruchu osobno, tylko postaramy się obsłużyć je wszystkie naraz. Jak to zrobić?

Na początku dodamy funkcję z której często będziemy korzystać – onMap. Sprawdza ona czy dane położenie (x, y) nie wykracza poza tablicę – czy bohater nie próbuje wyjść, albo wysunąć skrzynię poza mapę. Następnie dodajemy cztery nowe zmienne – nHeroX i nHeroY (n – new) oznaczają pole, na które zaraz będzie chciał przejść bohater. dHeroX (d – double), dHeroY to pole, na które potencjalnie będziemy przesuwać skrzynię – czyli pole z tak jakby podwójnym ruchem. Najpierw w switch wyliczamy te zmienne, a następnie pojawia się sporo if‘ów. Sprawdzamy czy pole, na które chcemy przejść jest na mapie i czy nie wchodzimy w ścianę, następnie czy to pole jest puste, lub czy jest to pole ze skarbem. Jeśli weszliśmy na skarb to ustawiamy zmienną won na true – oznacza to, że wygraliśmy grę. Ostatni if sprawdza czy na polu, na które chcemy przejść stoi skrzynia i czy na polu z “podwójnym ruchem” – czyli polu za skrzynią – nic nie ma. Jeśli tak to przesuwamy bohatera i skrzynię.

Bohater porusza się już we wszystkich kierunkach! Gra jest już prawie gotowa – musimy tylko dodać jakąś reakcję na zwycięstwo i lepszą inicjalizację niektórych zmiennych, tak aby łatwo było dodawać kolejne poziomy. Na razie, aby dodatkowo nie komplikować działania, dodajmy alert przy rysowaniu mapy.

Dodajmy również funkcję init, która ustawi levelArray na odpowiedni poziom, levelSize na rozmiar tego poziomu i odnajdzie współrzędne heroX i heroY na mapie.

Teraz będziemy mogli zmieniać poziom modyfikując tylko jedną linijkę – zmieniając levelArray na np. level2. Dokładny i poprawiony kod wraz z dwoma dodatkowymi levelami znajdziesz w odnośnikach na początku artykułu. W kodzie gry zakładaliśmy ciągle, że nasz labirynt jest kwadratem, ale bez problemu możemy stworzyć poziom o dowolnym kształcie jak np. ten poniżej (level3). Wystarczy, że odpowiednio porozstawiamy ściany w tablicy.

Zachęcam wszystkich do przejścia przykładowych poziomów – level2 (poniżej) nie jest taki trywialny.

Zauważyłeś, że labirynt wyświetla się teraz na środku płótna? Spróbuj sam zmodyfikować kod, aby właśnie w ten sposób rysował grę. Kod z tą modyfikacją jest w odnośnikach. Czekam na komentarze z waszymi własnymi poziomami!

Diamentowy Konkurs

Aby otrzymać Diamenty za wykonanie tego poradnika, wypełnij poniższy formularz.

Form could not be loaded. Contact the site administrator.

 

Ten artykuł jest trzecim wpisem z serii Irysy Tworzą Internet.

Jan S

Jan Sierpina

Programista back-end, zainteresowany informatyką jako nauką - algorytmiką, AI. Gracz szachów i bilarda.

Like
Like Love Haha Wow Sad Angry
1

4 Replies to “Irysy Tworzą Internet #3 – Gra w canvas – Piramida Ramzesa”

  1. Bardzo fajny poradnik, czekam na więcej 🙂

  2. Bardzo fajna gierka. Oby więcej takich !!!

  3. Świetny poradnik! Myślę, że najbliższy wieczór spędzę projektując nowe labirynty, ulepszając przy tym grę o nowe bajery 😉

Dodaj komentarz