Napisany w celach edukacyjnych. Na co dzień nie piszę w JS, więc postanowiłam sobie poćwiczyć. Zachęcam do oceny kodu, chętnie przyjmę krytykę na klatę.
Do samej mechaniki gry wykorzystałam framework Phaser.io (https://phaser.io/).
Ogólnie OK, ale jakbyś mogła popracować, bo klawisze bardzo "mulisto" reagują na wciśnięcia, długo trzeba czekać na reakcję, a jak przytrzymuję chwilę to zamiast szybkiego przesunięcia, to sobie klocek tak skacze. Nie wyobrażam sobie grać w ten sposób przy wyższych prędkościach. Wszystkie tetrisy które kojarzę, reagowały bardzo szybko i responsywnie. @furious programming może potwierdzić ;)
Tak na szybko:
- nie uzywaj
var
- uzywajconst
(lublet
jesli potrzebujesz ponownie przypisać wartość), - masz wszędzie magic numbers
- god objects (np. klasa
Game
) - mieszanie odpowiedzialności, przydługie metody, przydługie linie,
- problemy z DRY, np.
JSON.parse(JSON.stringify(this.blockShape[this.rotation]));
ciągle się powtarza (już nie wnikam czemu tak kopiujesz obiekty), -
for..in
nie słuzy do iterowania po tablicach (a to zdaje się robisz) - służy do tegofor..of
- WTF:
wait() {
//waste some time
this.r = 0;
for(var i = 0; i < 10000000; i++)
{
var a = Math.sqrt(i);
this.r += a * a;
}
}
Ogólnie cięzko mi się czyta ten kod, trochę takie spaghetti miejscami.
(już nie wnikam czemu tak kopiujesz obiekty)
Chciałam oderwać referencję, a inaczej mi nie wychodziło. Podpowiesz, jak powinno być?
WTF:
Noo tego to muszę się pozbyć :D Generalnie chodziło o to, że bez tego klocki latały jak naspidowane, przez co nie dało się grać. Jednak teraz wiem, że w efekcie np. u @cerrato skacze, zamiast działać płynnie... Więc muszę wymyślić jakiś lepszy sposób.
Nie jestem specem ani od mobilnego Tetrisa, ani od programowania aplikacji mobilnych, więc niewiele jestem w stanie doradzić w kwestii związanej z kodem źródłowym. Ogólnie to nienawidzę urządzeń mobilnych (smartfonów i tabletów) z dotykowymi ekranami. ;)
Jednak jeśli chodzi o mechanikę gry i jej grywalność, to natychmiastowa reakcja na gesty gracza jest w przypadku Tetrisa obowiązkowa – w końcu to gra zręcznościowa, w której refleks jest kwestią kluczową. W sumie tak samo obowiązkowe są specjalne ruchy (tuck – wsunięcia od boku; spin – obroty w celu wypełnienia dziur), które nie wymagają dodatkowej logiki, a powinny wynikać z dobrze napisanego silnika gry.
Jeśli chciałabyś co nieco dowiedzieć się o algorytmach dotyczących obsługi klawiszy sterowania i wykonywania kolejnych ruchów klocków to polecam artykuł Applying Artificial Intelligence to Nintendo Tetris i rozdział The Mechanics of Nintendo Tetris. Znajdziesz w nim dokładny opis mechaniki gry oraz kod źródłowy w assemblerku dla MOS6502 – co prawda nie ma on większej wartości w tym przypadku, ale w komentarzach jest podany odpowiednik w pseudokodzie podobnym do C, więc łatwo zrozumieć co jest grane.
Chodzi o ogólne rozeznanie się z algorytmami, nie o gotowce. Polecam ten artykuł dlatego że jest bardzo szczegółowy, a poza tym opisuje wg mnie najlepszą wersję Tetrisa pod względem klasycznej mechaniki (dość prostej w implementacji).
Chciałam oderwać referencję, a inaczej mi nie wychodziło. Podpowiesz, jak powinno być?
Jeśli obiekt który chcesz skopiować (this.blockShape[this.rotation]
) to tablica wyglądająca mniej więcej tak (przynajmniej tak wywnioskowałem z kodu, choć same nazwy nie pomagają):
const arr = [{ x: 1, y: 1 }, { x: 1, y: 2 }] // itd.
To możesz ją skopiować np w ten sposób:
const copy = arr.map(item => ({ ...item }))
Ale ogólnie nie powinnaś dopuścić do sytuacji gdzie takie kopiowanie jest potrzebne. Deep copy jest koszowne obliczeniowo - szczególnie gdy piszesz grę gdzie masz na wszystko te ~16ms.
Noo tego to muszę się pozbyć :D Generalnie chodziło o to, że bez tego klocki latały jak naspidowane, przez co nie dało się grać. Jednak teraz wiem, że w efekcie np. u @cerrato skacze, zamiast działać płynnie... Więc muszę wymyślić jakiś lepszy sposób.
Wykonuj ruch klocków co któryś tick gry zamiast sztucznie spowalniać pętlę gry. Dzięki temu prędkość będzie przewidywalna, a pozostałe akcje, jak sterowanie, będą niezależne od prędkości klocków.
Tu masz prymitywny przykład, z różnymi tickerami dla automatycznego progresu klocka i dla sterowania, z zachowaniem tych ~60 FPS:
- Robisz sobie obiekt z tickerami, dla każdego z nich:
- określasz inrervał - ile klatek pomiędzy poszczególnymi ruchami
- inicjalizujesz licznik / liczniki
Przykład:
const tickers = {
controls: {
interval: 10, // 6 times/s on 60FPS
counters: {
ArrowLeft: 0,
ArrowRight: 0,
},
},
game: {
interval: 60, // 1 time/s on 60FPS
counter: 0,
}
}
- W pętli gry (u Ciebie to bodajże metoda
update
) odpowiednio inkrementujesz / resetujesz liczniki i wykonujesz akcje gdy licznik zgadza się z interwałem, np zakładając, że mamy pojedynczy kwadratowy klocek (square
o rozmairzetileSize
) :
if (keyboard.ArrowLeft) {
if (tickers.controls.counters.ArrowLeft % tickers.controls.interval === 0) {
square.x -= tileSize
}
tickers.controls.counters.ArrowLeft++
} else {
tickers.controls.counters.ArrowLeft = 0
}
if (keyboard.ArrowRight) {
if (tickers.controls.counters.ArrowRight % tickers.controls.interval === 0) {
square.x += tileSize
}
tickers.controls.counters.ArrowRight++
} else {
tickers.controls.counters.ArrowRight = 0
}
if (tickers.game.counter % tickers.game.interval === 0) {
square.y += tileSize
tickers.game.counter = 0
}
tickers.game.counter++
Demo: https://codepen.io/caderek/pen/gOYXvJL?editors=0010
Oczywiście można to zrobić bardziej elegancko, ale co do idei to chyba spełnia założenia:
- nie wpływa na ogólną prędkość gry,
- elementy mają różne czasy reakcji wg potrzeb,
- reakcja na przycisk jest natychmiastowa (możliwy hyper tapping) przy zachowaniu interwałów gdy przycisk jest przytrzymany,
- można łatwo przyspieszać spadanie klocków (zwiększać level) zmniejszając
tickers.game.interval
oraz umożliwić graczowi ustawienie prędkości samopowtarzania.