Táto stránka pre svoju správnu funkčnosť vyžaduje súbory cookies. Slúžia na authentifikáciu návštevníka, analýzu návštevnosti a reklamnú personalizáciu.
logo
Prihlásenie / Registrácia
mobile

Zavrieť
 

Tetris v HTML5 – II. Herná logika 1/4

V druhom diely tohto seriálu začneme s implementáciou hernej logiky.
Keďže nemá zmysel opätovne „objavovať koleso“, tak určitá časť hernej logiky bude prevzatá z predchádzajúceho seriálu o hre Snake (> link <), keďže technické riešenie oboch hier veľmi podobné. V oboch hrách je herné pole zložené z určitého počtu pozícii, ktoré môžu mať vopred určené stavy a herná logika sa odvíja od týchto stavov. V hre Snake boli tieto stavy 3: prázdne pole, časť hada a potrava, no v Tetrise si vystačíme so stavom prázdne pole a pole s dielikom. Teoreticky by na tieto stavy stačilo použiť boolean ( true/false, 1/0), avšak takto sa dá herná logika rozšíriť o ďalšie možnosti (napríklad nejaké špeciálne typy dielikov a pod).

Nasledujúce úpravy budeme robiť v súbore main.js.

Začneme definovaním enumerátov pred objekt Game.

Zdrojový kód:
var FieldStates = {
    fs_Empty: 0,
    fs_Block: 1
};

var GameStates = {
    gs_Stop: 0,
    gs_Play: 1,
    gs_Loading: 2
};

var Game = {
...
}

FieldStates reprezentuje stavy políčka a GameStates zase stavy hry, pričom gs_Loading bude použitý len pri prvotnom nahrávaní hry.

Pre lepšiu prehľadnosť kódu, si objekte Game vytvoríme 3 skupiny premenných (pomocou komentárov „Nastavenia“, „Herne premenne“, „Obrazky“), do ktorých budeme vkladať jednotlivé premenné, podľa skupiny do ktorej patria.

Zdrojový kód:
var Game = {

    /* Nastavenia */

    //Rozmery hracieho pola (pocet policok, nie px)
    GameArrayWidth: 10,
    GameArrayHeight: 20,

    GameFieldWidth: 25, //Velkost jedneho policka
    BorderWidth: 2, //Hrubka oramovania hracieho pola

    BackColor: '#000000', //Farba pozadia
    BorderColor: '#303030', //Farba oramovania

    //Rozmery hracej plochy (v px)
    GameAreaWidth: 263,
    GameAreaHeight: 523,
    /* / Nastavenia */

    /* Herne premenne */
    Canvas: null,
    Context: null, //Kresliaca plocha
    ActivePiece: null, //Aktivna kocka
    NextPiece: null, //Nasledujuca kocka
    LastUpdate: null, //Cas poslednej aktualizacie hry
    LastRandomNumber: -1, //Posledne vratene nahodne cislo

    //Rozmery kresliacej plochy
    ContextWidth: 0,
    ContextHeight: 0,

    Status: GameStates.gs_Loading, //Status hry
    GameArray: null, //Herne pole
    ItemsToLoad: 0, //Pocet poloziek, ktore sa este nacitavaju
    /* /Herne premenne */

    /* Obrazky */
    img_empty: null, //Prazde policko
    img_colors: [], //Farebne kocky
    /* /Obrazky */
...
}

V predchádzajúcom kóde sme si už zadefinovali niektoré z premenných, ktoré budeme ďalej používať. Ku každej premennej je pridaný aj komentár, čiže zatiaľ ich bližšie popisovať netreba.

Ďalej si zadefinujeme funkciu KeyDown, ktorá bude slúžiť ako event handler na stlačené klávesy. V jej tele bude switch, ktorým si odchytíme len nami požadované klávesy (šípky, medzerník a enter) a zablokujeme ich ďalšie spracovanie v prehliadači cez funkciu preventDefault.

Zdrojový kód:
var Game = {
...
KeyDown: function (e)
    {
        switch (e.keyCode)
        {
            case 13: //Enter - Restart game
                {
                    e.preventDefault();
                }
                break;
            case 38: //Up - Rotate
                {
                    e.preventDefault();
                }
                break;
            case 40: //Down
                {
                    e.preventDefault();
                }
                break;
            case 37: //Left
                {
                    e.preventDefault();
                }
                break;
            case 39: //Right
                {
                    e.preventDefault();
                }
                break;
            case 32: //SpaceBar
                {
                    e.preventDefault();
                }
                break;
        }
    }
}

Z projektu Snake si požičiame funkcie CreateImage, ImageLoaded a CheckLoadedFiles, ktoré nám slúžia na načítanie obrázkov do projektu. CreateImage túto akciu inicializuje a ImageLoaded je volaná po jej úspešnom dokončení, pričom zvyšujú a znižujú hodnotu premennej ItemsToLoad v objekte Game, ktorá slúži ako indikátor načítania všetkých súborov do hry. Funkcia CheckLoadedFiles nakoniec skontroluje, či je všetko už načítané a ak áno, hra sa spustí cez funkciu NewGame ktorá bude zatiaľ obsahovať len jednoduchý alert.

Zdrojový kód:
CreateImage: function (FileName)
    {
        var img = new Image();
        this.ItemsToLoad++;
        img.src = "imgs/" + FileName + ".png";
        img.onload = this.ImageLoaded;
        return img;

    },
    ImageLoaded: function ()
    {
        this.onload = null;
        Game.ItemsToLoad--;
        Game.CheckLoadedFiles();
    },
CheckLoadedFiles: function ()
    {
        if (Game.ItemsToLoad <= 0)
        {
            this.NewGame();
        }
    },
NewGame: function ()
    {
        alert('New game');
    },

Funkcie na načítanie obrázkov do hry už máme, môžeme ich teda do hry pridať. Z nasledujúcej linky > link < si môžete stiahnuť potrebné obrázky a rozbaliť ich do priečinka imgs v našom projekte. Jedná sa o obrázky políčka hracej plochy. Konkrétne empty.png reprezentuje prázdne políčko a ostatné (blue, brown, green, purple, red a yellow) sú znakom plného políčka.

Určite ste si všimli, že podľa obrázkov môže byť políčko vykreslené až siedmimi obrázkami, no v enumeráte FieldStates sú len 2 možné hodnoty. Nie je to chyba, keďže z pohľadu hernej logiky je nepodstatné akou farbou je políčko vyplnené. Dôležitá je len informácia, či je políčko vyplnené alebo nie.

Na načítanie jednotlivých obrázkov pridáme funkciu LoadImages

Zdrojový kód:
LoadImages: function ()
    {
        this.img_empty = this.CreateImage("empty");

        //Nacitanie farebnych kociek
        this.img_colors[0] = this.CreateImage("blue");
        this.img_colors[1] = this.CreateImage("brown");
        this.img_colors[2] = this.CreateImage("green");
        this.img_colors[3] = this.CreateImage("purple");
        this.img_colors[4] = this.CreateImage("red");
        this.img_colors[5] = this.CreateImage("yellow");

    },

Obrázky už máme načítane, no nemáme ich ešte kde vykresľovať. Pridáme preto funkciu LoadCanvas v ktorej si načítame objekt canvasu, vytvoríme kresliaci context a odpamätáme ich rozmery.

Zdrojový kód:
LoadCanvas: function ()
    {
        this.Canvas = document.getElementById('gamecanvas');
        this.Context = this.Canvas.getContext("2d");

        // Odpamatame si rozmery kresliacej plochy
        this.ContextWidth = this.Canvas.offsetWidth;
        this.ContextHeight = this.Canvas.offsetHeight;
    },

Vrátime sa na moment k funkcii LoadGame kde nahradíme alert za volania doteraz vytvorených funkcii:

Zdrojový kód:
LoadGame: function ()
    {
        //Event listener na stlacene klavesy
        window.addEventListener('keydown', this.KeyDown);

        //Nacitanie obrazkov
        this.LoadImages();

        //Inicializacia kresliaceho prostredia
        this.LoadCanvas();

        //Skontrolujeme ci uz nebolo vsetko nahrate
        this.CheckLoadedFiles();
    }

Súbor game.js môžeme uložiť a stránku index.html môžeme spustiť v prehliadači. Ak sme urobili všetko správne, malo by sa nám po spustení stránky zobraziť hlásenie „New game“. Týmto sme si potvrdili že skript funguje a obrázky sa podarilo načítať.

Nasleduje inicializácia herného pola, ktoré nám reprezentuje premenná GameArray. Vytvoríme si funkcie CreateGameArray a CreateEmptyLine.

Zdrojový kód:
CreateGameArray: function ()
    {
        var i, j;

        this.GameArray = [];
        for (i = 0; i < this.GameArrayHeight; i++)
        {
            this.GameArray[i] = this.CreateEmptyLine();
        }
    },
    CreateEmptyLine: function ()
    {
        var ret = [];
        for (var j = 0; j < this.GameArrayWidth; j++)
        {
            ret[j] = FieldStates.fs_Empty;
        }

        return ret;
    }

Kód funkcie CreateEmptyLine budeme potrebovať aj na iných miestach, preto je definovaná samostatne.

Hracie pole máme definované, teraz si ho musíme aj nakresliť. Nakreslíme ho pomocou funkcie DrawGameField, ktorá najskôr vyčistí kresliacu plochu, farebne oddelí hraciu plochu (vytvorí obdĺžnik hernej plochy, ktorý bude neskôr slúžiť aj ako orámovanie) a nakoniec nakreslí samotné políčka. Na nakreslenie jednotlivých políčok bude slúžiť funkcia DrawBlock, ktorú použijeme aj všade tam, kde bude potrebné políčka aktualizovať.

Na výpočet vizuálnej pozície políčka na kresliacej ploche použijeme vzorec: [Reálna pozícia] = [Šírka orámovania] + [pozícia v GameArray] * [Vizuálna šírka hracieho políčka] + [pozícia v GameArray] . Pozícia v GameArray je vo vzorci 2x, nakoľko medzi jednolivými políčkami nechávame 1px medzeru. Tento vzorec zapíšeme do funkcie GetRealPosition.

Zdrojový kód:
DrawGameField: function ()
    {
        //Vyplnenie celej plochy farbou pozadia
        this.Context.fillStyle = this.BackColor;
        this.Context.fillRect(0, 0, this.ContextWidth, this.ContextHeight);

        //Vyplnenie hernej plochy farbou oramovania
        this.Context.fillStyle = this.BorderColor;
        this.Context.fillRect(0, 0, this.GameAreaWidth, this.GameAreaHeight);

        //Nakreslenie jednotlivych policok
        for (var i = 0; i < this.GameArrayHeight; i++)
        {
            for (var j = 0; j < this.GameArrayWidth; j++)
            {
                this.DrawBlock(i, j, this.img_empty);
            }
        }
    },
    GetRealPosition: function (pos)
    {
        return this.BorderWidth + pos * this.GameFieldWidth + pos;
    },
    DrawBlock: function (top, left, color)
    {
        var RealLeft = this.GetRealPosition(left);
        var RealTop = this.GetRealPosition(top);
        this.Context.drawImage(color, RealLeft, RealTop);
    },

Na moment sa vrátime k funkcii NewGame do ktorej pridáme nasledujúci kód.

Zdrojový kód:
NewGame: function ()
    {
        this.LastUpdate = new Date();

        //Vycistit hracie pole
        this.CreateGameArray();

        //Nakreslit hracie pole
        this.DrawGameField();
    },

Prázdna hracia plocha
Kód zabezpečí vytvorenie hracej plochy a jej následné nakreslenie. V úvode si poznačíme aktuálny dátum a čas do premennej LastUpdate. Tento údaj budeme neskôr potrebovať na výpočet času na aktualizáciu hry.

Pre kontrolu si projekt uložíme a spustíme v prehliadači. V prípade, ak sme sa nikde nepomýlili by ste mali vidieť prázdnu hraciu plochu.

Na prázdnej hracej ploche si veľa nezahráme, čiže nastal čas pridať hracie kocky. Hraciu kocku nám bude reprezentovať objekt Piece, ktorý bude obsahovať informáciu o jej šírke, dĺžke, pozícii na hracej ploche, farbe (resp o obrázku z ktorého je poskladaná) a o jednotlivých dielikoch, ktoré hraciu kocku tvoria. Na vytvorenie všeobecného objektu Piece vytvoríme funkciu CreatePiece.

Zdrojový kód:
CreatePiece: function (width, height, color)
    {
        var Ret = {Left: 0, Top: 0, Width: width, Height: height, Color: color, Blocks: []};

        Ret.Blocks = [];
        for (var i = 0; i < height; i++)
        {
            Ret.Blocks[i] = [];
            for (var j = 0; j < width; j++)
            {
                Ret.Blocks[i][j] = FieldStates.fs_Empty;
            }
        }

        return Ret;
    },

Kocka vytvorená touto funkciou má informáciu o svojich rozmeroch a farbe. Ostatné jej vlastnosti je potrebné zadefinovať pre každý typ kocky zvlášť. Preto vytvoríme pre každý typ kocky samostatnú inicializačnú funkciu, ktorá bude volať CreatePiece a následne doplní informáciu o rozmiestnení dielikov. Údaje o pozícii sa budú dynamicky meniť, preto sa budú nastavovať v menej všeobecných funkciách.

Zdrojový kód:
Create_L_Piece: function (color)
    {
        var piece = this.CreatePiece(3, 2, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[0][2] = FieldStates.fs_Block;
        piece.Blocks[1][0] = FieldStates.fs_Block;
        return piece;
    },
Create_J_Piece: function (color)
    {
        var piece = this.CreatePiece(3, 2, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[0][2] = FieldStates.fs_Block;
        piece.Blocks[1][2] = FieldStates.fs_Block;
        return piece;
    },
Create_T_Piece: function (color)
    {
        var piece = this.CreatePiece(3, 2, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[0][2] = FieldStates.fs_Block;
        piece.Blocks[1][1] = FieldStates.fs_Block;
        return piece;
    },
Create_O_Piece: function (color)
    {
        var piece = this.CreatePiece(2, 2, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[1][0] = FieldStates.fs_Block;
        piece.Blocks[1][1] = FieldStates.fs_Block;
        return piece;
    },
Create_I_Piece: function (color)
    {
        var piece = this.CreatePiece(4, 1, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[0][2] = FieldStates.fs_Block;
        piece.Blocks[0][3] = FieldStates.fs_Block;
        return piece;
    },
Create_S_Piece: function (color)
    {
        var piece = this.CreatePiece(3, 2, color);
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[0][2] = FieldStates.fs_Block;
        piece.Blocks[1][0] = FieldStates.fs_Block;
        piece.Blocks[1][1] = FieldStates.fs_Block;
        return piece;
    },
Create_Z_Piece: function (color)
    {
        var piece = this.CreatePiece(3, 2, color);
        piece.Blocks[0][0] = FieldStates.fs_Block;
        piece.Blocks[0][1] = FieldStates.fs_Block;
        piece.Blocks[1][1] = FieldStates.fs_Block;
        piece.Blocks[1][2] = FieldStates.fs_Block;
        return piece;
    },

Funkcie na vytvorenie kociek máme, potrebujeme ešte zabezpečiť, aby nám ich hra vytvárala v náhodnom poradí a v náhodnej farbe. JavaScript poskytuje funkciu Math.random(), ktorá vygeneruje náhodné desatinné číslo od 0 (vrátane) do 1. Čiže vrátené číslo môže byť napríklad 0,56934718. V prípade, ak potrebujeme náhodné číslo bez desatinných miest (Integer) a v nejakom vopred určenom rozsahu, môžeme použiť vzorec: [Náhodné číslo] = [Minimálna hodnota] + Math.floor(Math.random() * ([Maximálna hodnota] + 1)) .

Avšak, tu som narazil na problém… Hodnoty, ktoré boli vrátené týmto spôsobom sa veľmi často opakovali. Preto si poslednú vrátenú hodnotu budeme pamätať a nedovolíme ju opäť vrátiť.

Na výber náhodného čísla vytvoríme funkciu Random a použijeme ju vo funkcii Create_Random_Piece ktorá nám pomocou nej vytvorí náhodný typ kocky v náhodnej farbe.

Zdrojový kód:
Random: function (min, max)
    {
        var Ret = -1;
        do
        {
            Ret = min + Math.floor(Math.random() * (max + 1));
        } while (Ret === this.LastRandomNumber)

        this.LastRandomNumber = Ret;

        return Ret;
    },
//Vygenerovat nahodnu kocku s nahodnou farbou
Create_Random_Piece: function ()
    {
        //Typ kocky
        var pType = this.Random(0, 6);
        //Index farby
        var pColor = this.img_colors[this.Random(0, this.img_colors.length - 1)];

        switch (pType)
        {
            default:
            case 0:
                {
                    return this.Create_L_Piece(pColor);
                }
                break;
            case 1:
                {
                    return this.Create_J_Piece(pColor);
                }
                break;
            case 2:
                {
                    return this.Create_T_Piece(pColor);
                }
                break;
            case 3:
                {
                    return this.Create_O_Piece(pColor);
                }
                break;
            case 4:
                {
                    return this.Create_I_Piece(pColor);
                }
                break;
            case 5:
                {
                    return this.Create_S_Piece(pColor);
                }
                break;
            case 6:
                {
                    return this.Create_Z_Piece(pColor);
                }
                break;
        }
    },


Pokračujeme pridaním funkcií DrawPiece, PlacePiece, a NewPiece. DrawPiece bude slúžiť na vykreslenie objektu Piece na hernú plochu. Na základe pozície kocky vyráta pozície jej jednotlivých dielikov a vyplní ich príslušnou farbou, resp. jej obrázkom. PlacePiece sa postará o dátové umiestnenie kocky do hry, konkrétne výpočet jej úvodnej pozície, priradenie referencie do premennej ActivePiece a zavolanie DrawPiece). Tretia v poradí NewPiece bude volaná zo všetkých miest v programe, kde bude požadované vloženie ďalšej kocky do hry. Vyberie náhodnú kocku a pomocou PlacePiece ju umiestni na plochu (Neskôr si jej funkčnosť ešte rozšírime). Volanie funkcie NewPiece vložíme aj na koniec NewGame

Zdrojový kód:
NewGame: function ()
    {
...
        //Nova kocka na plochu
        this.NewPiece();
    },
DrawPiece: function (piece)
    {
        var Left = 0;
        var Top = 0;

        for (var i = 0; i < piece.Height; i++)
        {
            for (var j = 0; j < piece.Width; j++)
            {
                var FieldType = piece.Blocks[i][j];

                Top = piece.Top + i;
                Left = piece.Left + j;

                if (FieldType === FieldStates.fs_Block)
                {
                    this.DrawBlock(Top, Left, piece.Color);
                }
            }
        }
    },
PlacePiece: function (Piece)
    {
        var Left = Math.floor(this.GameArrayWidth / 2 - Piece.Width / 2);
        var Top = 0;
        Piece.Left = Left;
        Piece.Top = Top;

        this.ActivePiece = Piece;
        this.DrawPiece(Piece);

    },
NewPiece: function ()
    {
        var NewPiece = this.Create_Random_Piece();
        this.PlacePiece(NewPiece);
    }

Náhodná kocka v hernom poli
Po uložení projektu a zobrazení stránky v prehliadači, by sa v hornej časti hracej plochy mala objaviť náhodná kocka náhodnej farby. Funkčnost náhodnosti môžeme vyskúšať aj viacnásobným refresh-om stránky.

V nasledujúcom diely seriálu budeme pokračovať s hernou logikou, rozpohybujeme kocky, pridáme detekciu kolízie a rotáciu kocky.

Codeblog
Ostatné texty v seriály

Tetris v HTML5 – I. Popis riešenia a základná konštrukcia 0
Tetris v HTML5 – II. Herná logika 1/4 0
Tetris v HTML5 – III. Herná logika 2/4 0
Tetris v HTML5 – IV. Herná logika 3/4 0
Diskusia

Žiadne príspevky v diskusii.

Nový príspevok

Na prispievanie do diskusie musíte byť prihlásený.