Tetris v HTML5 – III. Herná logika 2/4
V tomto diely seriálu o hre Tetris budeme pokračovať v implementácii hernej logiky hry. Pridáme pohyb kocky, detekciu kolízie a rotáciu kocky.
Nastal čas, aby sme našej hre vdýchli trochu života. Začneme preto pohybom hracej kocky. Samotný pohyb sa bude prebiehať v dvoch krokoch. V prvom kroku kocku z plochy vymažeme a v druhom ju nakreslíme na novú pozíciu. Na výmaz kocky si zadefinujeme novú funkciu EreasePiece, ktorá na prekreslenie jednotlivých dielikov zavolá už definovanú funkciu DrawBlock. Funkcia jednoducho prekreslí relevantné pozície prázdnymi.
Ak by sme teraz do hry pridali volanie NewPiece do MoveNext pred return, pridal by sa nám nový diel do hry vždy, keď ten predchádzajúci dopadne na spodok hracej plochy. Po chvíli by sme si ale všimli, že kocky síce pribúdajú, ale nepadajú na seba. Dopadnú vždy len na spodok hracej plochy a navzájom sa prekrývajú. Tento stav je spôsobený tým, že kocka ktorá dopadla na spodok hracej plochy je tam len nakreslená a neexistuje o nej informácia v dátach, resp. obsah premennej ActivePiece sa zmení hneď po pridaní novej kocky do hry.Práve pre tento účel máme vytvorené pole GameArray. Pole bude obsahovať údaje o všetkých kockách ktoré už boli na ploche uložené. Tieto údaje využijeme na detekciu kolízie dvoch kociek aj vyplnenia riadka. Ako prvé je nutné vytvoriť funkciu, ktorá nám v prípade potreby bude vedieť uložiť konkrétnu kocku do poľa. Pridáme preto funkciu DockPiece ktorá nám zvolenú kocku „zakotví“ do poľa GameArray.
Ostatné texty v seriály
Zdrojový kód:
EreasePiece: function (piece)
{
var xPos = 0;
var yPos = 0; for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
yPos = piece.Top + i;
xPos = piece.Left + j;
if (piece.Blocks[i][j] === FieldStates.fs_Block)
{
this.DrawBlock(yPos, xPos, this.img_empty);
}
}
}
},
Samotná funkcia na posun kocky sa bude volať MovePiece a ako parametre budeme predávať požadovaný posun na dol a na stranu. K tretiemu parametru canDock sa vrátime neskôr.{
var xPos = 0;
var yPos = 0; for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
yPos = piece.Top + i;
xPos = piece.Left + j;
if (piece.Blocks[i][j] === FieldStates.fs_Block)
{
this.DrawBlock(yPos, xPos, this.img_empty);
}
}
}
},
Zdrojový kód:
MovePiece: function (top, left, canDock)
{
var piece = this.ActivePiece;
this.EreasePiece(piece);
piece.Top += top;
piece.Left += left;
this.DrawPiece(piece);
}
Pokračujeme vytvorením hernej slučky, pomocou funkcií GameUpdateRequest, GameUpdate a MoveNext. GameUpdateRequest bude volaná v tedy, keď dobehla posledná herná slučka a je požadované vyvolanie ďalšej. Funkcia vyráta čas, o ktorý sa má nová slučka vyvolať a pomocou zabudovanej funkcie window.setTimeout zavolá GameUpdate. Táto funkcia bude obsahovať volanie funkcie MoveNext, ktorá bude vyhodnocovať aktuálny stav hry, vyvolávať pohyb a prípadne meniť stav hry. V prípade, ak sa stav hry nezmení na pozastavenú, opätovne sa zavolá GameUpdateRequest, čím sa slučka zopakuje. Prvotné spustenie slučky spustíme vo funkcii NewGame, do ktorej pridáme zmenu stavu hry na GameStates.gs_Play a volanie GameUpdateRequest.{
var piece = this.ActivePiece;
this.EreasePiece(piece);
piece.Top += top;
piece.Left += left;
this.DrawPiece(piece);
}
Zdrojový kód:
NewGame: function ()
{
...
this.Status = GameStates.gs_Play;
this.GameUpdateRequest();
},
GameUpdateRequest: function ()
{
var LevelInterval = 500; var ActualDate = new Date();
var Interval = LevelInterval - (ActualDate - this.LastUpdate);
if (Interval < 0)
Interval = 0; window.setTimeout("Game.GameUpdate();", Interval);
},
GameUpdate: function ()
{
if (this.Status === GameStates.gs_Play)
{
this.LastUpdate = new Date();
this.MoveNext(); if (this.Status === GameStates.gs_Play)
{
this.GameUpdateRequest();
}
}
},
MoveNext: function ()
{
var Piece = this.ActivePiece;
if (Piece.Top + Piece.Height === this.GameArrayHeight)
{ //Pristaly sme na spodku hracej plochy
return;
} else //Nie sme na spodku
{
this.MovePiece( 1, 0, true);
}
}
Čas potrebný na vyvolanie novej slučky vyrátavame tak, že čas, ktorý nám zabralo vykonanie hernej logiky odpočítame od času, ktorý by mala trvať slučka (LevelInterval). Inak povedané, ak by nám z nejakého dôvodu trvalo vykonanie skriptu 200 ms a slučka má trvať 500 ms, tak nastavíme spustenie nasledujúcej slučky na 500 – 200, čiže 300 ms. Týmto je možné dosiahnuť cca rovnakú rýchlosť hry na rôznych zariadeniach. Avšak, mám pocit, že v našom prípade budú rozdiely medzi zariadeniami zanedbateľné. Po uložení projektu a jeho spustení v prehliadači uvidíme náhodne zvolenú kocku, ktorá postupne padá nadol. Keď dopadne, hra sa skončí (technicky vzaté časovač stále beží, no kocka už nemôže ďalej padať).Čo by to bola ale za hra, keby sme do nej nevedeli zasiahnuť. V nasledujúcom kroku pridáme možnosť upravovať pozíciu padajúcej kocky do strán a urýchlenie jej pádu. Všetko podstatné už máme zadefinované, preto nám stačí doplniť do funkcie KeyDown pár riadkov kódu na relevantné miesta:{
...
this.Status = GameStates.gs_Play;
this.GameUpdateRequest();
},
GameUpdateRequest: function ()
{
var LevelInterval = 500; var ActualDate = new Date();
var Interval = LevelInterval - (ActualDate - this.LastUpdate);
if (Interval < 0)
Interval = 0; window.setTimeout("Game.GameUpdate();", Interval);
},
GameUpdate: function ()
{
if (this.Status === GameStates.gs_Play)
{
this.LastUpdate = new Date();
this.MoveNext(); if (this.Status === GameStates.gs_Play)
{
this.GameUpdateRequest();
}
}
},
MoveNext: function ()
{
var Piece = this.ActivePiece;
if (Piece.Top + Piece.Height === this.GameArrayHeight)
{ //Pristaly sme na spodku hracej plochy
return;
} else //Nie sme na spodku
{
this.MovePiece( 1, 0, true);
}
}
Zdrojový kód:
KeyDown: function (e)
{
switch (e.keyCode)
{
...
case 40: //Down
{
if (Game.ActivePiece.Top + Game.ActivePiece.Height + 1 <= Game.GameArrayHeight)
{
Game.MovePiece(1, 0, false);
} e.preventDefault();
}break;
case 37: //Left
{
if (Game.ActivePiece.Left - 1 >= 0)
{
Game.MovePiece( 0, -1, false);
}
e.preventDefault();
}break;
case 39: //Right
{
if (Game.ActivePiece.Left + Game.ActivePiece.Width + 1 <= Game.GameArrayWidth)
{
Game.MovePiece( 0, 1, false);
} e.preventDefault();
}break;
...
}
},
Pridaný kód pozostáva z kontrol, či je požadovaný pohyb vzhľadom na aktuálnu pozíciu a veľkosť kocky možné vykonať, resp. či by pohyb nedostal kocku mimo hracie pole. Ak je z tohto hľadiska pohyb v poriadku, zavolá sa funkcia MovePiece, ktorá pohyb vykoná.Opäť môžeme projekt uložiť a vyskúšať. Hra by už mala reagovať na šípky do strán a nadol.{
switch (e.keyCode)
{
...
case 40: //Down
{
if (Game.ActivePiece.Top + Game.ActivePiece.Height + 1 <= Game.GameArrayHeight)
{
Game.MovePiece(1, 0, false);
} e.preventDefault();
}break;
case 37: //Left
{
if (Game.ActivePiece.Left - 1 >= 0)
{
Game.MovePiece( 0, -1, false);
}
e.preventDefault();
}break;
case 39: //Right
{
if (Game.ActivePiece.Left + Game.ActivePiece.Width + 1 <= Game.GameArrayWidth)
{
Game.MovePiece( 0, 1, false);
} e.preventDefault();
}break;
...
}
},
Prekrývajúce sa kocky
Zdrojový kód:
DockPiece: function (piece)
{
var Top = 0;
var Left = 0; for (var i = 0; i < piece.Height; i++)
{
Top = piece.Top + i; for (var j = 0; j < piece.Width; j++)
{
Left = piece.Left + j;
if (piece.Blocks[i][j] === FieldStates.fs_Block)
{
this.GameArray[Top][Left] = piece.Blocks[i][j];
}
}
} this.NewPiece();
}
Funkcia jednoducho zapíše všetky dieliky kocky do herného poľa a zavolá pridanie kocky na plochu.Niektorý z vás si už asi všimli, že tretí parameter canDock funkcie MovePiece, ktorý sme si ešte nepopisovali bude mať nejaký súvis práve s funkciou DockPiece. Parameter slúži ako príznak, či má pohyb pri kolízii s inou kockou vyvolať jej „zakotvenie“ na hraciu plochu. Inak povedané, keď kocka padá samovoľne a narazí na inú kocku, mala by tam už ostať. Avšak, ak narazíme dvoma kockami na seba len cez ich krajné dieliky (horizontálne) a kocka má kam ďalej padať, nie je dôvod na jej zakotvenie.Pridáme si funkciu CheckCollision, ktorá bude slúžiť na zistenie kolízie medzi konkrétnou kockou a údajmi v hernom poli, pri požadovanom posune kocky. {
var Top = 0;
var Left = 0; for (var i = 0; i < piece.Height; i++)
{
Top = piece.Top + i; for (var j = 0; j < piece.Width; j++)
{
Left = piece.Left + j;
if (piece.Blocks[i][j] === FieldStates.fs_Block)
{
this.GameArray[Top][Left] = piece.Blocks[i][j];
}
}
} this.NewPiece();
}
Zdrojový kód:
CheckCollision: function (piece, top, left)
{
var Top = 0;
var Left = 0; for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
Top = piece.Top + i + top;
Left = piece.Left + j + left; if (piece.Blocks[i][j] === FieldStates.fs_Block
&& this.GameArray[Top][Left] === piece.Blocks[i][j])
{
return true;
}
}
}
return false;
}
Keďže už vieme ukladať kocky a zisťovať kolíziu, môžeme kód funkcie MovePiece rozšíriť nasledovne:{
var Top = 0;
var Left = 0; for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
Top = piece.Top + i + top;
Left = piece.Left + j + left; if (piece.Blocks[i][j] === FieldStates.fs_Block
&& this.GameArray[Top][Left] === piece.Blocks[i][j])
{
return true;
}
}
}
return false;
}
Zdrojový kód:
MovePiece: function (top, left, canDock)
{
var piece = this.ActivePiece;
if (this.CheckCollision(piece, top, left))
{
if (canDock)
{
this.DockPiece(piece);
}
} else
{
this.EreasePiece(piece);
piece.Top += top;
piece.Left += left; //Prekreslenie
this.DrawPiece(piece);
}
},
Volanie DockPiece doplníme aj do kódu MoveNext:{
var piece = this.ActivePiece;
if (this.CheckCollision(piece, top, left))
{
if (canDock)
{
this.DockPiece(piece);
}
} else
{
this.EreasePiece(piece);
piece.Top += top;
piece.Left += left; //Prekreslenie
this.DrawPiece(piece);
}
},
Zdrojový kód:
MoveNext: function ()
{
var Piece = this.ActivePiece;
if (Piece.Top + Piece.Height === this.GameArrayHeight)
{ //Pristali sme na spodku hracej plochy
this.DockPiece(Piece); // <----------
return;
} else //Nie sme na spodku
{
this.MovePiece(1, 0, true);
}
},
Posledným vylepšením v tomto diely seriálu bude možnosť rotovať padajúcu kocku. Rotáciu budeme ovládať šípkou nahor a postará sa o ňu funkcia RotatePiece.{
var Piece = this.ActivePiece;
if (Piece.Top + Piece.Height === this.GameArrayHeight)
{ //Pristali sme na spodku hracej plochy
this.DockPiece(Piece); // <----------
return;
} else //Nie sme na spodku
{
this.MovePiece(1, 0, true);
}
},
Zdrojový kód:
RotatePiece: function (piece)
{
var newpiece = this.CreatePiece(piece.Height, piece.Width, piece.Color);
var newLeft = 0;
var widthDiff = newpiece.Width - piece.Width; if (widthDiff === 0)
{
newLeft = piece.Left;
} else
{
if (newpiece.Width > piece.Width)
{
newLeft = piece.Left - Math.floor(newpiece.Width / 2);
} else if (newpiece.Width < piece.Width)
{
newLeft = piece.Left + Math.floor(piece.Width / 2);
}
} if (newLeft < 0)
newLeft = 0; if (newLeft + newpiece.Width > this.GameArrayWidth)
{
newLeft = this.GameArrayWidth - newpiece.Width;
} newpiece.Top = piece.Top;
newpiece.Left = newLeft;
for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
//V smere hodinovych ruciciek
newpiece.Blocks[j][piece.Height - 1 - i] = piece.Blocks[i][j];
}
} //Zrotovana kocka musi vojst do priestoru
if (this.CheckCollision(newpiece, 0, 0))
{
return;
} //Zrotovana kocka nesmie prejst cez spodnu hranicu hracieho pola
if (newpiece.Top + newpiece.Height > this.GameArrayHeight)
{
return;
} //Vymaz starej kocky
this.EreasePiece(piece); this.ActivePiece = newpiece;
//Prekreslenie
this.DrawPiece(newpiece);
}
Princíp rotovania je relatívne jednoduchý. Vytvoríme si novú kocku, ktorá bude mať „prevrátené“ rozmery (K2.Width = K1.Height; K2.Height = K1.Width; ). Keďže zrotovaná kocka má inú šírku, je možné, že nebude môcť ostať na svojej pôvodnej pozícii, nakoľko by mohla zasahovať mimo hracieho poľa. Napríklad, keby sme mali kocku typu „I“ na pravom kraji hracej plochy a zrotovali by sme ju, polovica z nej by presahovala hraciu plochu za pravým okrajom. Preto okrem samotnej rotácie musí funkcia korigovať ja pozíciu novej kocky. Ďalej skontroluje, či zrotovaná kocka by nebola v kolízii s inou kocku, prípadne spodnou častou hracej plochy. V prípade, ak sú tieto podmienky splnené, vymažeme starú kocku a nahradíme ju novou. V opačnom prípade sa ponechá pôvodná kocka.Volanie funkcie RotatePiece pridáme do KeyDown.{
var newpiece = this.CreatePiece(piece.Height, piece.Width, piece.Color);
var newLeft = 0;
var widthDiff = newpiece.Width - piece.Width; if (widthDiff === 0)
{
newLeft = piece.Left;
} else
{
if (newpiece.Width > piece.Width)
{
newLeft = piece.Left - Math.floor(newpiece.Width / 2);
} else if (newpiece.Width < piece.Width)
{
newLeft = piece.Left + Math.floor(piece.Width / 2);
}
} if (newLeft < 0)
newLeft = 0; if (newLeft + newpiece.Width > this.GameArrayWidth)
{
newLeft = this.GameArrayWidth - newpiece.Width;
} newpiece.Top = piece.Top;
newpiece.Left = newLeft;
for (var i = 0; i < piece.Height; i++)
{
for (var j = 0; j < piece.Width; j++)
{
//V smere hodinovych ruciciek
newpiece.Blocks[j][piece.Height - 1 - i] = piece.Blocks[i][j];
}
} //Zrotovana kocka musi vojst do priestoru
if (this.CheckCollision(newpiece, 0, 0))
{
return;
} //Zrotovana kocka nesmie prejst cez spodnu hranicu hracieho pola
if (newpiece.Top + newpiece.Height > this.GameArrayHeight)
{
return;
} //Vymaz starej kocky
this.EreasePiece(piece); this.ActivePiece = newpiece;
//Prekreslenie
this.DrawPiece(newpiece);
}
Zdrojový kód:
KeyDown: function (e)
{
switch (e.keyCode)
{
...
case 38: //Up - Rotate
{
Game.RotatePiece(Game.ActivePiece);
e.preventDefault();
}break;
...
}
}
Projekt môžeme opäť uložiť a otestovať v prehliadači.V nasledujúcom diely si opäť rozšírime hernú logiku, pridáme možnosť ihneď zhodiť padajúcu kocku, detekciu zaplnenia hracej plochy a počítanie skóre.
{
switch (e.keyCode)
{
...
case 38: //Up - Rotate
{
Game.RotatePiece(Game.ActivePiece);
e.preventDefault();
}break;
...
}
}
Žiadne príspevky v diskusii.
Na prispievanie do diskusie musíte byť prihlásený.