Dziś dalszy ciąg opisu mojej wariacji o space invaders w JavaScripcie. Tym razem mamy do czynienia z pełną grą. Nie jest to jeszcze ostateczna wersja, ale na tę chwilę jest w stu procentach sprawna i grywalna. Bez zbędnych ceregieli, oto aktualna gra.
Tak samo jak w przypadku wisielca, przeanalizuję każdą z funkcji po kolei. Nie będę się zagłębiał w kod HTML, tak jak w poprzedniej grze, nie jest on specjalnie skomplikowany. Tak naprawdę potrzebowałem tylko parę divów, do wyświetlania gry plus przyciski i pola tekstowe do pobierania informacji od użytkownika.
Tym razem również użyłem biblioteki jQuery. Dzięki niej znacznie łatwiej jest odnosić się do poszczególnych elementów DOMu. W tej grze wykorzystałem również możliwość animowania stylów, które daje biblioteka. Było to dla mnie ciekawe doświadczenie, ponieważ wcześniej rzadko korzystałem z tej opcji. Ok, to lecimy z tym kodem.
Najpierw mamy listę potrzebnych nam zmiennych.
1 2 3 4 5 6 7 8 |
var stageSize = 600; var shipPos = 270; var rocketPos = 500; var pociski = 8; var alienPos = Math.ceil((Math.random()*540)); var alienTop = 30; var gameFinished = false; var alienKilled = false; |
Nie ma co tu dużo pisać. Zadeklarowałem zmienną zawierającą rozmiar pola gry, parę zmiennych przechowujących lokacje obiektów, oraz zmienne boolowskie potrzebne do zakończenia rozgrywki (game loop). Warto zwrócić w tym miejscu uwagę na zmienną alienPos. Ta zmienna zachowuje nam wartość, która później będzie przypisana do wartość css left naszego wroga. Zmienna ta jest generowana losowo i w każdej rozgrywce powinna być inna. Dzięki temu za każdym razem kosmita, będzie inaczej umiejscowiony poziomo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
jQuery("#gameStarter button").on("click", function() { setupGame(); }) function setupGame(){ jQuery("#gameStarter button").hide(); jQuery("#rocketControls, #shipControls, #fireButton").show(); setInfo("Podaj wspolrzedna na ktora chcesz wyslac statek i moc z jaka chcesz wystrzelic pocisk."); jQuery("#alien").animate({"left": alienPos}) jQuery("#fireButton").on("click", function(){ playGame(); }); } function setInfo(text){ jQuery("#informacje").text(text); } |
Ok, pierwszą rzeczą, która dzieje się w tym programie, jest przypisanie eventu do przycisku start. Po przyciśnięciu start, odpala się funkcja setupGame. Funkcja ta ukrywa nam przycisk start i pokazuje główne kontrolki gry. Do tego zmienia, przy użyciu funkcji setInfo tekst informujący gracza o stanie gry. Widzimy tutaj również zastosowanie animate jQuery. Element przedstawiający kosmitę zostaje przesunięty na pozycję left równą zmiennej alienPos. Na koniec do przycisku Fire zostaje przypisany event. Kliknięcie „Fire” odpali nam główną część gry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
function playGame(){ shipPos = parseInt(jQuery("#shipInput").val()); rocketPos = stageSize - parseInt(jQuery("#rocketInput").val()); if(isNaN(shipPos) || isNaN(rocketPos)) { setInfo("Podaj liczby"); jQuery("#informacje").addClass("warning"); return false; } if(shipPos < 0 || shipPos > 540) { setInfo("Wspolrzedna statku nieprawdilowa"); jQuery("#informacje").addClass("warning"); return false; } if(rocketPos < 0 || rocketPos > stageSize-150) { setInfo("Moc pocisku nieprawdilowa"); jQuery("#informacje").addClass("warning"); return false; } setInfo("Strzelam!"); jQuery("#informacje").removeClass("warning"); pociski --; jQuery("#statek").animate({"left":shipPos},{complete: function(){ jQuery("#rakieta").css({"display":"block"}); jQuery("#rakieta").css({"left":shipPos+15+"px"}); }}); setTimeout(function(){ jQuery("#rakieta").animate({"top":rocketPos},{complete: function(){ jQuery("#rakieta").css({"top":"500px"}); jQuery("#rakieta").css({"display":"none"}); }}); }, 1000); setTimeout(function(){ gameFinished = checkIfGameFinished(); if(gameFinished){ jQuery("#statek").animate({"left":shipPos}); jQuery("#rakieta").animate({"left":shipPos+15}); jQuery("#rakieta").animate({"top":rocketPos}); endGame(); } else { alienPos = Math.ceil((Math.random()*540)); moveAlien(); } }, 1500); } |
Ta kobylasta funkcja, jest głównym mechanizmem naszej gry. Pierwsze dwie linijki to ustawienie shipPos oraz rocketPos. Wartości pobierane są z odpowiednich pól tekstowych. Tych zmiennych używam do ustawienia pozycji statku oraz rakiety. shipPos odpowiadać będzie za zmianę styli CSS left rakiety i statku, natomiast rocketPos za styl top rakiety. Dlaczego odejmuję rocketPos od rozmiaru planszy gry? Założyłem, że zmienna ta będzie przedstawiać moc z jaką gracz chce wystrzelić pocisk. Im mniejszy styl top tym bliżej będzie górnej krawędzi, czyli przeciwnej do naszego statku. Mało sensu miałaby sytuacja w której wystrzelenie pocisku z mniejszą mocą, spowoduje, że zaleci on „dalej” 🙂 Dlatego sprytnie „odwróciłem” tę wartość.
Następne parę linijek to sprawdzanie czy podane przez gracza wartości mają sens. Chcę aby był w stanie podać tylko liczby. Nie chcę, natomiast, aby elementy wysuwały się poza planszę. Jeżeli, któryś z tych wyjątków się wydarzy, funkcja zwraca false i wyświetla odpowiedni komunikat (znów w użyciu funkcja setInfo). Dodamy też do naszego paragrafu informacyjnego klasę warning, przez co tekst wyświetlany będzie na czerwono.
Jeżeli funkcja przejdzie przez wszystkie ify, nie zwracając false, to znaczy, że gracz podał prawidłowe wartości i zaczyna się gra. Jeszcze tylko odejmujemy jeden od liczby pocisków i usuwamy klase warning z naszego tekstu informacyjnego i lecimy dalej. Tutaj zaczyna robić się ciekawie. Po kliknięciu fire, dzieję się parę rzeczy. Z punktu widzenia gracza ważne są 3 z nich:
- przesunięcie statku,
- wystrzelenie pocisku
- przesuniecie kosmity (o ile nie oberwał)
Każda z tych rzeczy jest animowana. Animacja trwa, a my chcemy, żeby wszystko wyglądało płynnie i naturalnie. Problem polega na tym, że JavaScript odpala animacje i leci dalej. Ostateczny rezultat jest taki, że wszystko rusza się na raz. Nie chcemy tego.
Żeby poskromić to zachowanie, sięgnąłem po dwa rozwiązania. Pierwszy z nich to użycie argumentu complete przy animacjach. Drugi to stary dobry waniliowy setTimeout. W moim zamierzeniu animowanie elementów miało być dodatkowym bajerem, a wyszedł z tego mini projekt 🙂
Po kolei. Pierwszy animujemy statek. Leci on sobie na taka wartość left jaką podał gracz. Jako drugi argument funkcji animate, podaje obiekt. Zawartością obiektu jest klucz complete do którego przypisana jest funkcja. To tzw. callback function. Jest ona wywoływana, kiedy zakończy swoje działanie funkcja, która ją wywołuje. Idealnie! O to nam chodziło. W funkcji pod complete ustawiam css pocisku i wyświetlam go na planszy (nie chcemy, żeby był widoczny przed odpaleniem działa 😉 a właśnie to stało by się gdybym, nie zrobił tego w callback funkcji).
Następnie animujemy pocisk. I tu niespodzianka. Cała funkcja animate pocisku wsadzona jest w setTimeout trwający sekundę. Czemu? Gdyby nie to, statek i pocisk zaczęłyby poruszać się (z punkty widzenia użytkownika) w tej samej chwili. No znów nie tego chcemy. Zamierzenie jest takie, że statek przyjmuje odpowiednią pozycję, po czym odpala działa! Stąd timeOut. Teraz statek ma całą sekundę, żeby dolecieć gdzie trzeba 🙂
Warto zauważyć, że przy animacji pocisku również dodany mamy argument z complete. Tym razem po zakończeniu animacji chowamy pocisk i ustawiamy go z powrotem gdzie jego miejsce, czyli koło statku. Znów. gdybyśmy nie zrobili tego po zakończeniu animacji, pocisk zniknął by od razu, przez co użytkownik, nie zobaczyłby go w ogóle!
Ostatnia partia kodu w funkcji playGame (tak to już prawie koniec 😉 ) też jest ‚wsadzona’ w setTimeout. tym razem czas oczekiwania to 1,5 sekundy. Dzięki temu, to zdarzenie odpala się nam grzecznie na samym końcu. Pierwsze co co się dzieje, to do zmiennej gameFinished przypisane zostaje to co zwraca funkcja checkIfGameFinished. To dokładnie to z czym mieliśmy do czynienia w wisielcu. Przejdziemy do tego za moment.
Następnie sprawdzamy czy gra faktycznie się zakończyła, testując wartość gameFinished. Jeśli tak, odpalamy funkcję endGame. Jeśli nie, losujemy nową pozycję statku obcych i odpalamy funkcję moveAlien. Ta ostatnia funkcja to powód, dla którego odczekiwaliśmy półtorej sekundy. Chciałem aby kosmita poruszył się dopiero po odpaleniu rakiety, nie w trakcie. Byłoby wtedy ciężko w niego trafić 😉
Zanim zabierzemy się za checkIfGameFinished, spójrzmy szybko na funkcje moveAlien.
1 2 3 4 5 6 |
function moveAlien() { alienTop += 50; jQuery("#alien").animate({"top":alienTop}); jQuery("#alien").animate({"left":alienPos}); console.log(alienTop+""+alienPos); } |
Najpierw do zmiennej alienTop dodajemy 50. Wartość tej zmiennej później przypisywana jest stylowi css top elementu przedstawiającego kosmitę. W ten sposób z każdą rundą obcy są coraz bliżej. No i trudniej go trafić, jak tak się cały czas rusza 🙂
Następnie animujemy ruch kosmity. I to tyle jeśli chodzi o moveAlien.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function checkIfGameFinished(){ if(shipPos+15 >= alienPos && shipPos+15 <= alienPos+60){ if(rocketPos >= alienTop && rocketPos <= alienTop+60) { alienKilled = true; jQuery("#ammo").text("Pozostalo pociskow: "+pociski); return true; } } if (pociski === 0) { return true; jQuery("#ammo").text("Pozostalo pociskow: "+pociski); } setInfo("Pudlo! Sprobuj ponownie!"); jQuery("#ammo").text("Pozostalo pociskow: "+pociski); jQuery("#shipInput").val(""); jQuery("#rocketInput").val(""); return false; }; |
Funkcja checkIfGameWon zwraca wartość boolowską. Dopóki zwraca false, gra trwa, lecz jeśli zwróci true, gra kończy się. Gra może zakończyć się w dwóch przypadkach. Jeśli gracz zestrzeli kosmitę, lub jeśli skończą mu się pociski.
To właśnie sprawdza funkcja checkIfGameWon. W pierwszym przypadku mamy zagnieżdżonego ifa. Sprawdzamy tu najpierw czy statek (a właściwie rakieta) jest w linii pionowej ze statkiem obcych. Jeśli tak, sprawdzamy dalej czy rakieta jest w tym samym miejscu co statek obcych. No cóż, dwa obiekty nie mogą zajmować tej samej przestrzeni, więc jeśli i rakieta i statek obcych znajdują się w tym samym miejscu… zła nowina dla obcych. Gracz wygrał! Funkcja ustawia zmienną alienKilled na true oraz zwraca true.
Jeżeli jednak gracz spudłował, funkcja checkIfGameWon testuje kolejny warunek. Tym razem sprawdzamy czy graczowi nie skończyły się pociski. Jeśli tak, to niestety, tym razem gracz ma pecha. Funkcja zwraca true.
Jeżeli do tej pory funkcja nie zwróciła żadnej wartości, oznacza to, że gracz nie trafił w kosmitę, lecz wciąż ma pociski. Informujemy go o stanie rzeczy i czyścimy zawartość pół tekstowych.
1 2 3 4 5 6 7 8 |
function endGame() { if(alienKilled){ setInfo("Udalo Ci sie zestrzelic statek obcych! Zostalo Ci "+pociski+" pociskow.") } else if(pociski === 0){ setInfo("Niestety, przez twoja nieudolnosc obcy najechali ludzkosc. Nastepnym razem postaraj sie troche bardziej") } jQuery("#rocketControls, #shipControls, #fireButton").hide(); }; |
Ostatnia funckja to funkcja endGame. Jest bardzo prosta. Sprawdza czy zmienna alienKilled jest prawdziwa, jesli tak, gratuluje graczowi. Jeśli, nie sprawdza czy gracz na pewno nie ma już pocisków (nie potrzebnie?). I wypisuje na ekranie to na co gracz sobie zasłużył.
Niezależnie od wyniku, ukrywamy kontrolki. To już koniec gry.
Na pewno koniec?
Otóż nie. Nie jest to jeszcze ostateczna wersja gry. Poprawić i usprawnić, można jeszcze sporo. Ja zanim odstawie ten projekt na półkę chcę zmienić tylko jedną rzecz. Tak jak pisałem w pierwszej części chcę aby wartości były podawane przez gracza po kolei, a nie w tym samym czasie. Najpierw gracz poda współrzędną dla statku i poruszy nim a następnie poda moc pocisku, który dopiero wystrzeli. Aby to zadziałało wprowadzę nowy mechanizm kontrolujący stan gry. Ale to już w innym poście 🙂
Uwagi i przemyślenia
Może nie jest to tak oczywiste w tym przypadku, ale też mamy tu do czynienia z game loopem. Nie jest widoczny ale zdecydowanie znajduję się w tym programie. Dopóki gracz klika Fire a zmienna gameWon jest fałszywa, gra trwa. Przeanalizujcie ten kod a na pewno to zobaczycie.
Druga sprawa. Uważam, że warto eksperymentować z nowymi rzeczami. To najlepszy sposób na naukę. Tak jak pisałem, nie używałem wcześniej animacji w jQuery. Użyłem ich aby uprościć cały kod a okazuje się, że to nie takie banalne sprawy. Na pewno będę dalej eksplorował ten temat, chociaż pewnie nie przy tworzeniu gier. Do tego zdecydowanie bardziej wolę canvas 🙂