Nie na długo odszedłem od tematu tworzenia gier 🙂 Od pewnego czasu zbierały mi się tematy, z którymi chciałem poeksperymentować. Przede wszystkim z funkcją, o której słyszałem, że bardzo usprawnia Tworzenie gier w JavaScript: requestAnimationFrame. Ponieważ w Święta człowiek ma sporo wolnego, to zamiast siedzieć, nudzić się i objadać sernikiem, postanowiłem spożytkować ten czas i wypróbować parę nowych technik programowania gier (no dobra, jedząc w tym czasie sernik 😉 ).
W taki sposób powstał mini-projekcik, z którego screen widzicie poniżej (Póki co nie jest to jeszcze gra:)). Dużo w nim poeksperymentowałem i sporo się nauczyłem. Wszystko oczywiście opiszę w tym poście. Projekt można obejrzeć klikając w obrazek poniżej. Jak zwykle przygotowałem też paczkę z kodem, aby każdy mógł sam sobie podłubać.
W projekcie, możemy latać czarownicą po lesie zajmującym teren pomiędzy jej chatką a magicznymi głazami. Myk polega na tym, że teren ten, jest znacznie większy niż obszar wyświetlany przez canvas. Gdy czarownica zbliży się do krańca wyświetlanego, przez płótno obszaru, pole widzenia przesunie się! Użytkownik nie jest ograniczony, krawędziami elementu canvas, może do woli eksplorować cały świat projektu.
Do tego, tło gry porusza się w innym tempie, co sprawia wrażenie, że jest dalej. Ten bardzo przyjemny wizualnie efekt, również został osiągnięty programowo w canvasie. Oprócz tych dwóch widocznych cech, nowością jest też wspomniany wcześniej requestAnimationFrame. Teraz ta metoda zajmuje się tym do czego wcześniej używałem setInterval i wielu pomocniczych mechanizmów obliczających upływający czas itp.
Ostania innowacja to sposób przechowywania grafik z gry. Nie jest to już wiele plików jak wcześniej, a jeden wielki plik png.
Czas przejść do kodu, jak zwykle HTML nie zawiera nic specjalnego. Jest tu oczywiście element canvas, bez którego ani rusz. Kod JavaScriptu odpalany jest gdy element body, załaduje się. Wywoływana zostaje wtedy funkcja startWitch, która wygląda ona tak:
1 2 3 4 5 |
function startWitch() { var canvas = document.getElementById('game'); var game = new WitchGame(canvas); game.loadGame(); } |
W tym fragmencie kodu też nie znalazło się nic zaskakującego. Najpierw pobieram element canvas i przypisuję go zmiennej. Następnie tworze instancję obiektu gry WitchGame i uruchamiam jej metodę loadGame.
Czas przejść do ‚mięcha’ czyli głównego obiektu gry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var WitchGame = function(canvas) { var self = this; this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.entities = []; //klasa Entity this.Entity = { sourceX: 0, sourceY: 0, sourceWidth: 70, sourceHeight: 70, x: 0, y: 0, width: 70, height: 70, } |
Jako argument, konstruktor obiektu przyjmuje referencję do płótna, którą od razu przypisuję do wewnętrznego pola. Tworzę również zmienną self, która przechowywać będzie odniesienie do this. self bardzo mi się przyda później. Pobieram też kontekst płótna i umieszczam w odpowiednim polu. Tworzę też polę entities, które początkowo zawiera pustą tablicę. W tej tablicy przechowywane będą wszystkie obiekty gry.
Gdy to już gotowe, tworzę polę Entity, które zawiera obiekt. Jest to jakby szablon, według którego tworzone wszystkie obiekty w pojawiające się w grze. Problemem w moich poprzednich grach było to, że wiele kodu w obiektach gry powtarzało się. Tworząc szablon, chcę tego uniknąć. Wprawdzie może tego nie być widać w tym projekcie (tworzę tylko 3 obiekty, z czego dwa to tła ;)), ale w przyszłości powinno się sprawdzić.
Ok szybki rzut okiem na pola, które zawiera mój szablon. Już po samych nazwach można domyślić się do czego służą. Będą one przechowywały dane, potrzebne do funkcji rysującej drawImage. Pierwsze cztery wskazują na miejsce z obrazka źródłowego, a kolejne cztery wskazują na miejsce na płótnie. drawImage opisałem w jednym z wcześniejszych postów. Klasa Entity zawiera te pola, ponieważ wszystkie obiekty, które pojawią się w tym projekcie będą ich potrzebowały. Tym razem mam tylko jeden obrazek źródłowy, i każdy z tych obiektów musi zostać najpierw z niego pobrany. Obrazek źródłowy polecam obejrzeć w paczce z kodem projektu.
A oto instancje klasy Entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//Obiekty tła this.background = Object.create(this.Entity); this.background.sourceWidth = 12000; this.background.sourceHeight = 600; this.background.width = 12000; this.background.height = 600; this.moonStars = Object.create(this.Entity); this.moonStars.sourceY = 670, this.moonStars.sourceWidth = 12000; this.moonStars.sourceHeight = 600; this.moonStars.width = 12000; this.moonStars.height = 600; //obiekt czarownicy this.witch = Object.create(this.Entity); this.witch.sourceY = 600, this.witch.x = 30; this.witch.y = this.canvas.height/2 - this.witch.height/2; this.witch.speed = {x:0,y:0}; this.witch.goingUp = false; this.witch.goingDown = false; this.witch.goingLeft = false; this.witch.goingRight = false; this.witch.facing = 0; |
Mam trzech aktorów. background, moonStars oraz witch. Wszytskie tworzę za pomocą metody globalnego obiektu Object, create. W skrócie, create zwraca kopię obiektu przekazanego jej jako argument i przypisuje ją do zmiennej. Dzięki temu wiersz:
1 |
this.background = Object.create(this.Entity); |
powoduje, że obiekt background, ma identyczne pola jak Entity. Oczywiście niektóre muszę nadpisać.
Obrazek tła znajduje się na samej górze obrazka źródłowego i zajmuje całą jego szerokość. Nie muszę więc zmieniać pol x i y a jedynie rozmiar obrazka. Rozmiar po wklejeniu do płótna będzie równy rozmiarowi w źródle.
moonStars jest bardzo podobny. Zawiera on obrazek gwiazd i księżyca. Ponieważ, chcę aby były ruchome, nie umieściłem ich bezpośrednio na tle. Zamiast tego znajdują się na samym dole łączonego obrazka i posiadają przeźroczyste tło.
Obiekt witch, reprezentuje oczywiście bohaterkę gry. Oprócz pól opisujących lokację obrazka w źródle i na płótnie, posiada parę dodatkowych. speed zawiera obiekt z dwoma polami x oraz y, będa one przechowywały informacje o tym z jaką prędkością (o ile pikseli na klatkę) porusza się czarownica. goingUp, goingDown, goingLeft oraz goingRight, to pola o wartościach boolowskich. True na którymś z tych pól oznacza, że czarownica porusza się w danym kierunku. Ostatnie pole czarownicy to facing i oznacza ono stronę w którą zwrócona jest czarownica.
Kolejne dwa obiekty są dość wyjątkowe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
this.gameWorld = { width: this.background.width, height: this.background.height, }; // ekran this.screen = { x:0, y:0, width: this.canvas.width, height: this.canvas.height, speedX: 0, prevX: 0, rightScrollZone: function(){ return this.x + (this.width * 0.75); }, leftScrollZone: function(){ return this.x + (this.width * 0.25); } }; |
To dzięki tym dwój obiektom, gra ‚przesuwa się’. Wyjaśnie jak to dokładnie działa trochę dalej. Póki co opiszę mniej więcej informacje, które przechowują. Pierwsze co można zauważyć to to, że nie są to instancje klasy Entity. Obiekty, te nie są rysowane na planszy, więc nie potrzebują pól Entity. To tylko abstrakcyjne modele, według których, obliczane będzie co powinno zostać wyświetlone na płótnie. Teraz może wydawać to się mało zrozumiałe, ale gdy dojdę do objaśniania mechanizmów które tym zarządzają, powinno być ok.
Obiekt gameWorld ma wymiary równe obiektowi background. To dlatego, że cały świat gry jest przedstawiony na tamtym obrazku. Ten obiekt potrzebny będzie do kontrolowania, aby widok gry nie ‚wyjechał’ poza zdefiniowany obszar świata.
Obiekt screen reprezentuje ‚wyświetlacz’ czyli obszar obecnie wyświetlany na płótnie. Jego początkowe współrzędne x oraz y równe są zero. Będą zmieniać się gdy ekran ‚przesunie’ się aby pokazać dalszy fragment świata. To znaczy, zmieniać będzie się x, ponieważ wysokość wyświetlacza, świata oraz płótna są takie same. Szerokość orz wysokość obiektu screen są takie samy jak wymiary płótna. Pola speedX oraz prevX, służą do obliczenia prędkości z jaką porusza się ekran, co zarazem pozwoli później na przesunięcie gwiazd i księżyca.
Obiekt screen posiada też dwie metody: rightScrollZone oraz leftScrollZone. pomagają one zdefiniować obszary po lewej i prawej stronie wyświetlacza o szerokości równej jednej czwartej jego rozmiaru. A dokładniej to zwraca wartości graniczne tych obszarów. Dzięki funkcjom, które omówię za chwilę, ekran zacznie przesuwać się, kiedy czarownica przekroczy te granice.
Czas przejść do metod głównego obiektu gry. One wykorzystują informacje w polach które opisałem powyżej. Najpierw loadGame, która jest odpalona zaraz po załadowania strony:
1 2 3 4 5 6 7 |
this.loadGame = function(){ this.masterSprite = new Image(); this.masterSprite.src = "GFX/sprite2.png"; this.masterSprite.addEventListener("load",function(){ self.start(); },false); }; |
W tej metodzie tworzę nowe pole masterSprite. Bęzie ono zawierało nowy obiekt Image, reprezentujący obrazek ze wszystkimi grafikami. Oprócz adresu obrazka do obiektu przypisuję również eventListner, który nasłuchuje na event load. To jest nowość. Funkcja przekazana do listenera zostanie wykonana dopiero kiedy obrazek skończy się ładować. Ponieważ obrazek jest pokaźnych rozmiarów, jest to praktycznie niezbędne. Uniknę dzięki temu sytuacji, w których obiekty zostaną narysowane na płótnie zanim ich grafiki się wczytają, przez co nie będą widoczne.
Gdy obrazek już się załaduje wywołana zostaje metoda start:
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 |
this.start = function() { this.entities.push(this.background); this.entities.push(this.moonStars); this.entities.push(this.witch); window.addEventListener("keydown", function(event){ switch(event.keyCode){ case 38: self.witch.goingUp = true; break; case 40: self.witch.goingDown = true; break; case 39: self.witch.goingRight = true; self.witch.facing = 0; break; case 37: self.witch.goingLeft = true; self.witch.facing = 1; break; } },false) window.addEventListener("keyup", function(event){ switch(event.keyCode){ case 38: self.witch.goingUp = false; break; case 40: self.witch.goingDown = false; break; case 39: self.witch.goingRight = false; break; case 37: self.witch.goingLeft = false; break; } },false) this.update(); }; |
Najpierw dodaję wszystkie obiekty, które mają być rysowane do tablicy entities. Dodane na początku będą rysowane jako pierwsze. Muszę o tym pamiętać, żeby przypadkiem nie przykryć czarownicy obrazkiem tła. Następnie dodaję dodaję do gry nasłuchiwania na przyciśnięcie klawiszy. Nie jest to nic nowego, moje poprzednie projekty, korzystały już z takich listenerów. Jeżeli zostanie naciśnięta któraś ze strzałek, odpowiednie pole w obiekcie czarownicy otrzymuje wartość true. Gdy przycisk zostanie puszczony przez użytkownika, pole otrzymuje wartość false. W przypadku strzałek w lewo i prawo zmieniam również wartość pola facing czarownicy. Dzięki temu, będę mógł zmieniać obrazek tak aby patrzyła w stronę w którą leci 🙂 . Nie jest to może idealny system, kontroli postaci ale na potrzeby tego projektu się sprawdza.
Na końcu funkcja start uruchamia funkcję 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 46 47 48 |
this.update = function() { requestAnimationFrame(self.update,self.canvas); if(self.witch.goingUp && !self.witch.goingDown){ self.witch.speed.y = -5; } if(self.witch.goingDown && !self.witch.goingUp){ self.witch.speed.y = 5; } if(self.witch.goingLeft && !self.witch.goingRight){ self.witch.speed.x = -5; } if(self.witch.goingRight && !self.witch.goingLeft){ self.witch.speed.x = 5; } if(!self.witch.goingUp && !self.witch.goingDown){ self.witch.speed.y = 0; } if(!self.witch.goingRight && !self.witch.goingLeft){ self.witch.speed.x = 0; } self.witch.x += self.witch.speed.x; self.witch.y += self.witch.speed.y; self.witch.x = Math.max(0, Math.min(self.witch.x + self.witch.speed.x, self.gameWorld.width - self.witch.width)); self.witch.y = Math.max(0, Math.min(self.witch.y + self.witch.speed.y, self.gameWorld.height - self.witch.height)); if(self.witch.x < self.screen.leftScrollZone()){ self.screen.x = Math.floor(self.witch.x - (self.screen.width * 0.25)); } if(self.witch.x + self.witch.width > self.screen.rightScrollZone()){ self.screen.x = Math.floor(self.witch.x + self.witch.width - (self.screen.width * 0.75)); } if(self.screen.x < 0) { self.screen.x = 0; } if(self.screen.x + self.screen.width > self.gameWorld.width){ self.screen.x = self.gameWorld.width - self.screen.width; } self.screen.speedX = self.screen.x - self.screen.prevX; self.moonStars.x += self.screen.speedX / 1.5; self.screen.prevX = self.screen.x; self.draw(); }; |
I tutaj zaczyna się już robić ciekawie. Najpierw uruchamiam requestAnimationFrame. Ta jedna linijka zastępuje cały mechanizm setInterval z moich poprzednich projektów. Przyjmuje ona dwa parametry. Pierwszy to funkcja, która ma wywołać kiedy przeglądarka jest gotowa na wyświetlenie kolejnej klatki animacji, a druga to odnośnik do canvas, na którym animacja ma miejsce. I tyle. Wszystkie inne obliczenia związane z klatkami, upływem czasu i tak dalej, są wykonywane automatycznie. Idealnie.
Następnie mam 6 wyrażeń if. Pierwsze cztery, zmieniają prędkość czarownicy w zależności w jakim kierunku się porusza (Pisząc prędkość mam oczywiście na myśli o ile zmieni się jej położenie względem osi współrzędnych na canvasie, ale to już wszyscy wiemy 🙂 ). Oczywiście pod warunkiem, że nie porusza się w tym samym czasie w przeciwnym kierunku. Dwa ostatnie ify, zmieniają prędkość czarownicy na zero, jeżeli nie rusza się w ogóle. Logiczne.
Kolejne dwie linijki, zmieniają położenie wiedźmy, zależnie od jej aktualnej prędkości.
Następnie pojawiają dwie linijki, które sprawiają, że czarownica nie wyleci poza zdefiniowany przez gameWorld świat gry. Wykorzystuję do tego bardzo sprytne funkcje obiektu Math: max oraz min. Funkcja max zwraca największa z podanych jako argumenty. Funkcja min robi to samo z tą różnicą że zwraca najmniejszą. Jak się to ma do położenia czarownicy.
Moim celem jest utrzymanie x oraz y czarownicy w granicach pomiędzy zerem a rozmiarami świata. Dlatego muszę sprawdzić czy po ich położenia czarownicy, te wartości nie zostały przekroczone. Funkcja min zwraca to co jest mniejsze, obecny x/y czarownicy, lub rozmiar świata. Jeśli czarownica przekroczy granice, zostanie cofnięta. Wynik tej operacji jest przekazany jako argument wraz z zerem do funkcji max. To niezły trick. Nie ma możliwości aby została zwrócona wartość mniejsza niż zero lub większa od rozmiaru świata.
Kolejne dwa wyrażenia warunkowe sprawdzają czy czarownica nie przekroczyła którejś z granic przesuwnia wyświetlacza. Jeżeli tak, x wyświetlacza, zmniejsza lub zwiększa (zależnie od tego, w którą stronę leci czarownica) o taką wartość o jaką czarownica, przeleciała przez daną granicę. Dlaczego nie testuję wartości y? Nie ma to sensu, wysokość wyświetlacza jest taka sama jak wysokość świata. Jednak gdyby zaszła taka potrzeba, mógłbym bez problemu dodać strefy przewijania przy górnej i dolnej krawędzi wyświetlacza, a w tym miejscu sprawdzać czy nie zostały przekroczone.
Powoli zbliżam się do końca metody update. Następna jej część to znów dwa ‚ify’. Pierwszy sprawia, że x wyświetlacza nie będzie mniejsze od zera a drugi, że nie będzie większy od wielkości świata minus szerokość wyświetlacza. Dzięki temu, wyświetlacz będzie pokazywał tylko obiekty w granicach świata.
Ostatnie trzy linijki odpowiadają za to, że gwiazdy i księżyc przesuwają się wolniej niż ziemia i drzewa. OK, postaram wyjaśnić jak to działa. Przede wszystkim, drzewa i ziemia nie poruszają się w ogóle. Porusza się wyświetlacz, co powoduje, że patrzącemu wydaje się, że poruszają się obiekty. To tak jak po paru godzinach w PKP patrząc przez okno wydaje się, że to pola i lasy pędzą obok stojącego pociągu. To tylko iluzja. Natomiast gwiazdy i księżyc naprawdę się przesuwają. Ale przesuwają się wolniej niż wyświetlacz, przez co po pewnym czasie zostają w tyle! Wystarczy co każdą klatkę trochę zwiększyć wartość ich pola x, przez co zostają w wyświetlaczu dłużej. Dzięki temu uzyskiwany jest ten przyjemny dla oka efekt, że znajdujące się daleko obiekty poruszają się wolniej. OK, tyle teorii, teraz praktyka
Najpierw obliczam o ile pikseli przesunął się wyświetlacz. Od jego poprzedniej wartości x odejmuję aktualną. (poprzednią wartość zapisuję co każdą klatkę, aby mieć do niej łatwy dostęp). Gdy już to wiem, przesuwam gwiazdy i księżyc o trochę mniej. I gotowe! Do tego mechanizm ten działa niezależnie od tego w którą stronę przesuwa się wyświetlacz. Jeżeli przesunie się w lewo, wartości będą ujemne i wszystko zadziała jak należy, gwiazdy ‚wrócą’ na swoje miejsce.
Ostatnia linijka metody update, to wywołanie metody draw.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
this.draw = function() { if(this.entities.length !== 0){ this.ctx.save(); this.ctx.translate(-this.screen.x, -this.screen.y); for (var i = 0; i < this.entities.length; i++) { var entity = this.entities[i]; if(entity.facing){ this.ctx.drawImage(this.masterSprite,entity.sourceX+entity.width,entity.sourceY,entity.sourceWidth,entity.sourceHeight,Math.floor(entity.x),Math.floor(entity.y),entity.width,entity.height); } else { this.ctx.drawImage(this.masterSprite,entity.sourceX,entity.sourceY,entity.sourceWidth,entity.sourceHeight,Math.floor(entity.x),Math.floor(entity.y),entity.width,entity.height); } } this.ctx.restore(); } }; |
To już prawie koniec. Ta metoda nie powinna być zaskoczeniem dla kogoś kto zna moje poprzednie projekty. Po prostu wyrysowuję wszystkie obiekty, w ich aktualnym stanie, na płótnie. Jeżeli obiekt posiada pole facing, to znaczy, że jet czarownicą i trzeba narysować inny niż zwykle obrazek.
Została tylko jedna rzecz do omówienia, Na początku pojawia się metoda kontekstu save a na końcu restore. Metody te służą do kolejno, zapamiętania aktualnego punktu na który wskazuje x:0 i y:0 oraz do przywrócenia go. Potrzebne są mi, ponieważ zmieniam położenie tego punktu, robię to metodą translate. Metoda translate przyjmuje dwa parametry, po jej wywołaniu, punkt o współrzędnych określony tymi dwoma parametrami, jest teraz przez płótno traktowany jako punkt 0,0.
Co ja przekazuję metodzie translate? Aktualny x oraz y wyświetlacza. Ale odwracam te wartości, co oznacza, że punkt rysowania zostanie przesunięty o x wyświetlacza w lewo i o y wyświetlacza w górę. Potem gdy obiekt background zostanie narysowany względem tego punktu, w wyświetlaczu pokaże się dokładnie to co chcę aby się pokazało.
To własnie ostatni kawałek układanki. Po przestudiowaniu tego kodu parę razy, na pewno zobaczycie, jak prosta idea kryje się za przesuwanym światem gry. Nic nie stoi na przeszkodzie, żeby stworzyć ‚światy’ które są wyższe niż wyświetlacz. Stąd krótka droga do tworzenia platformówek i wielu innych ciekawych gier.
A co z czarownicą? Szczerze mówiąc, nie wiem. Jeśli macie jakieś pomysły na to jak rozwinąć ten projekt, podzielcie się nimi w komentarzach. Można też kontaktować się ze mną przez moją stronę na facebooku. Przy okazji zachęcam mocno do polubienia. Dzięki temu na pewno będziesz na bieżąco ze wszystkimi nowinkami z bloga. Nowe posty już niedługo 🙂
Iiififjfjj