Stary projekt zakończony, czas na coś nowego. Dalsze zabawy z elementem canvas zaowocowały kilkoma pomysłami. Jednym z tych pomysłów było odtworzenie popularnej niegdyś gry snake.
Oto wyniki mojej pracy.
Jak widać nie jest to jeszcze kompletna gra. Tak naprawdę, daleko jeszcze do tego 🙂 Póki co mój snake, porusza się tylko po zamkniętej przestrzeni. Za pomocą strzałek możena zmienić kierunek w którym podąża. I póki co tyle… Jednak mam tu już dość kodu aby móc go przeanalizować. Obecnie jest to praktycznie tylko jeden obiekt, Snake, który zawiera całą potrzebną nam logikę.
Oczywiście w finalnej wersji gry, obiekt ten może wyglądać zupełnie inaczej, ale zrozumienie tego co jak działa już na tym etapie, na pewno pomoże.
Mój Snake w HTML5 – analiza kodu
Cała magia dzieje się dzięki funkcji init, która odpala się podczas eventu onload elementu body
1 |
<body onload="init()"> |
A treść funkcji wygląda tak
1 2 3 4 5 6 7 8 9 10 11 |
function init(){ function Snake(ctx, canvas) { //tu tresc konstruktora Snake } var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); var wunsz = new Snake(ctx, canvas); wunsz.initSnake(); } |
Pierwszy, dość spory kawałek kodu to funckcja – konstruktor Snake. Wywołanie jej będzie tworzyć nowy obiekt typu Snake. Następne dwie zmienne, wskazują na element canvas oraz jego kontekst. Gdy to jest już gotowe, tworzę instancje obiektu Snake, używając wcześniej zadeklarowanego konstruktora. Jako argumenty przekazuję dwie zmienne, jedną wskazującą na canvas i drugą na jego kontekst.
Na koniec wywołuję metodę obiektu Snake – initSnake.
Nic skomplikowanego. Dalej też będzie prosto 🙂 Zerknijmy do obiektu Snake.
Na pierwszy ogień pójdą pola, które w nim deklaruję:
1 2 3 4 5 6 7 |
this.canvas = canvas; this.ctx = ctx; this.speed = 150; this.size = 25; this.bodyParts = 8; this.bodyPartsHolder = []; this.direction = "right"; |
- canvas oraz ctx otrzymują wartość parametrów, przekazanych podczas tworzenia instancji obiektu
- speed będę używał podczas wywołania setInterval, jest to liczba milisekund, co którą będzie odświeżać się animacja.
- size to nic innego jak długość boku kwadrata/segmentu węża.
- bodyParts to liczba segmentów, które wąż będzie posiadał na początku
- bodyPartsHolder to pusta póki co pusta tablica. Będzie ona zawierać obiekty zawierające położenie konkretnych segmentów węża.
- direction to zmienna określająca stan węża. Od jej zawartości zależeć będzie w którą stronę porusza się wąż.
Zerknijmy teraz na metody.
Pierwsza, wywoływana w głównej funkcji init, initSnake wygląda tak:
1 2 3 4 5 6 7 8 9 10 |
this.initSnake = function(){ var that = this; for (var i=0; i < this.bodyParts; i++) { this.bodyPartsHolder.push({x:i,y:0}); }; this.makeSnakeListen(window); this.intervalID = setInterval(function(){ that.moveSnake(); },this.speed); }; |
Pierwsze co rzuca się tu w oczy to deklaracja zmiennej that, do której przypisywana jest wartość this. O co tu chodzi? No cóż… Muszę to zrobić, ponieważ dalej chcę wywołać metodę obiektu Snake w innym kontekście (setInterval). Tam this będzie już inne. Skomplikowane? Nie tak bardzo jak się wydaje. Poświęcę temu osobny wpis, póki co skupmy się na fakcie że to działa 🙂
Następnie przy pomocy pętli for, wypełniam tablice bodyPartsHolder. Każdy element to obiekt odzwierciedlający jeden segment węża. Obiekty te zawierają dwa pola, x oraz y. Te wartości określają, gdzie na canvasie będzie narysowany każdy segment wężą. Póki co wszystkie elementy maja y ustawione na 0, natomiast x-om przypisywana jest aktualna wartość licznika pętli. Oznacza to, że na początku wąż ustawiony będzie na planszy poziomo, przylegając do górnej krawędzi canvas.
Kolejną rzeczą, która dzieje się w initSnake jest wywołanie innej metody makeSnakeListen.
Na końcu ustawiam setInterval. Wywołuję metodę obiektu snake – moveSnake co speed milisekund. Można tu zauważyć, że wewnątrz setInterval nie używam zmiennej this tylko that. Wspomniałem o tym wyżej.
Warto zwrócić uwagę, że w odróżnieniu do tego co robiłem w przypadku odbijających się kulek, tutaj cały blok setInterval przypisuję do zmiennej. Dzięki temu, będę mógł później przerwać działanie.
Kolejna metoda, odpalana przez initSnake to makeSnakeListen:
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 |
this.makeSnakeListen = function (window){ var that = this; window.onkeydown = function(e){ switch(e.keyCode){ case 37: if(that.direction != "right"){ that.direction = "left"; } break; case 38: if(that.direction != "down"){ that.direction = "up"; } break; case 39: if(that.direction != "left"){ that.direction = "right"; } break; case 40: if(that.direction != "up"){ that.direction = "down"; } break; } }; }; |
Znów mam tutaj zmienną that do której przypisujemy wartość this. Robię to przed funkcją zawartą w evencie onkeydown, ponieważ w evencie this będzie inne, a ja chcę zmienić wartość pola obiektu Snake.
Ta metoda, przypisuje globalnemu obiektowi window event onkeydown. Event ten wywoływany jest przez naciśnięcie przycisku na klawiaturze. Odpala to funkcję, której przekazywany jest kod przyciśniętego klawisza.
W funkcji mamy konstrukcję switch z czterema case’ami. Do switcha przekazujemy kod przyciśniętego klawisza. Każdy case odpowiada jednej ze strzałek na klawiaturze, np 39 to kod strzałki w lewo.
Kod zmienia wartość pola direction na kierunek odpowiadający naciśniętej strzałce.
Oczywiście wąż nie może zawrócić o 180 stopni, więc przy każdym case sprawdzam, czy przypadkiem obecny direction, nie jest przeciwny do wybranego. Wartość direction zmieni się tylko jeśli nie jest to prawda.
Kolejna metoda, to serce obiektu Snake, metoda odpalana przez setInterval co 150 milisekund czyli moveSnake:
1 2 3 4 5 6 7 8 9 10 11 |
this.moveSnake = function(){ this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height); for(var i=0; i < this.bodyParts; i++) { this.ctx.beginPath(); this.ctx.rect(this.bodyPartsHolder[i].x*this.size,this.bodyPartsHolder[i].y*this.size,this.size,this.size); this.ctx.fill(); this.ctx.closePath(); }; this.updatePosition(); this.checkColissions(); }; |
Najpierw moveSnake czyści canvas, standard podczas animacji, to samo robiłem przy kulkach. Następnie rysowany jest wąż. Tutaj też nie dzieje się nic nadzwyczajnego. Używam metody kontekstu canvas do rysowania prostokątów – rect, której podawane są cztery argumenty. Pierwsze dwa to współrzędne lewego górnego wierzchołka rysowanego prostokąta, a dwie kolejne to długości jego boków.
Ponieważ mój wąż składa się z paru kwadratów (dokładna ich ilość do wartość pola bodyParts), których dane przechowuję w tablicy bodyPartsHolder (wypełnionej już elementami przez metodę initSnake, do narysowania ich wszystkich używam pętli for.
Aby wyliczyć miejsce w którym ma znajdować się konkretny segment węża, przemnażam x oraz y aktualnego obiektu z tablicy bodyPartsHolder, przez wartość pola size.
Gdy wąż jest już narysowany, odpalane są dwie kolejne metody updatePosition oraz checkColissions.
Zacznę od updatePosition:
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 |
this.updatePosition = function(){ var tempx = this.bodyPartsHolder[this.bodyParts-1].x; var tempy = this.bodyPartsHolder[this.bodyParts-1].y; var prevX = tempx; var prevY = tempy; console.log(this.direction); switch(this.direction){ case "right": this.bodyPartsHolder[this.bodyParts-1].x++; break; case "down": this.bodyPartsHolder[this.bodyParts-1].y++; break; case "left": this.bodyPartsHolder[this.bodyParts-1].x--; break; case "up": this.bodyPartsHolder[this.bodyParts-1].y--; break; } for(var i = this.bodyParts-2; i>=0;i--) { tempx = this.bodyPartsHolder[i].x; tempy = this.bodyPartsHolder[i].y; this.bodyPartsHolder[i].x = prevX; this.bodyPartsHolder[i].y = prevY; prevX = tempx; prevY = tempy; } }; |
Celem tej metody jest przesunięcie węża o jedno pole w kierunku, określanego przez aktualny stan pola direction.
Pierwszym co muszę tu zrobić to znaleźć x oraz y ‚główki’ węża, czyli jego ostatniego elementu.Wartości te przypisuję do tymczasowych zmiennych.
Następnie konstrukcja switch zmienia x lub y ‚główki’ w zależności od tego, jaką wartość ma obecnie pole direction.
W końcu, pętlą for przechodzę przez wszystkie elementy tablicy bodyPartsHolder od końca, i przypisuję każdemu aktualnie przerabianemu elementowi wartości x oraz y poprzedniego elementu. Pierwszemu przerabianemu elementowi (czyli drugiemu od końca), przypisywane są oryginalne wartości x oraz y ‚główki’. To powoduje, że przy następnym narysowaniu wąż przesunie się o jedno pole.
Ostatnią metodą do opisania, jest checkColissions:
1 2 3 4 5 6 7 8 9 |
this.checkColissions = function(){ var sneakHead = this.bodyPartsHolder[this.bodyParts-1]; if(sneakHead.x*this.size >= canvas.width || sneakHead.x < 0){ clearInterval(this.intervalID); } if(sneakHead.y*this.size >= canvas.height || sneakHead.y < 0){ clearInterval(this.intervalID); } }; |
Ta funkcja ma na celu sprawdzenie, czy wąż przypadkiem nie wpadł na coś na co wpaść nie powinien. W tej chwili są to tylko krawędzi elementu canvas (tak, snake, może póki co przechodzić przez samego siebie 🙂 )
Jeśli główka węża znajdzie się po za krawędzią canvas, wywoływana jest funkcja clearInterval. Funkcji tej przekazywane jest ID setInteval, które powinno zostać zatrzymane.
W moim przypadku jest to pole intervalID, któremu przypisany był setInteval. Czyli jeśli snake wpadnie na ścianę, gra zatrzymuje się. Jedyny sposób aby zacząć od nowa to póki co odświeżenie przeglądarki.
Warto zauważyć, że wąż zatrzymywany jest, po zaktualizowaniu jego pozycji, ale przed wyrysowaniem jej. Dzięki temu, główka węża nie wypadnie po za canvas, kiedy gracz skusi.
I to póki co cały obiekt snake a zarazem gra snake w moim wykonaniu.
Następne cele krótkodystansowe to:
- dodanie sprawdzania kolizji węża z własnym ciałem (proste, wystarczy parę dodatkowych linijek w metodzie checkColissions
- dodanie pożywienia dla węża, które będzie powodowało, że wąż rośnie (to będzie trochę większe wyzwanie, nowy obiekt?
Jeśli chodzi o cele długodystansowe, to chcę dodać stany całej gry: Menu startowe, możliwość pauzowania, punktacje dla gracza. Zobaczymy co z tego wyjdzie 🙂 Jak na razie idzie dobrze, trzymajcie kciuki.