Space Attack – Gra napisana w JavaScript zaczyna nabierać rumieńców. W tym poście przedstawię kolejny update, prosto z edytora tekstu 🙂 . Nowości jest sporo. Przede wszystkim, Space Attack nadaje się już do grania. Można spróbować klikając w obrazek poniżej. Jak zwykle, przygotowałem też paczkę z kodem oraz grafikami projektu.
Projekt znacznie się rozrósł. Rozmiar pliku .js to obecnie ponad 600 linijek kodu. Między innymi dlatego nie będę dokładnie opisywał wszystkiego po kolei, było by ciężko 🙂 zresztą większość mechanizmu działania gry opisałem już w poprzednim poście. Omówię zatem tylko ważne zmiany i to nowości 🙂 . Najpierw zacznę ogólnego opisu kondycji gry.
Space Attack – Gra napisana w JavaScript – ogólny opis zmian
W głównej mechanice nie zmieniło się nic. Zmiany w samym obiekcie silnika gry, GameEngine, są nieznaczne. Przede wszystkim w obiekcie config pojawiło się kilka nowych pól. Głównie są to pola przechowujące takie wartości jak etap gry czy punktacja gracza.
W obsługujących stos stanów gry metodach changeState i removeState usunąłem sprawdzanie warunku, czy stany posiadają metodę leave. Ostatecznie, żaden stan, nie miał takiej. Do ustawiania wartości gry przy zmianie stanu wykorzystuję tylko metody enter.
Za to pojawiły się dwa nowe stany: LevelAnnouncement oraz GameOver. Pierwszy stan obsługuję scenę przed uruchomieniem głównego stanu. Wyświetla informacja o tym, że gracz zostanie za chwilę przeniesiony do kolejnego etapu. Co robi stan GameOver chyba nie muszę tłumaczyć 🙂
Główny stan gry GameState rozrósł się znacznie. Przede wszystkim w grze pojawili się wrogowie, których można unieszkodliwiać rakietami 🙂 Za każdego ustrzelonego wroga gracz otrzymuje punkty. Im wyższy etap gry, tym szybciej poruszają się przeciwnicy. Jeśli zielony kosmita (dron) dotrze do dolnej krawędzi sceny gry, gracz traci życie. Do tego na wyższych etapach gry pojawiają się inni przeciwnicy, którzy wypuszczają własne pociski w stronę gracza! Jeżeli gracz zostanie trafiony, również traci życie. Po utracie trzech żyć, gra kończy się. Jeżeli natomiast gracz ustrzeli odpowiednią ilość obcych, zostaje przeniesiony do kolejnego etapu. Ilość wrogich statków, które należy zestrzelić, wzrasta wraz z etapami.
Okej tyle jeśli chodzi o ogóły, czas przejść do tego co najciekawsze, czyli do kodu 🙂
Space Attack – Gra napisana w JavaScript – nowy kod
Na początek szybki rzut okiem na nowe pola w obiekcie config silnika gry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
this.config = { score: 0, level: 1, toKill: 10, lives: 3, width: canvas.width, height: canvas.height, fps: 50, shipPicSrc: "GFX/statek.png", rocketPicSrc: "GFX/rakieta.png", dronePicSrc: "GFX/alien.png", bomberPicSrc: "GFX/bomber.png", explosionPicSrc: "GFX/explosion.png", plasmaPicSrc: "GFX/plasma.png", fighterPicSrc: "GFX/fighter.png", }; |
- score – w tym polu przechowywana będzie ilość punktów, którą zdobył gracz.
- level – czyli aktualny etap gry. Domyślnie jest to oczywiście pierwszy etap.
- toKill – liczba kosmitów jaką musi zestrzelić gracz aby przejść do kolejnego etapu.
- lives – liczba żyć gracza, jeżeli je straci, gra kończy się.
Do tego jest kilka nowych pól przechowujących adresy grafik obiektów.
Kolejny kod to dwa nowe stany LevelAnnouncement oraz GameOver. Zacznę od kodu LevelAnnouncement.
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 |
this.LevelAnnouncement = function(game){ var ctx = game.gameBoard.getContext("2d"); this.displayTime = 1.2; this.timer = 0; this.draw = function() { ctx.clearRect(0,0, game.config.width, game.config.height); ctx.font="40px Arial"; ctx.fillStyle = '#ffffff'; ctx.textBaseline="center"; ctx.textAlign="center"; ctx.fillText("Entering Level: "+game.config.level, game.config.width / 2, game.config.height/2 - 50); ctx.font="23px Arial"; ctx.fillText("Get Ready!", game.config.width / 2, game.config.height/2); }; this.update = function() { if(this.timer >= this.displayTime) { game.changeState(game.gameStates.gameState); } else { this.timer += 1/game.config.fps; } }; this.enter = function() { this.timer = 0; }; } |
Tak naprawdę nie dzieje się tu nic skomplikowanego. Jak każdy stan gry, i ten gdy jest tworzony otrzymuje w argumencie wskaźnik na silnik gry (jeżeli nie wiesz w jaki sposób tworzę stany i jak są one obsługiwane przez silnik gry, przeczytaj poprzedni post, w którym to opisuję). Pierwsze pole to odnośnik do kontekstu płótna. Dwa następne służą do kontrolowania czasu wyświetlenia tego stanu. displayTime to czas jaki ten stan ma być wyświetlany. timer przechowuje wartość równą czasowi wyświetlania. Metoda draw po prostu wypisuje tekst na scenie gry, nic specjalnego 🙂 . Metoda update tego stanu, liczy jaki czas upłynął od początku wyświetlania stanu. Jeżeli timer jest równy displayTime, silnik zmienia stan na główny stan gry. Jeżeli timer jest mniejszy, zostaje zwiększony o wartość równą ilości upłyniętego czasu. Na koniec metoda enter, która odpalana jest jako pierwsza, ustawia po prostu timer na zero.
Kolejny nowy stan to GameOver:
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 |
this.GameOver = function(game){ var ctx = game.gameBoard.getContext("2d"); this.draw = function(){ ctx.clearRect(0,0, game.config.width, game.config.height); ctx.font="40px Arial"; ctx.fillStyle = '#ffffff'; ctx.textBaseline="center"; ctx.textAlign="center"; ctx.fillText("GAME OVER", game.config.width / 2, game.config.height/2 - 80); ctx.font="23px Arial"; ctx.fillText("Aliens have invaded the Earth :(", game.config.width / 2, game.config.height/2-50); ctx.fillText("Press 'Space' to continue.", game.config.width / 2, game.config.height/2); }; this.enter = function(){ game.config.level = 1; game.config.score = 0; game.config.toKill = 10; game.config.lives = 3; }; this.keyDown = function(key){ if(key === 32 ){ // Spacja game.changeState(game.gameStates.menuState); } }; } |
Tak jak poprzedni i ten stan podczas tworzenia otrzymuje w argumencie referencje do silnika gry. Po utworzeniu pola, zawierającego kontekst płótna są tylko metody. Draw po prostu wypisuje na ekranie smutną wiadomość oraz informacje co gracz ma robić dalej. Metoda enter resetuje dane gry z obiektu config silnika gry, tak aby w kolejnej rozgrywce, zaczynał od początku. Resetowane informacje to etap gry, zdobyte punkty, liczba kosmitów do ustrzelenia oraz ilość żyć gracza. Wszystko to, co zmienia się w czasie rozgrywki. Na koniec metoda keyDown sprawdza czy została wciśnięta spacja. Jeśli tak, gra przechodzi w stan menuState.
Skoro to już mam za sobą, czas na ‚mięcho’ tego wpisu, czyli zmiany w stanie GameState. Nie będę wklejał całego kodu, tylko nowości i zmiany. Jeżeli ktoś nie pamięta co dokładnie działo się w tym stanie może zerknąć do poprzedniego wpisu, w którym wszystko jest przedstawione 🙂 zawsze można zerknąć w kod z paczki i próbować rozgryźć system na własną rękę.
Na początek nowe pola:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var ctx = game.gameBoard.getContext("2d"); this.aliensKilled = 0; this.rockets = []; this.drones = []; this.bombers = []; this.fighters = []; this.explosions = []; this.plasmas = []; this.droneTillSpawn = 0.6; this.droneSpawnTime; this.bomberTillSpawn = 2.3; this.bomberSpawnTime; this.fighterTillSpawn = 1.5; this.fighterSpawnTime; this.aliensLeft; |
Pierwsze pole to oczywiście kontekst płótna. Kolejne to aliensKilled. Przechowuje ono informacje o tym ile kosmitów zostało już zestrzelonych w tej scenie. Ta informacja przyda się później. Kolejne pola to tablice. Będą zawierały one informacje o aktorach gry. Odszedłem od opcji, gdzie wszystkie obiekty wrzucane były do jednej tablicy actors. Podzielenie tego wydaje mi się być bardziej czytelne. Do tego każdy z tych obiektów zachowuje się inaczej, co też łatwiej jest zakodować, kiedy są odpowiednio pogrupowane. Następne sześć pól to liczniki czasu respawnu kosmitów. Pola tillSpawn to czas na respawn konkretnego wroga. Pola spawnTime przechowywać będą informacje o tym ile czasu upłynęło od pojawienia się poprzedniego wroga tego typu. Na końcu znajduje się pole aliensLeft. To zmienna pomocnicza, przechowująca różnicę pomiędzy liczbą kosmitów, która należy zestrzelić a już zestrzelonymi.
Muszę przyznać, że ta część kodu trochę mi zgrzyta. Z jakiegoś powodu nie odpowiada mi, że te pola tak po prostu tu „wiszą”. Nauczyłem się ufać przeczuciom gdy programuje, więc pewnie w ciągu dalszej pracy przyjże się temu bardziej. Może powinny być w innym miejscu, a może zapakować je w obiekt coś ala config… Zobaczymy 😉
Kolejna część stanu GameState to wykorzystywane przez niego obiekty. Ship oraz Rocket są już znane. Nowe obiekty to Plasma, czyli pocisk kosmitów, Drone, Bomber i Fighter czyli rodzaje przeciwników oraz Explosion czyli obiekt reprezentujący to co zostaje po kosmicie gdy zostanie trafiony 🙂
Nie będę pokazywał kodu każdego z tych obiektów. Zachęcam za to do studiowania źródła w miarę czytania wpisu. Plasma działa dokładnie tak jak Rocket, po prostu będzie leciał w przeciwną stronę 🙂 . Obiekty Drone, Bomber oraz Fighter, są do siebie bardzo podobne, dlatego na tapetę wezmę tylko jednego z nich.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
this.Bomber = function(x,speed){ this.size = 60; this.speed = speed*0.8; this.value = 12; this.x = x; this.y = -60; this.vector = {x:0,y:1}; this.lastBombed = 2; this.bombInterval = 2; this.bomberPic = new Image(); this.bomberPic.src = game.config.bomberPicSrc; this.draw = function() { ctx.drawImage(this.bomberPic, this.x, this.y); }; }; |
Bombowiec obcych podczas tworzenia przyjmuje dwie wartości: x czyli poziomą lokację na planszy, oraz speed, czyli ilość pixeli jakie pokonuje w ciągu sekundy. Pole size to po prostu wielkość obiektu. Pole speed to prędkość, jest ona generowana na zewnątrz ponieważ zależy od etapu gry (im wyższy, tym szybciej poruszają się obcy). w przypadku bombowca, jest ona trochę zmniejszana ponieważ z założenia ma on być wolniejszy niż inne statki przeciwnika. Pole value to ilość punktów jaką zdobywa gracz po zestrzeleniu tego obiektu. Znaczenie x oraz y powinno być już jasne, warto zwrócić uwagę, że x początkowo jest ujemny, ponieważ chcę aby wrogowie spawnowali się poza plansza i dopiero na nią wlatywali.
Pole vector to nowość. Zawiera ono obiekt,który z kolei ma własne pola x oraz y. Mogą one być równe 0, 1 lub -1 i oznaczają przesunięcie. Ta wartość jest brana pod uwagę podczas wyliczania nowej lokacji obcego przy zmianie klatki. Na tę chwilę warto zapamietać ze 1 oznacza poruszanie się zgodnie z iksami lub igrekami (chodzi oczywiście o to jak są one liczone w układzie canvas), -1 poruszenie w przeciwnym kierunku, a zero brak przesunięcia na tej osi. Dla przykładu bombowiec, który ma vector.x równy zero, nie przesunie się w poziomie. vector.y równy 1 oznacza, że bombowiec będzie przesuwał się w dół płótna. Dla przykładu myśliwiec ma początkowy vector równy x:1 y:1 co oznacza, że będzie przesuwał się po skosie z prawej na lewo i z góry na dól.
Następne dwa pola lastBombed oraz bombInterval potrzebne są w mechanizmie odliczania czasu kolejnego strzału kosmity. Wartość pola lastBombed każdej instancji bombowca zmniejszana będzie co klatkę gry. Gdy dojdzie do zera, kosmita wypuści bombę. Następnie pole lastBombed otrzyma wartość równą wartości pola bombInterval i tak w kółko. Na początku lastBombed wynosi już 2 aby przeciwnik nie strzelał gdy tylko pojawi się na planszy. Myśliwce mają wbudowany taki sam mechanizm.
Kolejne pola odpowiadają, za ustawienie obiektu obrazka dla bombowca. Na koniec bardzo prosta metoda draw, która dla wszystkich aktorów gry wygląda tak samo.
Kolejny obiekt w głównym stanie gry, który chcę opisać to Explosion
1 2 3 4 5 6 7 8 9 10 11 12 |
this.Explosion = function(x,y){ this.size = 45; this.x = x; this.y = y; this.visibleTime = 0.5; this.visibleFor = 0; this.explosionPic = new Image(); this.explosionPic.src = game.config.explosionPicSrc; this.draw = function() { ctx.drawImage(this.explosionPic, this.x, this.y); } } |
Jak widać jest to bardzo prosty obiekt. Jako argumenty przyjmuje wartości dla x oraz y, które potem przypisywane są do odpowiednich pól. Obiekt posiada też pola odpowiedzialne za obrazek oraz metodę draw. To na co chcę skupić tutaj uwagę to pola visibleTime oraz visibleFor. Jak łatwo się domyślić, służą one do kontrolowania czasu wyświetlania eksplozji.
visibleFor wzrasta co klatkę gry. Gdy osiągnie wartość równą visibleTime, znika. Oznacza to, każda eksplozja będzie widoczna na planszy przez pół sekundy.
To tyle jeśli chodzi o obiekty stanu gry. Kolejny punkt do omówienia to metody, które obsługują zachowania wszystkich obiektów w głównym stanie gry.
Pierwsza jest metoda enter, odpalana za każdym razem, gdy główny stan gry zostanie uruchomiony.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
this.enter = function(){ game.pressedKeys = {}; this.ship = new this.Ship; this.aliensKilled = 0; this.rockets = []; this.drones = []; this.explosions = []; this.bombers = []; this.plasmas = []; this.fighters = []; this.droneSpawnTime = 2-((game.config.level/10)*2); this.bomberSpawnTime = 5-((game.config.level/10)); this.fighterSpawnTime = 4-((game.config.level/10)); }; |
Najpierw usuwam wszystkie dane z obiektu pressedKeys silnika gry. Następnie tworzę nową instancję statku. Nie znajduje się on już w jednej tablicy z rakietami a jest osobnym obiektem. Następnie aliensKilled jest zerowane, ponieważ każde wejście tego stany to nowy etap gry. Z tego samego powodu w kolejnych paru linijkach czyszczę wszystkie tablice zawierające obiekty gry. Nie chcę aby w nowym etapie gry od razu latali jacyś kosmici, nie mówiąc już o kulach plazmy 🙂 . Ostatnie trzy linijki to ustawienie czasu co jaki będą spawnawać się odpowiedni przeciwnicy. Program oblicza te wartości za każdym razem, gdy wchodzi nowy stan gry. Wartości te, obliczane są na podstawie poziomu gry. Metodą prób i błędów doszedłem do wzorów, które są tam obecnie.
Kolejna metoda to draw:
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 |
this.draw = function(){ //clearing ctx.clearRect(0,0, game.config.width, game.config.height); //drawing actors this.ship.draw(); for (var i = 0; i < this.rockets.length; i++) { this.rockets[i].draw(); } for (var i = 0; i < this.explosions.length; i++) { this.explosions[i].draw(); } for (var i = 0; i < this.drones.length; i++) { this.drones[i].draw(); } for (var i = 0; i < this.bombers.length; i++) { this.bombers[i].draw(); } for (var i = 0; i < this.fighters.length; i++) { this.fighters[i].draw(); } for (var i = 0; i < this.plasmas.length; i++) { this.plasmas[i].draw(); } //drawing UI ctx.font="20px Arial"; ctx.fillStyle = '#ffffff'; ctx.textAlign = "left"; ctx.fillText("score: "+game.config.score, 10, 20); ctx.fillText("level: "+game.config.level, 10, 38); for(i=1;i<=game.config.lives;i++){ ctx.drawImage(this.ship.shipPic, 0, 0, 60, 60, game.config.width-(15*i), 5, 15, 15); }; ctx.textAlign = "right"; ctx.font="10px Arial"; ctx.fillText("aliens left: "+this.aliensLeft, game.config.width-10, game.config.height - 10); }; |
Pomogłem tu sobie komentarzami. Przypominam, metoda ta wywoływana jest przez game loop co sekunda dzielona na liczbę klatek. Czyli to tutaj rysowane są klatki tworzące animacje gry. Oczywiście, najpierw czyszczony jest canvas. Następnie, metoda przechodzi przez wszystkie obiekty we wszystkich tablicach zawierających aktorów i wywołuje ich metody draw.
Gdy to jest gotowe rysowane jest GUI czyli graficzny interfejs użytkownika 🙂 . W wypadku mojej gry jest on bardzo prosty. w górnym lewym rogu wyświetlam ilość punktów zdobytych przez gracza oraz numer, etapu na którym się znajduje.
W prawym górnym rogu wyświetlam ilość żyć, która pozostała graczowi. Zamiast po prostu wypisać liczbę, za pomocą pętli for rysuję mały obrazek statku tyle razy ile żyć gracz posiada (Jak narysować taki mały obrazek używając istniejącego już obiektu image? Warto wiedzieć, więcej informacji tutaj).
W prawym dolnym rogu wyświetlana jest informacja o tym ilu jeszcze kosmitów należy zestrzelić aby ukończyć dany etap.
kolejna metoda to update:
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 |
this.update = function(){ this.aliensLeft = game.config.toKill-this.aliensKilled; if(game.pressedKeys[37]) { if(this.ship.x > 0){ this.ship.x -= this.ship.speed * 1/game.config.fps; } } if(game.pressedKeys[39]) { if(this.ship.x < game.config.width - this.ship.size){ this.ship.x += this.ship.speed * 1/game.config.fps; } } if(game.pressedKeys[32]) { if(!this.ship.onCooldown){ this.fireRocket(this.ship.x+15); this.ship.onCooldown = true; } } if(game.pressedKeys[80]) { game.addState(game.gameStates.pauseState) } this.updateRockets(); this.updateDrones(); this.updateBombers(); this.updateFighters(); this.updatePlasmas(); this.updateExplosions(); this.checkShipCannons(); this.spawnDrone(); if(game.config.level > 2) { this.spawnBomber(); } if(game.config.level > 4) { this.spawnFighter(); } this.checkCollisions(); if(this.aliensKilled >= game.config.toKill) { game.config.level++; game.config.toKill+=5; game.changeState(game.gameStates.levelAnnounce); } if(game.config.lives <= 0) { game.changeState(game.gameStates.gameOver); } }; |
Metoda update w pierwszej kolejności aktualizuje pole aliensLeft, którego wartość oznacza ile jeszcze zostało kosmitów do zestrzelenia. Następnie sprawdzany jest obiekt keysPressed silnika gry. Jeżeli został wciśnięty odpowiedni przycisk, zostaje odpalona odpowiednia metoda (nic nowego, było). Następnie metoda update uruchamia po kolei wszystkie metody, odpowiedzialne za zaktualizowanie aktorów gry. Po tym uruchamiane są metody odpowiedzialne są za spawnowanie nowych przeciwników. Nowe drony, spawnowane są od razu, natomiast bombowce i myśliwce dopiero na wyższych poziomach. Gdy to jest już gotowe, metoda update uruchamia checkCollision czyli sprawdzanie kolizji.
Ostatnią rzeczą jaką robi update stanu gry to sprawdzenie czy gra nie powinna zmienić stanu. Jeżeli pole aliensKilled jest równe polu toKill z obiektu config silnika gry to poziom gry jest zwiększany o jeden, a wartość toKill o 5 i wstawiany jest stan levelAnnounce. Jeżeli natomiast pole lives obiektu config silnika gry ma wartość równą zeru, oznacza to, że gracz przegrał i wstawiany jest stan gameOver.
Metody związane z aktualizowaniem statku i rakiet zostały już opisane wcześniej, nie licząc zmian kosmetycznych, są wciąż takie same. Pierwsza nowa metoda obiektu stanu GameState to updateExplosions.
1 2 3 4 5 6 7 8 |
this.updateExplosions = function(){ for(var i = 0;i<this.explosions.length;i++) { this.explosions[i].visibleFor += 1/game.config.fps; if(this.explosions[i].visibleFor >= this.explosions[i].visibleTime) { this.explosions.splice(i,1); } } } |
Jak widać, nie jest zbyt skomplikowana. Sprawdzany jest po prostu stan pól visibleFor każdej z instancji eksplozji znajdujących się w tablicy explosions. Jeżeli któraś osiągnęła wartość równą visibleTime, jest usuwana z tablicy.
Kolejną nową, prostą metodą jest updatePlasmas:
1 2 3 4 5 6 7 8 |
this.updatePlasmas = function(){ for(var i = 0;i<this.plasmas.length;i++) { this.plasmas[i].y += this.plasmas[i].speed * 1/game.config.fps; if(this.plasmas[i].y > game.config.height+20){ this.plasmas.splice(i,1); } } }; |
Działa ona bardzo podobnie, wszystkie plazmy znajdujące się w tablicy plasmas, mają uaktualniny stan ich pól y. Jeżeli któraś wyleci poza mapę, jest usuwana z tablicy.
Kolejne nowe metody, to te odpowiedzialne za spawnowanie oraz aktualizowanie przeciwników. W stanie GameState każdy rodzaj wroga ma takie metody. Działają one praktycznie tak samo, zmieniają się jedynie wartości liczbowe. Jako przykład zaprezentuję metody myśliwca, czyli spawnFighter oraz updateFighter.
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 |
this.spawnFighter = function(){ if(this.fighterTillSpawn <= 0){ var x = Math.floor(Math.random()*(game.config.width-60)); var speed = 350; this.fighters.push(new this.Fighter(x,speed)) this.fighterTillSpawn = this.fighterSpawnTime; } else { this.fighterTillSpawn -= 1/game.config.fps } } this.updateFighters = function(){ for(var i = 0; i<this.fighters.length;i++){ var fighter = this.fighters[i]; fighter.x += fighter.speed * fighter.vector.x * 1/game.config.fps; fighter.y += fighter.speed * fighter.vector.y * 1/game.config.fps; if(fighter.lastBombed <= 0) { this.plasmas.push( new this.Plasma(fighter.x+10,fighter.y+40)) fighter.lastBombed = fighter.bombInterval } else { fighter.lastBombed -= 1/game.config.fps } if(fighter.x <= 10 && fighter.vector.x === -1){ fighter.vector.x = 1; } if(fighter.x >= game.config.width - 50 && fighter.vector.x === 1){ fighter.vector.x = -1; } if(fighter.y <= 10 && fighter.vector.y === -1) { fighter.vector.y = 1; } if(fighter.y >= game.config.height-100 && fighter.vector.y === 1){ fighter.vector.y = -1 } } } |
spawnFighter najpierw sprawdza czy wartość pola fighterTillSpawn jest równa zero. Jeżeli tak, zostaje wylosowana wartość x dla nowego obiektu myśliwca. Wartość speed jest stała. Myśliwce i tak będą poruszać się bardzo szybko, nie chciałem aby zwiększały prędkość wraz z etapem gry jak w przypadku innych statków obcych. Następnie do tablic fighters dodawany jest nowy obiekt o odpowiednich parametrach. Na koniec wartość fighterTillSpawn ustawiana jest na równą wartości fighterSpawnTime. Jeżeli fighterTillSpawn nie jest równe zero, jest pomniejszane o wartość równą jeden dzielone na liczbę klatek.
Metoda updateFighters jest dłuższa, przez co wydaje się być trochę bardziej skomplikowana, ale to pozory. Ponownie, metoda przechodzi przez tablice zawierającą wszystkie obiekty myśliwców. Dla każdego z nich aktualizowane są wartości x oraz y są one obliczane na podstawie speed obiektu, ilości klatek na sekundę oraz odpowiedniego wartości z pola vector. Jeżeli vector równe jest -1 ruch jest odwracany, jeżeli 0, nie ma żadnego ruchu. Pisałem o tym wcześniej, ale tutaj można zobaczyć to ‚w naturze’.
Kolejna część metody odpowiada za zrzucanie przez myśliwiec pocisków. Ten mechanizm wszyscy powinni już znać na pamięć. Jeżeli upłynie odpowiedni czas zostaje zrzucona kula plazmy. Jest ona dodawana jako nowa instancja obiektu Plasma do tablicy plasmas. Jej x jest wyliczany na podstawie położenia myśliwca, który ją zrzucił.
Ostatnia część metody updateFighters zawiera cztery if’y. Służą one to sprawdzenia i zmiany kierunku, w którym będzie leciał myśliwiec. Wspomnę tutaj o ogólnym zachowaniu statków obcych. Dron, porusza się pionowo w dół, jeżeli opuści pole gry, gracz traci życie. Bombowiec również porusza się pionowo ale gdy osiągnie dolną krawędź mapy zaczyna lecieć do góry (wartość jego vector.y jest obracana), gdy osiągnie górną krawędź znów zaczyna lecieć w dół. Jego celem jest przeszkadzanie graczowi i odebranie mu żyć za pomocą bomb. Myśliwiec jako jedyny porusza się zarówno poziomo jak i pionowo. Niczym odbijająca się od krawędzi pola gry kuleczka. Do tego właśnie służą te if’y w metodzie updateFighters, sprawdzają czy mysliwieć dotarł do którejś z krawędzi i sprawdzają czy dalej leci w jej kierunku (ta informacja jest zawarta w vector). Jeżeli tak, kierunek lotu jest zmieniany.
Ostatnia metoda jaka została mi do opisania to checkCollision:
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 49 50 51 52 |
this.checkCollisions = function() { for(var i = 0;i<this.rockets.length;i++) { var rocket = this.rockets[i]; for(var j = 0;j<this.drones.length;j++) { var drone = this.drones[j]; if(rocket.x >= drone.x-(0.5*rocket.size) && rocket.x <= drone.x+45){ if(rocket.y >= drone.y && rocket.y <= drone.y+(0.5*rocket.size)){ this.drones.splice(j,1); this.rockets.splice(i,1); game.config.score += drone.value; this.aliensKilled++; this.explosions.push(new this.Explosion(drone.x, drone.y)) } } } for(var k = 0;k<this.bombers.length;k++) { var bomber = this.bombers[k]; if(rocket.x >= bomber.x-(0.5*rocket.size) && rocket.x <= bomber.x+45){ if(rocket.y >= bomber.y && rocket.y <= bomber.y+(0.5*rocket.size)){ this.bombers.splice(k,1); this.rockets.splice(i,1); game.config.score += bomber.value; this.aliensKilled++; this.explosions.push(new this.Explosion(bomber.x, bomber.y)) } } } for(var m = 0;m<this.fighters.length;m++) { var fighter = this.fighters[m]; if(rocket.x >= fighter.x-(0.5*rocket.size) && rocket.x <= (fighter.x+40)){ if(rocket.y >= fighter.y && rocket.y <= fighter.y+(0.5*rocket.size)){ console.log("wchodzi y") this.fighters.splice(m,1); this.rockets.splice(i,1); game.config.score += fighter.value; this.aliensKilled++; this.explosions.push(new this.Explosion(fighter.x, fighter.y)) } } } } for(var i = 0;i<this.plasmas.length;i++) { var plasma = this.plasmas[i]; var ship = this.ship; if(plasma.x >= ship.x+5 && plasma.x <= ship.x+55){ if(plasma.y-15 >= ship.y && plasma.y <= ship.y+60){ game.config.lives--; this.plasmas.splice(i,1); } } } }; |
Pomimo pokaźnej wielkości (przynajmniej w porównaniu z innymi funkcjami) ten potworek wcale nie jest taki skomplikowany. Po dokładnym przyjrzeniu się widać, że metodę tę można podzielić na dwie pętle for. Pierwsza z nich przechodzi przez tablicę rockets a druga przez tablicę plasmas. Czyli sprawdzane są położenia wszystkich pocisków, ma to sens 🙂 W pierwszej pętli dla każdej z rakiet, tworzone są kolejne trzy pętle, dla każdej z tablic przeciwników. Kod przechodzi przez każdego przeciwnika i sprawdza, czy rakieta nie zajmuje tej samej przestrzeni co on. Jeżeli tak, zarówno rakieta jak i przeciwnik usuwani są z odpowiedniej tablicy, a do tablicy explosions dodawana jest nowa instancja eksplozji o x oraz y równym tym samym wartościom przeciwnika.
Sprawa podobnie ma się z drugiem for’em. Tym razem lokacja każdej z plazmy, porównywana jest z lokacją statku gracza. Jeżeli przypadkiem znajdują się w tym samym miejscu, gracz traci jedno życie a plazma jest usuwana z gry.
Muszę się przyznać że do sposobów porównywania lokacji dochodziłem metodą prób i błędów. Niestety same obiekty są większe niż obrazki, co czasem skutkowało dziwnymi zachowaniami. Myślę jednak, że efekt, który ostatecznie udało mi się osiągnąć jest zadowalający. Chociaż na pewno jest to coś co w przyszłości mogę poprawić, jeśli nie w tej grze to w kolejnych.
I takim pozytywnym akcentem udało się dotrzeć do końca tego postu. Reszta kodu jest już znana, lub powinna nadawać się do interpretacji przy pomocy informacji, które zamieściłem powyżej. Myślę, że to jeszcze nie koniec tej gry. Mam parę pomysłów na to aby ją rozwinąć, ale zobaczymy jak to pójdzie.
Jeżeli masz jakieś pytania nie wahaj się zadać ich w komentarzach. Świetnym miejscem na kontakt ze mną jest również moja strona na facebooku (bardzo zachęcam do polubienia), którą odwiedzam i aktualizuję na bieżąco.