Pong. Pisanie prostych gier 2D w języku JavaScript


Cykl artykułów o grze ChessMemory cieszył się pewnym zainteresowaniem, lecz postanowiłem nie kontynuować rozwoju tej gry podczas tegorocznej edycji studiów podyplomowych (http://eti.pg.edu.pl/katedra-architektury-systemow-komputerowych/studia-podyplomowe/). Podczas jednego ze zjazdów napisaliśmy za to całkiem nową, równie prostą grę: Pong. Zapraszam do lektury artykułu, w którym pokażę, jak zrealizowaliśmy to ciekawe zadanie przy pomocy „gołego” języka JavaScript…

Założenia

Celem tego artykułu jest pokazanie jak najprostszej drogi do napisania własnej gry 2D w języku JavaScript, bez konieczności stosowania bibliotek zewnętrznych czy innych dodatków. Skorzystamy natomiast z bardzo przydatnego obiektu canvas (ang: płótno), który pojawił się wraz z nadejściem HTML5. Dziś już praktycznie wszystkie przeglądarki obsługują ten standard. Z powodzeniem można więc założyć, że będzie to gra uniwersalna, w którą da się zagrać na dowolnej przeglądarce.

Zasady gry

Trudno o grę, której zasady byłyby łatwiejsze: dwóch graczy umieszczonych po dwóch stronach boiska odbija piłkę rakietkami w uproszczonej symulacji tenisa stołowego. Rakietki poruszają się tylko w jednej, pionowej płaszczyźnie. Obydwaj gracze korzystają z tej samej klawiatury. Gracz z lewej strony ma do dyspozycji klawisze A i Z, ten po prawej – używa klawiszy K i M. Równie dobrze sprawdza się też gra jednoosobowa „na dwie ręce”, w której staramy się utrzymać jak najdłużej piłkę w grze. Ominięcie piłki skutkuje zdobyciem punktu przez przeciwnika. Wygrywa gracz, który jako pierwszy zdobędzie 10 punktów.

Zaczynamy

Ponieważ gra ma się wyświetlać w przeglądarce, naszą pracę rozpoczniemy od przygotowania pliku index.html, który posłuży do wyświetlenia obiektu canvas. Pełny listing tego pliku wygląda następująco:

Jak widać, powyższy kod nie zawiera nic poza „szkieletem” gry, który obejmuje:

  • definicje stylów dla elementów body i canvas,
  • nagłówek (oraz tytuł) gry,
  • definicja głównego elementu gry, czyli obiektu canvas,
  • oraz wywołanie pliku js, w którym znajdzie się reszta programu z logiką gry w języku JavaScript.

Gra właściwa

Reszta kodu będzie już napisana w języku JavaScript i w całości zostanie umieszczona w pliku gra.js. Najpierw musimy zadeklarować dwie zmienne zawierające kontekst wyświetlania obrazu na płótnie oraz samo płótno:

Dodatkowo utworzono zmienną obiektową pong, która będzie głównym obiektem gry, zawierającym informacje o jej aktualnym stanie, graczach, piłce itp. Następnie utworzymy dwa konstruktory. Pierwszy jest funkcją tworzącą obiekt pilka.

Do utworzenia piłki wystarczą dwa parametry: promień (w pikselach) i kolor. Obiekt poza tym zawiera współrzędne piłki na płótnie (x, y), przesunięcie w poziomie oraz pionie (offsetX, offsetY), pomocne do określania parametrów ruchu, a także metodę rysuj(), służącą do wyświetlania piłki w określonej pozycji. Drugim konstruktorem jest funkcja tworząca obiekt rakietka, czyli reprezentacja gracza na ekranie.

Obiekty utworzone przy pomocy tego konstruktora obok właściwości potrzebnych do wyświetlania (współrzędne, wymiary, kolor, przesunięcie oraz metoda rysuj()) będą zawierać kilka dodatkowych informacji: nazwę gracza, jego wynik, oraz dwie flagi (doGory, doDolu) oznaczające kierunek ruchu. Flagi te wykorzystamy do obsłużenia klawiszy A,Z,K,M, za pomocą których gracze będą kontrolować swoje rakietki. W następnym kroku dodamy funkcję graInit(), służącą do inicjalnego ustawienia podstawowych parametrów gry.

Funkcja „wypełnia” danymi utworzony wcześniej obiekt pong. Pierwsze dwie właściwości będą pomocne w obsłużeniu kilku różnych stanów gry, które mogą się pojawić w jej trakcie. Zmienna stan będzie przyjmować 4 wartości zgodnie z komentarzem (0: początek gry, 1: gra w trakcie, 2: jeden z graczy zdobywa punkt, 3: koniec gry). Druga właściwość, czyli pauza, jest flagą, która mówi o tym, czy w danym momencie piłka się porusza czy nie.

Następnie tworzone są trzy obiekty za pomocą zdefiniowanych wcześniej konstruktorów: pilka oraz po jednej rakietce, które umieszczamy w tablicy gracze[]. Ponadto określamy też liczbę żyć (liczbaZyc), która oznacza liczbę punktów potrzebnych do zwycięstwa, a także  właściwość zwyciezca, jaką wykorzystamy do oznaczenia tego gracza, który wygrał zakończoną rozgrywkę. Na koniec wywołujemy funkcję graReset(), którą dodamy w następnym kroku.

O ile graInit() zostanie uruchomiona raz na rozgrywkę, to funkcja graReset() będzie wywoływana znacznie częściej, bo  po każdym zdobyciu punktu. Funkcja ta ustawia obiekty pilka, gracz[0] oraz gracz[1] w początkowych pozycjach, przywraca również domyślny offset piłki (będzie się zmieniać w czasie gry). Poza tym ustawia flagę pauza, aby po zdobyciu każdego punktu wznowienie gry nie następowało automatycznie.

W dalszej kolejności za pomocą dwóch funkcji dodamy obsługę klawiszy A,Z,K,M oraz spacji.

Dwie ostatnie instrukcje wiążą zdarzenia onKeyDown i onKeyUp z odpowiadającymi im powyższymi funkcjami. W funkcjach tych natomiast po wykryciu zdarzeń naciśnięcia lub zwolnienia określonego klawisza ustawiane są zdefiniowane wcześniej flagi: doGory, doDolu, oraz pauza w przypadku spacji. Następnie zajmiemy się wykrywaniem kolizji pomiędzy piłką i rakietką. W tym celu wykorzystamy następną funkcję.

Funkcję czyOdbiciePilki() zaczerpnięto ze strony http://goalkicker.com/, na której znajdziemy zestaw podręczników zawierających szereg bardzo przydatnych przykładów z różnych języków programowania. Powyższa funkcja znajduje się w podręczniku „HTML5 Canvas” (http://goalkicker.com/HTML5CanvasBook/) w rozdziale 11: „Collisions and Intersections”, podrozdziale 11.3: „Are a circle and rectangle colliding?”. Warto korzystać z wiedzy i doświadczenia programistów, którzy wiele problemów rozwiązali już przed nami. Gorąco zachęcam do analizowania cudzego kodu! W kolejnym kroku dodamy trzy funkcje wyświetlające wyniki, czyli aktualną punktację oraz komunikaty pojawiające się na ekranie w czasie gry.

Funkcje te na sztywno mają ustawione parametry tekstu, takie jak czcionka, kolor, pozycja. Zmieniać się będzie tylko wyświetlany tekst. Następnie dodajemy niewielką funkcję, która będzie odpowiadać za wyświetlenie na płótnie całego obszaru roboczego gry, czyli piłki, rakietek oraz aktualnego wyniku. Funkcja ta będzie wywoływana nawet kilkadziesiąt razy w ciągu sekundy, co pozwoli osiągnąć efekt ruchu. Inaczej mówiąc, poniższa funkcja będzie generować jedną klatkę animacji.

Teraz jesteśmy gotowi do utworzenia funkcji odpowiedzialnej za całą logikę gry, zawierającą m.in. odbijanie piłki od poziomych ścian (sufitu i podłogi), zarządzanie kierunkiem lotu piłki (określanym za pomocą dwóch zmiennych offsetX i offsetY), przesuwaniem rakietek i zdobywaniem punktów przez graczy.

Jak widać, samo „rysowanie” widoku gry (funkcja graRysuj()) jest dużo prostsze od obliczania przekształceń potrzebnych do wyznaczenia pozycji poszczególnych obiektów w kolejnej klatce animacji. Niemniej przekształcenia te nie są niczym innym niż zestawem prostych instrukcji, które tu i ówdzie dodają lub odejmują po kilka pikseli do aktualnych wartości zmiennych. Na uwagę zasługuje fragment odpowiadający za sprawdzenie, czy w takcie odbicia piłki rakietka jest w trakcie ruchu (czyli ustawiona jest flaga doGory lub doDolu). Jeśli taki warunek nastąpi, zmienna offsetY zmienia się o plus lub minus jeden, co da nam efekt „podkręcenia” piłki w trakcie odbicia. Ostatnią funkcją naszej gry będzie funkcja główna, której zadaniem jest obsługa wszystkich możliwych stanów gry.

Ostatnią instrukcją w powyższej funkcji jest tajemnicza requestAnimationFrame(), do której jako parametr wejściowy podajemy nazwę funkcji głównej. W ten sposób funkcja graj() będzie wywoływana w pętli nieskończonej. Instrukcja requestAnimationFrame() jest podobna do popularnej setInterval(), w której można ustawić wielkość interwału, oznaczającego, co ile milisekund ma być wywoływana dana funkcja. Różnica polega na tym, że requestAnimationFrame() jest zoptymalizowana specjalnie pod kątem wyświetlania animacji. Oznacza to, że przeglądarka sama będzie zarządzać czasem potrzebnym do wyświetlenia poszczególnych klatek, zgodnie z dostępnymi zasobami, czyli pamięcią i procesorem. Takie podejście ma tę zaletę, że każda klatka zostanie wyświetlona nie wcześniej, niż zakończy się generować poprzednia klatka, co pozytywnie wpływa na płynność animacji. Ponadto, animacja jest wyświetlana dopóty, dopóki użytkownik nie przejdzie w przeglądarce do innej zakładki. Pozwala to oszczędzić zasoby z braku konieczności wykonywania przez procesor zbędnych obliczeń, co może mieć wymierny wpływ na wydajność lub np. na zużycie baterii w smartfonie. Nie ma potrzeby podawania interwału, ale w praktyce klatki animacji będą wyświetlane co kilkanaście milisekund, co pozwoli na osiągnięcie kilkudziesięciu klatek na sekundę. Np. dla częstotliwości 16 ms, co podobno jest granicą postrzegania ludzkiego oka, otrzymujemy około 62 klatek na sekundę.

Na koniec pozostało już tylko wywołać funkcję inicjującą oraz uruchomić grę:

Końcowy rezultat można przetestować tutaj:

Podsumowanie

Ostatecznie powstała prosta gra, w którą da się zagrać w przeglądarce. Ma ona swoje wady (jak np. brak możliwości odbijania piłki pod różnymi kątami w zależności od fragmentu rakietki, w który trafiła piłka) oraz potencjalne problemy (jak zapętlenie piłki przy odbiciu rogiem rakietki z powodu wykorzystania funkcji detekcji kolizji między okręgiem i prostokątem bez uwzględnienia kierunku ruchu piłki). Celem nie było jednak napisanie gry kompletnej pod względem jakościowym, a jedynie takiej, w którą da się zagrać. W aspekcie komercyjnym powiedzielibyśmy, że powstał prototyp, który udowadnia zasadność prowadzenia dalszych prac nad rozwojem produktu. Niemniej już na tym prostym przykładzie widać potencjał zastosowanych metod do tworzenia gier uniwersalnych, niewrażliwych na wersję systemu operacyjnego i przeglądarki.

Linki

  1. Strona kierunku „Aplikacje i usługi internetowe”: http://eti.pg.edu.pl/katedra-architektury-systemow-komputerowych/studia-podyplomowe/
  2. Cykl artykułów o grze ChessMemory: http://blog.atena.pl/?s=chessmemory
  3. Podręcznik „HTML5 Canvas. Notes for Professionals”: http://goalkicker.com/HTML5CanvasBook/
  4. Historia gry Pong: https://pl.wikipedia.org/wiki/Pong
  5. Pliki zawierające grę można pobrać stąd: index.html, gra.js.

Jarosław Fostacz

O Jarosław Fostacz

Od 2003 roku jestem związany z firmą ATENA - początkowo jako programista, następnie projektant. Aktualnie jestem kierownikiem Zespołu Badań i Rozwoju oraz opiekunem niniejszego blogu. Moją siłą napędową jest nieustanne poszukiwanie innowacji oraz nowych technologii.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

4 komentarzy do “Pong. Pisanie prostych gier 2D w języku JavaScript

  • Memorabbit

    Świetny wpis! Dopiero zaczynam przygodę z javascriptem. Proste aplikacje nie stanowiły dla mnie aż takiego problemu więc postanowiłem że wezmę się za grę i… poległem. Zacząłem szperać po necie i trafiłem na Twojego bloga. Można powiedzieć że zyskałeś kolejnego czytelnika, podoba mi się to co tu pokazujesz. Gry tylko wydają się proste, w rzeczywistości trzeba mieć łeb na karku i niezłą wiedzę żeby sprawnie się poruszać w tym wszystkim. Pozdrawiam i czekam na kolejne wpisy! 🙂

    • Jarosław Fostacz
      Jarosław Fostacz Autor wpisu

      Dzięki!
      Zbieram się do napisania kolejnego artykułu w cyklu programowania w JS, ale wciąż jest jeszcze w fazie koncepcji… 🙂