W dzisiejszym odcinku zajmiemy się stworzeniem prostej gry "memory".
Gra taka polega na odkrywaniu par obrazków.
Do stworzenia naszej aplikacji po raz kolejny wykorzystamy jQuery. Nie jest to konieczne, gdyż
poniższy kod bez problemu można napisać w czystym javascript.
Tradycyjnie rozpiszmy zasadę działania naszej gry:
Skoro określiliśmy co mamy do zrobienia, przejdźmy do pracy.
Szkielet aplikacji będzie bardzo prosty. Sprowadzać się będzie tylko do planszy, punktacji i przycisku startującego grę. Reszta to czysta dynamika napisana w JS.
<div class="plansza"> ...tutaj dynamicznie umieścimy kafelki </div> <div class="punktacja"> </div> <button class="startGame">Rozpocznij grę</button>
Punktację umieszczamy poza planszą, by nie przysłaniała nam kafelków.
Aby widzieć rezultaty nadajmy naszym elementom przykładowe style:
.plansza {
position: relative;
background: #fff;
overflow: hidden;
width: 440px;
height: 355px;
box-shadow: 1px 1px 1px #fff, inset 1px 1px 6px rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.kafelek {
position: absolute;
float: left;
width: 80px;
height: 80px;
background: #f6da17 url(title.png) center center no-repeat;
margin: 5px;
border-radius: 5px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2), inset 1px 1px 1px #fff, 1px 1px 3px rgba(0,0,0,1);
}
W tej chwili nasze elementy wyglądają następująco:
Jedyną rzeczą która warta jest uwagi to centrowane przezroczyste tło kafelka, które jest obrazkiem znaku zapytania:
Aby "odsłaniać" kafelki zastosujemy pewien trick. Nasz kafelek jak przed chwilą określiliśmy ma domyślne centrowane tło "znaku zapytania".
Po kliknięciu na dany kafelek podmienimy to tło na odpowiedni inny obrazek dzięki czemu uzyskamy efekt "odsłonięcia".
Każdy para kafelków na planszy będzie miała swoje tło.
Naszą pracę zaczniemy od stworzenia wymieszanej tablicy numerów kafelków (które określać będą właśnie tła dla danego kafelka), umieszczenia kafelków na planszy, a następnie przypisania im zdarzenia click, które będzie sprawdzać czy wybrana para jest taka sama czy też nie (jak to mają w zwyczaju mawiać Amerykańscy naukowcy).
Zaczynamy!
Na sam początek stwórzmy kilka zmiennych i stałych:
const LICZBA_KAFELKOW = 20;
const KAFELKI_NA_RZAD = 5;
var kafelki = [];
var pobraneKafelki = [];
var liczbaRuchow = 0;
var obrazkiKafelkow = [
'title_1.png',
'title_2.png',
'title_3.png',
'title_4.png',
'title_5.png',
'title_6.png',
'title_7.png',
'title_8.png',
'title_9.png',
'title_10.png'
]
Stała LICZBA_KAFELKOW określa jak sama nazwa wskazuje liczbę kafelków. Pamiętajmy, że liczba ta jest dzielona na 2, gdyż nasze kafelki występują w parach.
Stala KAFELKI_NA_RZAD określa ile kafelków będziemy pokazywać w rzędzie. Tak więc nasza plansza będzie się składała z LICZBA_KAFELKOW / KAFELKI_NA_RZAD kolumn :).
Nazwy stałych pisane są z dużych liter. Jesto to tylko ułatwiająca czytanie kodu tradycja stosowana w różnych językach (której nawiasem mówiąc warto się trzymać)
W tablicy kafelki będziemy przechowywać "typ" poszczególnych kafelków. Typy te będą występowały parami i będą okreslać po prostu numer obrazka dla danego kafelka.
liczbaRuchow określan nam ile razy użytkownik wykonał ruch. Zwykła zmienna informacyjna :)
Ostatnia zmienna obrazkiKafelkow definiuje obrazki, które będą się pojawiały na kafelkach po kliknięciu na nie. Każda para kafelków będzie posiadała inny obrazek, który określimy na podstawie tablicy kafelki :)
Przykladowe obrazki kafelków:
Metodą jaką napiszemy na początku będzie startGame.
function startGame() {
...
...
}
Pierwszym zadaniem jaki musi zrobić nasza startująca funkcja to czyszczenie wszystkich zmiennych (nasza gra może rozpoczynać się kilka razy) i stworzenie pomieszanej tablicy par numerów dla kafelków:
var plansza = $('.plansza').empty();;
kafelki = [];
pobraneKafelki = [];
moznaBrac = true;
liczbaRuchow = 0;
for (var i=0; i<LICZBA_KAFELKOW; i++) {
kafelki.push(Math.floor(i/2));
}
for (i=LICZBA_KAFELKOW-1; i>0; i--) {
var swap = Math.floor(Math.random()*i);
var tmp = kafelki[i];
kafelki[i] = kafelki[swap];
kafelki[swap] = tmp;
}
Po stworzeniu tablicy z numerami kafelków, możemy na jej podstawie wstawić kafelki na planszę:
for (i=0; i<LICZBA_KAFELKOW; i++) {
var tile = $('<div class="kafelek"></div>');
plansza.append(tile);
tile.data('cardType',kafelki[i]);
tile.css({
left : 5+(tile.width()+5)*(i%KAFELKI_NA_RZAD)
});
tile.css({
top : 5+(tile.height()+5)*(Math.floor(i/KAFELKI_NA_RZAD))
});
tile.bind('click',function() {
klikniecieKafelka($(this))
});
}
Po kliknięciu na dany kafelek będziemy pobierać numer który został jemu przypisany. Numer taki musimy więc gdzieś przechować. Idealnie do tego celu nadaje się metoda jQuery .data(). Służy ona właśnie do podpinania elementom html dodatkowych informacji. Zasadę jej działania przedstawia poniższy skrypt:
var $p = $('p');
$p.data('numer', 'Jestem ważną informacją' );
console.log( $p.data('numer') ); //wypisze "Jestem ważną informację"
Nasze kafelki pozycjonujemy absolutnie wykorzystując do tego prostą pętlę. Pomyślisz, że wystarczyło by floatować je do lewej strony i były by ładnie ułożone. Jednak takie podejście sprawiło by, że po usunięciu z planszy kafelków, reszta by się przemiszczała na ich miejsce...
Po kliknięciu na dany kafelek odpalamy jego obsługę.
Służy do tego metoda klikniecieKafelka(), którą zaraz napiszemy.
Zasada działania tej metody jest bardzo prosta.
Na samym początku sprawdzamy, czy dany kafelek nie jest już wcześniej kliknięty (czyli czy nie jest wrzucony do tablicy pobraneKafelki).
Jeżeli nie jest, wtedy go tam wrzucamy oraz ustawiamy mu odpowiednie tło, którego numer pobieramy z "data" danego kafelka.
Nastepuje wybranie 2 kafelka. Jeżeli "data" obydwu elementów tablicy pobraneKafelki jest taka sama, znaczy to, że para
kafelków została dopasowana. Odpalamy więc funkcję usunKafelki(). Jeżeli "data" jest różna, znaczy to że kafelki są różne, więc musimy je ukryć na nowo. Służy do tego metoda
zresetujKafelki.
Obie metody odpalamy z opuźnieniem 500ms, tak by wybrane kafelki nie ukrywały się od razu :)
function klikniecieKafelka(element) {
if (pobraneKafelki.indexOf(element)==-1) {
pobraneKafelki.push(element);
element.css({'background-image' : 'url('+obrazkiKafelkow[picked.data('cardType')]+')'})
}
if (pobraneKafelki.length == 2) {
moznaBrac = false;
if (pobraneKafelki[0].data('cardType')==pobraneKafelki[1].data('cardType')) {
setTimeout('usunKafelki()', 500);
} else {
setTimeout('zresetujKafelki()', 500);
}
liczbaRuchow++;
}
}
}
W postaci takiej jak powyższa nasz skrypt nie będzie działał prawidłowo. Wyobraź sobie sytuację: gracz wybiera 2 kafelki, zostaje odpalona z opuźnieniem funkcja usunKafelki lub zresetujKafelki. Jednak w tym samym czasie
gracz może spokojnie klikać resztę kafelków i nasze działania zostają zakłócone.
Aby to naprawić dodamy do naszego skryptu dodatkową zmienną moznaBrac. I nie - nie określa ona podejścia typowego polityka :)
const LICZBA_KAFELKOW = 20;
const KAFELKI_NA_RZAD = 5;
var kafelki = [];
var pobraneKafelki = [];
var liczbaRuchow = 0;
var moznaBrac = true;
....
function klikniecieKafelka(element) {
if (moznaBrac) {
if (pobraneKafelki.indexOf(element)==-1) {
pobraneKafelki.push(element);
element.css({'background-image' : 'url('+obrazkiKafelkow[element.data('cardType')]+')'})
}
if (pobraneKafelki.length == 2) {
moznaBrac = false;
if (pobraneKafelki[0].data('cardType')==pobraneKafelki[1].data('cardType')) {
setTimeout('usunKafelki()', 500);
} else {
setTimeout('zresetujKafelki()', 500);
}
liczbaRuchow++;
$('.moves').html(liczbaRuchow)
}
}
}
Ostatnie funckje jakie zostały nam do napisania do usunKafelki() i zresetujKafelki().
Jak widać powyżej, jeżeli oba pobrane kafelki mają taki sam "cartType", wtedy odpalamy funkcję usunKafelki.
Kafelki do usunięcia znajdują się teraz w tablicy pobraneKafelki. Wystarczy je więc usunąć, po czym na nowo
ustawić tablicę pobraneKafelki. Oczywiście po usunięciu kafelków włączamy na nowo możliwość pobrania nowej pary:
function usunKafelki() {
pobraneKafelki[0].fadeOut(function() {
$(this).remove();
});
pobraneKafelki[1].fadeOut(function() {
$(this).remove();
moznaBrac = true;
pobraneKafelki = new Array();
});
}
Czemu resetowanie zmiennych i usuwanie obiektów robimy wewnątrz fadeOut()? Ponieważ chcemy te działania przeprowadzić dopiero PO zakończeniu animacji fadeOut. A jak wiemy z jQApi, wystarczy wewnątrz deklaracji fadeOut podać funckję, by ta została odpalona po zakończeniu animacji zanikania :)
Funckja resetująca kafelki do stanu zakrytego jest jeszcze prostsza. Oba kafelki wracają po prostu do statu początkowego, po czym tak samo jak powyżej ustawiamy na nowo talicę pobraneKafelki i zmienną moznaBrac.
function zresetujKafelki() {
pobraneKafelki[0].css({'background-image':'url(title.png)'})
pobraneKafelki[1].css({'background-image':'url(title.png)'})
pobraneKafelki = new Array();
moznaBrac = true;
}
Ostatnimi rzeczami którymi się zajmiemy to pokazywanie graczowi liczby ruchów które wykonał oraz zakończnie gry.
Liczenie ruchów to nic tródnego. Przy każdym kliknięciu na kafelek po prostu zwiększamy zmienną, którą dodamy do naszego skryptu:
const LICZBA_KAFELKOW = 20; const KAFELKI_NA_RZAD = 5; var kafelki = []; var pobraneKafelki = []; var moznaBrac = true; var liczbaRuchow = 0; ... function klikniecieKafelka(element) { if (moznaBrac) { ... if (pobraneKafelki.length == 2) { ... liczbaRuchow++; $('.moves').html(liczbaRuchow) } } }
Pamiętajmy też, żeby przy starcie gry wyzerować nasz licznik:
function startGame() {
kafelki = [];
pobraneKafelki = [];
moznaBrac = true;
liczbaRuchow = 0;
...
$('.moves').html(liczbaRuchow)
}
Pozostało nam do wykonania sprawdznie czy użytkownik nie odkrył wszystkich kafelków i tym samym nie zakończył gry. Wysatrczy wprowadzić dodatkową zmienną, w której będziemy
przechowywać liczbę odkrytych par. Jeżeli liczba ta będzie >= połowie wszystkich kafelków znaczy to, że gra została zakończona :)
Oczywiście sprawdzenie takie wykonujemy tylko przy usunięciu kafelków z planszy:
const LICZBA_KAFELKOW = 20;
const KAFELKI_NA_RZAD = 5;
var kafelki = [];
var pobraneKafelki = [];
var moznaBrac = true;
var liczbaRuchow = 0;
var paryKafelkow = 0;
function usunKafelki() {
pobraneKafelki[0].fadeOut(function() {
$(this).remove();
});
pobraneKafelki[1].fadeOut(function() {
$(this).remove();
paryKafelkow++;
if (paryKafelkow >= LICZBA_KAFELKOW / 2) {
alert('gameOver!');
}
moznaBrac = true;
pobraneKafelki = new Array();
});
}
Tak jak poprzednio, nie zapomnijmy wyzerować naszej zmiennej przy starcie gry
function startGame() {
kafelki = [];
pobraneKafelki = [];
moznaBrac = true;
liczbaRuchow = 0;
paryKafelkow = 0;
...
$('.moves').html(liczbaRuchow)
}
Naszą pracę kończymy podpięciem pod przycisk rozpoczęcia gry:
$(document).ready(function() {
$('.start_game').click(function() {
startGame();
});
})
Cały kod naszej aplikacji wygląda teraz tak:
HTML:
<div class="plansza">
</div>
<div class="moves"></div>
<button class="start_game">Rozpocznij Grę</button>
CSS:
body {
padding: 10px;
}
.plansza {
position: relative;
background: #fff;
overflow: hidden;
width: 440px;
height: 355px;
box-shadow: 1px 1px 1px #fff, inset 1px 1px 6px rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.kafelek {
position: absolute;
float: left;
width: 80px;
height: 80px;
background: #f6da17 url(title.png) center center no-repeat;
margin: 5px;
border-radius: 5px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2), inset 1px 1px 1px #fff, 1px 1px 3px rgba(0,0,0,1);
}
Javascript:
const LICZBA_KAFELKOW = 20;
const KAFELKI_NA_RZAD = 5;
var kafelki = [];
var pobraneKafelki = [];
var moznaBrac = true;
var liczbaRuchow = 0;
var paryKafelkow = 0;
var obrazkiKafelkow = [
'title_1.png',
'title_2.png',
'title_3.png',
'title_4.png',
'title_5.png',
'title_6.png',
'title_7.png',
'title_8.png',
'title_9.png',
'title_10.png'
];
function startGame() {
kafelki = [];
pobraneKafelki = [];
moznaBrac = true;
liczbaRuchow = 0;
paryKafelkow = 0;
var plansza = $('.plansza').empty();
for (var i=0; i<LICZBA_KAFELKOW; i++) {
kafelki.push(Math.floor(i/2));
}
for (i=LICZBA_KAFELKOW-1; i>0; i--) {
var swap = Math.floor(Math.random()*i);
var tmp = kafelki[i];
kafelki[i] = kafelki[swap];
kafelki[swap] = tmp;
}
for (i=0; i<LICZBA_KAFELKOW; i++) {
var tile = $('<div class="kafelek"></div>');
plansza.append(tile);
tile.data('cardType',kafelki[i]);
tile.css({
left : 5+(tile.width()+5)*(i%KAFELKI_NA_RZAD)
});
tile.css({
top : 5+(tile.height()+5)*(Math.floor(i/KAFELKI_NA_RZAD))
});
tile.bind('click',function() {klikniecieKafelka($(this))});
}
$('.moves').html(liczbaRuchow);
}
function klikniecieKafelka(element) {
if (moznaBrac) {
if (pobraneKafelki.indexOf(element)==-1) {
pobraneKafelki.push(element);
element.css({
'background-image' : 'url('+obrazkiKafelkow[element.data('cardType')]+')'
})
}
if (pobraneKafelki.length == 2) {
moznaBrac = false;
if (pobraneKafelki[0].data('cardType') == pobraneKafelki[1].data('cardType')) {
setTimeout('usunKafelki()', 500);
} else {
setTimeout('zresetujKafelki()', 500);
}
liczbaRuchow++;
$('.moves').html(liczbaRuchow)
}
}
}
function usunKafelki() {
pobraneKafelki[0].fadeOut(function() {
$(this).remove();
});
pobraneKafelki[1].fadeOut(function() {
$(this).remove();
paryKafelkow++;
if (paryKafelkow >= LICZBA_KAFELKOW / 2) {
alert('gameOver!');
}
moznaBrac = true;
pobraneKafelki = new Array();
});
}
function zresetujKafelki() {
pobraneKafelki[0].css({'background-image':'url(title.png)'})
pobraneKafelki[1].css({'background-image':'url(title.png)'})
pobraneKafelki = new Array();
moznaBrac = true;
}
$(document).ready(function() {
$('.start_game').click(function() {
startGame();
});
})
I na tym kończy się kolejna porcja kodowania. Gotową grę możesz zobaczyć tutaj :)