We wcześniejszych wpisach pokazywałem już jak napisać prostą grę w jQuery, popularnym javascriptowym frameworku. Przykładem może być moja gra tekstowa lub wariacja na temat space invaders. Fakt, jQuery nie sprawuje się tak dobrze jak canvas. Ma znacznie uboższe możsliwośći animacji. Jest jednak, w moim mniemaniu, mniej skomplikowane i do prostych gier nadaje się idealnie.
Weekendowy spacer w lesie zainspirował mnie do stworzenia takiej właśnie prostej gry – układanki. Celem jest ułożenie elementów w odpowiedniej kolejności, tak aby pokazywały zdjęcie. Najlepiej zrobić to w jak najkrótszym czasie. Sprawa jest o tyle trudna, że można przesuwać tylko po jednym klocku jeśli akurat obok niego jest wolne miejsce. Elementy można przesunąć klikając na nich. Gra daje również możliwość wyboru poziomu zaawansowania. Dostępne są poziomy: „łatwy”, „średni” i „trudny”. Po ułożeniu zdjęcia, gra wyświetla wynik, czyli czas jaki zajęło graczowi ukończenie układanki.
Nie wykorzystałem w tym projekcie bardzo skomplikowanych technik. Jak zwykle wystarczy podstawowa znajomość JavaScriptu oraz jQuery. Postaram się w miarę dokładnie przedstawić cały kod. Jeżeli, nie będziesz pewny / pewna jak dany fragment działa, śmiało pytaj w komentarzu. Warto też pytać o pomoc wujka google 🙂 Jeżeli chodzi o jQuery, zachęcam do czytania dokumentacji tej biblioteki (język angielski).
Pełny kod zobaczyć można klikając na stronie z grą prawym przyciskiem i wybierając opcję „wyświetl źródło strony”. Będzie tam widoczne wszystko co nas interesuje, ponieważ zarówno kod JS jak i style CSS zostały umieszczone w dokumencie HTML. Zwracam na to uwagę, ponieważ w tym wpisie, będę omawiał jedynie kod JavaScriptu i jQuery. Po wszystko inne trzeba sobie zajrzeć do źródła strony :).
opis kodu
Moja układanka będzie obiektem JavaScriptowym o nazwie Puzzle. Konstruktor tego obiektu przyjmuje trzy parametry. Pierwsze dwa to elementy HTML podane przez jQuery: plansza dla puzzli oraz miejsce do wyświetlania upływającego czasu. Trzeci parametr to adres obrazka, z którego powstanie układanka. Obrazek powinien być kwadratowy, najlepiej w wymiarach 600px na 600px.
1 2 3 4 5 6 7 8 9 10 11 12 |
var Puzzle = function(container, infoContainer, picture) { var that = this; this.infoBoard = infoContainer; this.picPath = picture; this.board = container; this.boardSize = container.width(); this.startMenu = "<div id='menuElement'><h1>WYBIERZ POZIOM TRUDNOŚCI ABY ROZPOCZĄĆ GRE</h1><h2>UŁÓŻ UKŁADANKĘ JAK NAJSZYBCIEJ</h2><input type='button' data-difficulty-level='3' value='EASY'><input type='button' data-difficulty-level='4' value='MEDIUM'><input type='button' data-difficulty-level='5' value='HARD'></div>" this.finishMenu = "<div id='menuElement'><h1>Gratulacje! Ukończyłes gre!</h1><h2></h2><input type='button' value='Nowa Gra'></div>"; this.pieceNum = 0; this.pieceSize = 0; this.emptyPiece = { }; this.Timer; |
Na początku tworzę zmienną that, której będę używał w innych scopeach w obiekcie, takich jak eventy lub setInterval. Do kolejnych trzech pól przypisuje wartości parametrów konstruktora. Następne pole, to szerokość elementu, który zawierać będzie układankę. Następnie definuje dwa pola zawierające kod HTML. pierwsze z nich będzie wyświetlane jako menu startowe gry. Drugie to gratulacje, które pojawią się gdy gracz skończy rozgrywkę. Warto zwrócić uwagę, że menu startowe zawiera trzy elementy input, każdy z nich odpowiada innemu poziomowi trudności, a to dzięki atrybutom, które sam stworzyłem data-difficulty-level. Kolejne trzy pola, są puste. Otrzymają wartości później, będą zależne od wybranego poziomu trudności. Na koniec definiuję, zmienną, która przechowywać będzie interval mierzący czas gry.
Przyszła kolej na dwie pierwsze metody, setLevelDetails oraz renderStart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
this.setLevelDetails = function(num){ this.pieceNum = num; this.pieceSize = Math.floor(this.boardSize/this.pieceNum); this.emptyPiece = { left:this.boardSize - this.pieceSize, top:this.boardSize - this.pieceSize } } this.renderStart = function() { this.board.html(""); this.board.css({"background-image": "url("+this.picPath+")"}) this.board.append(this.startMenu); jQuery("div#menuElement input").on("click", function(){ that.setLevelDetails(jQuery(this).attr("data-difficulty-level")); that.initPieces(); }) } |
Zacznę od renderStart. Ta metoda, wywoływana będzie aby zainicjalizować grę, oraz gdy po zakończonej rozgrywce, gracz kliknie ‚nowa gra’. Nie dzieje się w niej nic skomplikowanego. Najpierw html głównego elementu układanki jest opróżniany a następnie jako jego tło ustawiany jest obrazek. Kolejny krok, to dodanie menu startowego, czyli zawartości pola startMenu. Jest to tekst witający gracza, plus trzy przyciski do wybrania poziomu gry. Do przycisków dodany zostaje event on click. Po przyciśnięciu przycisku wywoływana jest metoda setLevelDetails z parametrem równym atrybutowi data-difficulty-level a następnie metoda initPieces.
Co takiego robi metoda setLevelDetails? Chodzi o ustawienie wartości, które są zależne od wybranego poziomu trudności. Pole obiektu Puzzle – pieceNum otrzymuje wartość przekazaną w parametrze. To pole będzie często wykorzystywane, oznacza ilość elementów w jednym rzędzie układanki. Im wyższy poziom gry wybrał gracz, tym więcej będzie części układanki. Kolejne pole ustawione w metodzie setLevelDetails to pieceSize, czyli rozmiar jednego klocka układanki. Ta wartość też często się przyda. Jest obliczana, przez podzielenie rozmiaru boku elementu całej układanki przez ilość klocków w jednym rzędzie (czyli pole pieceNum). Ostatnie pole, które ustawiam to emptyPiece. Jest to obiekt, zwierający dwa własne pola: top oraz left. Te wartości opisują lokacje pustego miejsca w układance, na to miejsce będzie można przesuwać inny klocek. Wartości top i left to rozmiar całej układanki minus rozmiar klocka. W ten sposób, pusty element będzie na początku ulokowany w prawym, dolnym rogu.
Kolejna metoda to initPieces. W niej tworze klocki układanki, oraz umieszczam je w odpowiedniej kolejności na planszy. Oto kod:
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 |
this.initPieces = function() { this.board.css({"background-image":"none"}) this.board.empty(); var counter = 0; for (var i = 0; i < this.pieceNum; i++) { for (var j = 0; j < this.pieceNum; j++) { var piece = $("<div>"); piece.addClass("puzzPiece"); piece.attr("data-piece-number", counter++); piece.css({"position":"absolute", "top":i*this.pieceSize, "left":j*this.pieceSize, "width":(this.pieceSize-1)+"px", "height":(this.pieceSize-1)+"px", "background-image": "url("+this.picPath+")", "background-position": (-j*this.pieceSize)+"px "+(-i*this.pieceSize)+"px", "border":"1px solid black"}); this.board.append(piece); } } this.board.find(".puzzPiece:last-child").remove(); this.shufflePieces(); this.board.find(".puzzPiece").on("mousedown", function(){ that.checkPiece(jQuery(this)); }) } |
Najpierw szykuję miejsce na układankę. Usuwam tło oraz cały html znajdujący się w głównym elemencie. Następnie dwoma pętlami for zaczynam ustawiać elementy. Pętle iterują tyle razy, ile wynosi wartość pola pieceNum. W każdym obiegu tworzony jest nowy klocek układanki. Jest too nowy element div, którego style position ustawiony jest na absolute. Do każdego dodana jest klasa puzzPiece oraz atrybut data-piece-number, którego wartość to liczba inkrementująca się co obieg od zera.
Następnie na podstawie numeru iteracji zewnętrznej i wewnętrznej pętli oraz rozmiaru klocka obliczane są wartości styli top (zewnętrzna pętla) oraz left (wewnętrzna pętla). To sprawia, że klocki wypełniają główny element. Ponieważ chcę aby klocki były otoczone ramką o szerokości jednego piksela, dlatego, aby wszystko się pomieściło, muszę od wysokości i szerokości odjąć po jednym pikselu. Przyszła kolej na główny trick, tego programu, czyli jak to jest, że obrazek z układanki jest dzielony i mieszany tak jakby zamieniał się w wiele mały obrazków. Otóż, naprawdę jest to wiele obrazów! Każdy element otrzymuje ten sam obrazek jako tło, trick polega na ulokowaniu tego tła w każdym z elementów tak, że połączone, wyglądają jak jeden obraz. Osiągam to dzięki stylowi background-position. Jego wartości też są uzyskiwane dzięki przemnożeniu numeru iteracji przez rozmiar klocka. Prawie tak jak top i left, z tą różnicą, że tym razem wartości te są ujemne.
Kiedy wszystkie klocki są już na planszy, usuwam ostatni z nich. To będzie puste miejsce, na które będzie można przesuwać inne klocki. Gdy to wszystko jest już gotowe, wywoływana jest metoda shufflePieces oraz do każdego elementu przypisywany jest event on click, który powoduje, że po kliknięciu wywoływana jest metoda checkPiece.
Metoda checkPiece oraz metoda switchPiece zawierają kod, który powoduje, że klocki poruszają się po układance.
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.checkPiece = function(piece) { var currTop = Number(piece.css("top").split("px")[0]); var currLeft = Number(piece.css("left").split("px")[0]); if(currLeft === this.emptyPiece.left && currTop - this.pieceSize === this.emptyPiece.top) { this.switchPiece(piece,currTop,currLeft); return false; } else if (currTop === this.emptyPiece.top && currLeft + this.pieceSize === this.emptyPiece.left) { this.switchPiece(piece,currTop,currLeft); return false; } else if (currTop + this.pieceSize === this.emptyPiece.top && currLeft === this.emptyPiece.left) { this.switchPiece(piece,currTop,currLeft); return false; } else if (currTop === this.emptyPiece.top && currLeft - this.pieceSize === this.emptyPiece.left) { this.switchPiece(piece,currTop,currLeft); return false; } else { console.log("nope"); } } this.switchPiece = function(piece,oldTop,oldLeft){ piece.animate({ top: this.emptyPiece.top+"px", left: this.emptyPiece.left+"px" }); this.emptyPiece.top = oldTop; this.emptyPiece.left = oldLeft; } |
Metoda checkPiece jest wywoływana po kliknięciu na klocek układanki, jako parametr otrzymuje ten właśnie klocek. Pierwsza rzecz jaka się tu dzieje, to ustawienie zmiennych, currLeft oraz currTop. Są one wyciągane ze stylów elementu przekazanego jako argument. Warto zwrócić uwagę, że od wartości odcinam litery „px” oraz zamieniam to co zostało na wartość Number, to dlatego, że top i left pustego elementu to liczby a nie łańcuchy znaków. Gdy te dwie zmienne są już gotowe, sprawdzam po kolei, czy któryś z czterech elementów stykających się bezpośrednio z klikniętym klockiem nie jest czasem pustym elementem. Jeżeli tak, wywoływana jest metoda switchPlace.
Metoda switchPlace wykorzystuje animacje jQuery. Animuje zmianę styli css top oraz left klikniętego klocka na te wartości z pustego elementu. Następnie pustemu elementowi przypisywane są wartości top i left klikniętego. W ten sposób zamieniają się miejscami.
Kolejna metoda to shufflePieces. Ta metoda, jak nazwa wskazuje, miesza znajdujące się na planszy klocki, aby gracz, mógł je poukładać 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
this.shufflePieces = function() { var moveTimes = 150*that.pieceNum; var i = 0; var shufflingMessages = ['Mieszam','Mieszam.','Mieszam..','Mieszam...']; var intervalID = setInterval(function(){ if(moveTimes%15 == 0){ that.infoBoard.find("p#shufflingMessge").text(shufflingMessages[i++]) if(i>3) i = 0; } var randomPieceNumber = Math.floor(Math.random()*(that.pieceNum*that.pieceNum-1)); that.checkPiece(jQuery("div[data-piece-number="+randomPieceNumber+"]")); moveTimes--; if(!moveTimes) { window.clearInterval(intervalID); that.startTimer(); } },10); } |
Jak to działa? Na początku ustawiam wartość zmiennej moveTimes na poziom trudności, czyli liczbę klocków w jednym rzędzie, przemnożony przez 150. Następnie tworzę zmienną i, która na początku wynosi zero, oraz tablicę, która przechowuje cztery elementy, łańcuchy znaków równe „Mieszam” plus parę kropek.
Gdy to jest już gotowe, uruchamiam interval, które co jedną setną sekundy robi następujące rzeczy jeśli moveTimes, akurat dzieli się przez 15 bez reszty, podnoszę licznik i oraz wyświetlam element tablicy shufflingMessages znajdujący się pod indeksem równym licznikowi i. Jeśli i jest większe od trzech zostaje wyzerowane. Dzięki temu, co piętnaście wywołań setInterval, zmienia się treść komunikatu mieszam, powodując efekt animowanych trzech kropek.
Następnie wybieram losowo liczbę z przedziału od zera do kwadratu pieceNum minus jeden. Znajduję element, którego atrybut data-piece-number (wartość ustawiona podczas tworzenia klocków) jest równy tej liczbie i próbuję go przesunąć, wywołując metodę checkPiece, używając wylosowanego elementu. I tak 150 pomnożone przez poziom trudności razy 🙂 Wystarczy aby porządnie pomieszać wszystkie klocki, a zarazem daje pewność, że cały proces jest odwracalny.
Pod koniec każdego wywołania setInterval, zmniejszam wartość moveTimes o jeden. Gdy dojdzie do zera, zatrzymuję setInterval i odpalam metodę startTimer
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 |
this.startTimer = function(){ this.infoBoard.find("p#shufflingMessge").text("00:00:00"); var tick = 0; var secondsPassed = 0; var minutesPassed = 0; var hoursPassed = 0; var timerText = ""; this.Timer = setInterval(function(){ tick++; if(tick%4 === 0) { secondsPassed++; } timerText = ""; if(secondsPassed > 60) { secondsPassed = 0; minutesPassed++; } if(minutesPassed > 60) { minutesPassed = 0; hoursPassed++; } (hoursPassed<10) ? timerText +=("0"+ hoursPassed) : timerText +=hoursPassed; timerText += ":"; (minutesPassed<10) ? timerText +=("0"+ minutesPassed) : timerText +=minutesPassed; timerText += ":"; (secondsPassed<10) ? timerText +=("0"+ secondsPassed) : timerText +=secondsPassed; that.infoBoard.find("p#shufflingMessge").text(timerText); if(that.checkIfWon()) { that.renderEnd(timerText); } }, 250) } |
Ta metoda służy to uruchomienia licznika czasu, jaki graczowi zajmie ukończenie układanki. Ten licznik, również będzie zawierał kod sprawdzający czy gra już została zakończona. Do stworzenia licznika użyłem, oczywiście setInterval. Lecz zanim zostanie zadeklarowana, najpierw wstawiam tekst przedstawiający same zera w miejsce, w którym pojawiał się tekst informujący o mieszaniu klocków. Następnie deklaruje cztery zmienne. pierwsza służy do liczenia ilości wywołań, setInterval, kolejne będą przechowywać ilość sekund, minut oraz godzin spędzonych nad układanką. Następnie odpalam setInterval, kod będzie wywoływany co jedną czwartą sekundy. Można zastanowić się, dlaczego nie co sekundę, skoro ma mierzyć czas. Wyjaśnię to na koniec.
Z każdym wywołaniem setInterval inkrementuję wartość zmiennej tick. Co cztery wywołania, inkrementuję wartość zmiennej secondsPassed. Następnie mam dwa wyrażenia warunkowe. Pierwsze sprawdza czy liczba sekund nie przekroczyła 60, jeśli tak, są one zerowane a liczba minut zostaje podniesiona o jeden. Drugie, robi to samo lecz z minutami i godzinami. Kolejna część kodu zajmuje się tworzeniem łańcucha znaków przedstawiającego wartości godzin, minut oraz sekund rozdzielonych dwukropkami. Sprawdzam przy okazji tworzenia łańcucha czy któraś z wartości nie jest mniejsza niż 10. Jeżeli jest dodaje przed nią zero, tak aby wszystko prezentowało się tak jakby był to prawdziwy stoper. Kiedy łańcuch znaków jest gotowy, zostaje wyświetlony obok planszy gry.
Ostatnie trzy linijki sprawdzają czy układanka została ułożona. Jeżeli metoda checkIfWon zwróci wartość true, wywołana zostanie metoda renderEnd. Dlatego chciałem aby setInterval był wywoływany co jedną czwartą sekundy zamiast co sekundę. Dzięki temu, gra będzie sprawdzana na tyle często, że gdy klocki zostaną ułożone w odpowiedniej kolejności, zatrzyma się. Gdyby czas ten był dłuższy, gracz mógłby zdążyć zrobić coś niespodziewanego, po ułożeniu klocków. A tak nie daję mu szansy 🙂
Przejdźmy do metod checkIfWon oraz renderEnd
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 |
this.checkIfWon = function(){ var correctPieces = 0; var i = 0; var j = 0; this.board.find(".puzzPiece").each(function(){ var currPieceTop = Number((jQuery(this).css("top")).split("px")[0]); var currPieceLeft = Number((jQuery(this).css("left")).split("px")[0]); if((j!=0)&&(j%that.pieceNum === 0)){ j=0; i++; } j++; if((currPieceTop === i*that.pieceSize) && (currPieceLeft === (j-1)*that.pieceSize)){ correctPieces++; } }); if(correctPieces === (this.pieceNum*this.pieceNum-1)){ return true; } else { return false; } } this.renderEnd = function(finalTime){ clearInterval(this.Timer); this.board.find(".puzzPiece").off().css({"cursor":"default"}); this.board.html(""); this.infoBoard.find("p#shufflingMessge").text(""); this.board.css({"background-image": "url("+this.picPath+")"}) this.board.append(this.finishMenu); this.board.find("#menuElement h2").text("Czas: "+ finalTime); this.board.find("input").on("click", function(){ that.renderStart(); }) } |
Metoda checkIfWon służy do sprawdzenia czy klocki zostały ułożone w odpowiedniej kolejności. Działa to w następujący sposób: klocki są ponumerowane, każdy numer powinien mieć odpowiednie wartości styli top oraz left. Dokładnie takie, jakie przypisane były im przez zagnieżdżone pętle for w metodzie initPieces.
Tutaj będę jednak używał metody jQuery each, za pomocą której sprawdzę każdy klocek. Na szczęście nie jest trudno zasymulować wartości liczników zagnieżdżonych pętli for i to właśnie robię. Po kolei sprawdzam czy style elementów się zgadzają, jeżeli tak, podnoszę wartość zmiennej correctPieces o jeden.
Na koniec sprawdzam czy liczba correctPieces jest równa, liczbie wszystkich klocków. Jeżeli tak, metoda zwraca wartość true, co z kolei spowoduje wywołanie metody renderEnd.
renderEnd działa podobnie do renderStart. Usuwam w niej elementy układanki, a na planszy gry jako tło
ustawiam cały obrazek. Wyświetla się treść pola finishMenu, czyli gratulacje dla gracza oraz przycisk. Oprócz tego metoda wypisuje także czas jaki zajęło graczowi ukończenie układanki. Na koniec do przycisku przypisany jest event on click, który powoduje, że po kliknięciu zostanie wywołana metoda renderStart i zabawa zacznie się od nowa 🙂
I tak dochodzimy do końca. Starałem się wyjaśnić wszystko w miarę jasno, ale zdaję sobie sprawę, że momentami mogłem podawać informacje skrótowo. Może i dobrze, ten wpis i tak jest rekordowo długi 🙂 W każdym razie, jeżeli coś jest nie jasne, zachęcam do zostawiania komentarzy, chętnie wyjaśnię wszelkie nieścisłości.
Jeżeli podobał Ci się ten wpis, polub moje konto na FaceBooku. Dzięki temu będziesz zawsze na bieżąco z nowymi wpisami 🙂