Programování Her v JavaScriptu

Programování Her v JS

Canvas Tahák

4. Programování Hry

Co si naprogramujeme za hru?

V této části už bychom mohli být schopni naprogramovat nějakou jednoduchou hru. Zkusíme si vytvořit hru ve které budeme ovládat kbelík pomocí kterého budeme chytat shora padající kapky. Jak budeme kapky chytat tak budou postupně padat rychleji a rychleji a hra tak bude těžší a těžší. Cílem bude nachytat do kbelíku co nejvíce kapek. Hra skončí když hráč kapku do kbelíku nechytí.

Aby i začátečníci pochopili jak tato hra funguje, tak nebudeme při jejím programování používat žádné třídy a tak dále. V podstatě jen použijeme věci o kterých jste se mohli dočíst v minulých částech.

Vytvoření základního kódu

Na začátek si vytvoříme tři soubory. Prvním souborem bude javascript soubor který nazveme mainLoop.js a tam si vytvoříme Main Loop, které nám bude v pravidelném časovém intervalu volat funkce update a draw. Pokud jste četli předchozí kapitolu, tak už možná tak nějak víte jak Main Loop funguje, takže ho v naší hře jen použijeme. Druhým souborem bude také javascript soubor, sem ale budeme psát kód týkající se naší hry. Můžeme ho nazvat třeba hra.js. Posledním souborem bude HTML stránka na kterou přidáme canvas na který budeme kreslit a připojíme si sem i naše dva javascript soubory.

Níže máte html kód který si můžete zkopírovat do svého vytvořeného HTML souboru a kód s Main Loop který si také můžete zkopírovat.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            /* základní nastylování canvasu */
            canvas {
                border: 2px solid #f7f7f7;
                background-color: #252525;
            }
        </style>
    </head>
    <body>
        <!-- vytvoření canvasu o šířce 300 a výšce 500 pixelů -->
        <canvas width="300" height="500" id="MujCanvas"></canvas>

        <!-- připojení javascript souborů na stránku -->
        <script src="./mainLoop.js"></script>
        <script src="./hra.js"></script>
    </body>
</html>
const MAX_FPS = 60;
const CASOVY_KROK = 1000/60;

let casPoslednihoSnimku = 0;
let delta = 0;

function mainLoop(aktualniCas) {
    if (aktualniCas < casPoslednihoSnimku + (1000 / MAX_FPS)) {
        requestAnimationFrame(mainLoop);
        return;
    }

    delta += aktualniCas - casPoslednihoSnimku;
    casPoslednihoSnimku = aktualniCas;

    let pocetUpdateVolani = 0;
    while (delta >= CASOVY_KROK) {
        update(CASOVY_KROK/1000);
        delta -= CASOVY_KROK;

        pocetUpdateVolani++;
        if (pocetUpdateVolani > 240) {
            delta = 0; // v našem případě při překročení maximálního limitu volání update funkce jen vynulujeme delta time (jinak bychom mohli třeba zastavit hru nebo tak něco)
            break;
        }
    }
    draw();
    requestAnimationFrame(mainLoop);
}

Vytvoření update a draw funkce

V souboru hra.js, kde budeme psát kód pro naši hru si musíme vytvořit update a draw funkce které nám bude volat Main Loop. Pojďme si je tam tedy vytvořit a kdyžtak zatím do hry přidáme jen kbelík který ještě zatím nepůjde ovládat. A namísto vykreslování obrázku kbelíku můžeme prozatím použít jen obyčejný obdelník který za obrázek změníme až později. Nemusíme na všechno hned ze začátku mít obrázky, občas si třeba jen chceme zkusit jak se hra bude hrát a pokud budeme s hrou spokojeni tak pro ni můžeme vytvořit i obrázky.

// konstanty které budeme ve hře používat (pokud budeme později chtít nějakou z těchto hodnot změnit, tak to může udělat zde a nemusíme procházet celý soubor)
const SIRKA_KBELIKU = 50;
const VYSKA_KBELIKU = 60;

// získání kontextu ke canvasu
let canvas = document.getElementById("MujCanvas");
let ctx = canvas.getContext("2d");

// nastavení barvy kterou budeme vykreslovat obdelník
ctx.fillStyle = "green";

// vytváříme si objekt kbelik ve kterém budeme ukládat souřadnice kbelíku
let kbelik = {
    xSouradnice: (canvas.width/2)-(SIRKA_KBELIKU/2),
    ySouradnice: 435
};

// nastartujeme si Main Loop
requestAnimationFrame(mainLoop);

// update funkce ve které později budeme psát logiku naší hry
function update(delta) {
}

// draw funkce která bude sloužit k vykreslování naší hry
function draw() {
    // vymažeme obsah canvasu
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // vykreslíme kbelík (zatím teda jen obdelník)
    ctx.fillRect(kbelik.xSouradnice, kbelik.ySouradnice, SIRKA_KBELIKU, VYSKA_KBELIKU);
}

Ovládání kbelíku

Kbelík máme ve hře přidaný, ale ještě ho nemůžeme ovládat. Přidáme si tedy do našeho kódu event listenery, které nám umožní kbelík posouvat. Pokud stiskneme šipku doleva, tak budeme chtít aby se kbelík začal posouvat doleva a pokud šipku doprava, tak aby se posouval doprava.

Tady na stránce se vám může ovládání trochu sekat, pokud si ale při čtení tohoto tutoriálu hru zkoušíte programovat sami, tak by to mělo být v pohodě.

const SIRKA_KBELIKU = 50;
const VYSKA_KBELIKU = 60;
// vytvořili jsme si novou konstantu, která bude definovat rychlost pohybu kbelíku
const RYCHLOST_POHYBU_KBELIKU = 200;

let canvas = document.getElementById("MujCanvas");
let ctx = canvas.getContext("2d");

ctx.fillStyle = "green";

let kbelik = {
    xSouradnice: (canvas.width/2)-(SIRKA_KBELIKU/2),
    ySouradnice: 435
};

// vytvořili jsme si proměnné, které budeme používat k zjišťování jestli hráč drží šipku doleva/doprava a nebo ne
let sipkaDolevaStisknuta = false;
let sipkaDopravaStisknuta = false;

// přidáme event listener na stisknutí tlačítka na klávesnici
document.addEventListener("keydown", (e) => {
    // pokud hráč stiskl šipku doleva nebo doprava tak nastavíme příslušnou proměnnou, která indikuje jestli je šipka stisknutá na true
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = true;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = true;
    }
});
// přidáme event listener na uvolnění tlačítka na klávesnici
document.addEventListener("keyup", (e) => {
    // pokud hráč uvolnil šipku doleva nebo doprava tak nastavíme příslušnou proměnnou, která indikuje jestli je šipka stisknutá na false
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = false;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = false;
    }
});

requestAnimationFrame(mainLoop);

function update(delta) {
    // pokud hráč drží šipku doprava, tak budeme kbelík pohybovat doprava
    if (sipkaDopravaStisknuta) {
        kbelik.xSouradnice += RYCHLOST_POHYBU_KBELIKU * delta;
        // pokud už kbelík dorazil k pravému okraji canvasu, tak už mu neumožníme se dál pohybovat
        if (kbelik.xSouradnice > canvas.width-SIRKA_KBELIKU) {
            kbelik.xSouradnice = canvas.width-SIRKA_KBELIKU;
        }
    }
    // pokud hráč drží šipku doleva, tak budeme kbelík pohybovat doleva
    if (sipkaDolevaStisknuta) {
        kbelik.xSouradnice -= RYCHLOST_POHYBU_KBELIKU * delta;
        // pokud už kbelík dorazil k levému okraji canvasu, tak už mu neumožníme se dál pohybovat
        if (kbelik.xSouradnice < 0) {
            kbelik.xSouradnice = 0;
        }
    }
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillRect(kbelik.xSouradnice, kbelik.ySouradnice, SIRKA_KBELIKU, VYSKA_KBELIKU);
}

Zobrazení skóre a startování hry

Nyní bychom mohli do naší hry přidat zobrazení skóre v levé horní části canvasu a možnost začít hrát hru až po stisknutí tlačítka Enter. Skóre zatím bude samozřejmě stále na nule, ještě jsme do hry nepřidali žádné kapky, které by hráč mohl do kbelíku chytat a zvyšovat si tak skóre.

Pokud už v canvasu nevidíte nápis že pro start hry máte stisknout tlačítko Enter, tak můžete obnovit stránku.

const SIRKA_KBELIKU = 50;
const VYSKA_KBELIKU = 60;
const RYCHLOST_POHYBU_KBELIKU = 200;

let canvas = document.getElementById("MujCanvas");
let ctx = canvas.getContext("2d");

// určíme si jak by měl vypadat text, který budeme v canvasu vykreslovat (více se můžete dozvědět v Canvas Taháku)
ctx.font = "20px Arial";
ctx.textBaseline = "top";

let kbelik = {
    xSouradnice: (canvas.width/2)-(SIRKA_KBELIKU/2),
    ySouradnice: 435,
    pocetChycenychKapek: 0 // zde budeme ukládat kolik kapek hráč chytil (je to v podstatě skóre)
};

// tato proměnná nám bude sloužit k určení jestli hra začala nebo ne
let hraZacala = false;

let sipkaDolevaStisknuta = false;
let sipkaDopravaStisknuta = false;

document.addEventListener("keydown", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = true;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = true;
    } else if (e.code === "Enter") { // přidali jsme další podmínku ve které se ptáme jestli hráč stisknul enter
        // pokud hráč stisl Enter, tak hra může začít
        hraZacala = true;
    }
});
document.addEventListener("keyup", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = false;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = false;
    }
});

requestAnimationFrame(mainLoop);

function update(delta) {
    // pokud hra ještě nezačala tak nebudeme dělat nic, ukončíme update funkci
    if (!hraZacala) {; // ten vykřičník znamená že se true změní na false a naopak
        // následující příkaz ukončí funkci (kód který následuje za tímto příkazem už se neprovede)
        return;
    }

    if (sipkaDopravaStisknuta) {
        kbelik.xSouradnice += RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice > canvas.width-SIRKA_KBELIKU) {
            kbelik.xSouradnice = canvas.width-SIRKA_KBELIKU;
        }
    }
    if (sipkaDolevaStisknuta) {
        kbelik.xSouradnice -= RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice < 0) {
            kbelik.xSouradnice = 0;
        }
    }
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    if (hraZacala) {
        // pokud hra začala, tak můžeme vykreslovat kbelík (později sem přidáme k vykreslení i kapku)
        ctx.fillStyle = "green"; // protože už teď vykreslujeme i text, tak musíme barvu před vykreslováním obdelníku nastavovat zde
        ctx.fillRect(kbelik.xSouradnice, kbelik.ySouradnice, SIRKA_KBELIKU, VYSKA_KBELIKU);
    } else {
        // pokud hra ještě nezačala, tak vykreslíme text kde uživatele vyzveme aby hru začal
        ctx.fillStyle = "white";
        ctx.fillText("stiskni Enter pro start hry", 32, canvas.height/2);
    }

    // zobrazíme kolik kapek hráč chytil (jaké má skóre)
    ctx.fillStyle = "white";
    ctx.fillText("Skóre: " + kbelik.pocetChycenychKapek, 5, 5);
}

Přidání padajících kapek

Teď bychom mohli do hry přidat padající kapky, které hráč bude chytat a zvyšovat si tak skóre. Místo obrázku kapky můžeme stejně jako u kbelíku prozatím vykreslovat jen obdelník.

Poté už bude hra v podstatě hotová a už jen nahradíme obdelníky za obrázky a přidáme zvuk.

const SIRKA_KBELIKU = 50;
const VYSKA_KBELIKU = 60;
// vytvoříme si dvě nové konstanty pro šířku a výšku kapky
const SIRKA_KAPKY = 30;
const VYSKA_KAPKY = 40;
const RYCHLOST_POHYBU_KBELIKU = 200;

let canvas = document.getElementById("MujCanvas");
let ctx = canvas.getContext("2d");

ctx.font = "20px Arial";
ctx.textBaseline = "top";

let kbelik = {
    xSouradnice: (canvas.width/2)-(SIRKA_KBELIKU/2),
    ySouradnice: 435,
    pocetChycenychKapek: 0
};

// vytvoříme si objekt který bude obsahovat souřadnice kapky a její rychlost kterou budeme postupně zvyšovat (pro jednoduchost budeme mít v naší hře pouze jednu kapku a když ji hráč chytí tak ji posuneme zpět nahoru aby ji mohl chytit znovu)
let kapka = {
    xSouradnice: Math.random() * (canvas.width-SIRKA_KAPKY), // na začátku nastavíme kapce náhodnou x souřadnice (funkce Math.random vrací náhodné číslo od 0 do 1)
    ySouradnice: 0-VYSKA_KAPKY,
    rychlost: 120
};

let hraZacala = false;

let sipkaDolevaStisknuta = false;
let sipkaDopravaStisknuta = false;

document.addEventListener("keydown", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = true;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = true;
    } else if (e.code === "Enter") {
        hraZacala = true;
        // při startu hry vyresetujeme počet chycených kapek aby hráč nepokračoval se skóre z minulé hry
        kbelik.pocetChycenychKapek = 0;
    }
});
document.addEventListener("keyup", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = false;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = false;
    }
});

requestAnimationFrame(mainLoop);

function update(delta) {
    if (!hraZacala) {;
        return;
    }

    if (sipkaDopravaStisknuta) {
        kbelik.xSouradnice += RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice > canvas.width-SIRKA_KBELIKU) {
            kbelik.xSouradnice = canvas.width-SIRKA_KBELIKU;
        }
    }
    if (sipkaDolevaStisknuta) {
        kbelik.xSouradnice -= RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice < 0) {
            kbelik.xSouradnice = 0;
        }
    }

    // postupně budeme kapku posouvat dolů
    kapka.ySouradnice += kapka.rychlost * delta;

    // následující podmínka se ptá jestli kbelík koliduje s kapkou
    if (!(
        (kapka.xSouradnice > kbelik.xSouradnice+SIRKA_KBELIKU || kapka.xSouradnice+SIRKA_KAPKY < kbelik.xSouradnice) ||
        (kapka.ySouradnice > kbelik.ySouradnice+VYSKA_KBELIKU || kapka.ySouradnice+VYSKA_KAPKY < kbelik.ySouradnice)
    )) {
        // hráč chytil kapku, zvýšíme počet chycených kapek (skóre)
        kbelik.pocetChycenychKapek++;

        // nastavíme náhodně novou x souřadnici kapky a posuneme ji zpět nahoru
        kapka.xSouradnice = Math.random() * (canvas.width-SIRKA_KAPKY);
        kapka.ySouradnice = 0 - VYSKA_KAPKY;

        // zvýšíme rychlost kapky (čím více kapek hráč pochytá, tím rychlejší kapka bude)
        kapka.rychlost += 10;
    }

    // následující podmínka se ptá jestli už kapka nepřejela dolní okraj canvasu
    if (kapka.ySouradnice+VYSKA_KAPKY > canvas.height) {
        // když kapka přejela dolní část canvasu, tak ukončíme hru
        hraZacala = false;
        // vyresetujeme kapku
        kapka.xSouradnice = Math.random() * (canvas.width-SIRKA_KAPKY);
        kapka.ySouradnice = 0 - VYSKA_KAPKY;
        kapka.rychlost = 120;
    }
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    if (hraZacala) {
        ctx.fillStyle = "green";
        ctx.fillRect(kbelik.xSouradnice, kbelik.ySouradnice, SIRKA_KBELIKU, VYSKA_KBELIKU);
        // vykreslíme kapku (zatím teda jen obdelník)
        ctx.fillRect(kapka.xSouradnice, kapka.ySouradnice, SIRKA_KAPKY, VYSKA_KAPKY);
    } else {
        ctx.fillStyle = "white";
        ctx.fillText("stiskni Enter pro start hry", 32, canvas.height/2);
    }

    ctx.fillStyle = "white";
    ctx.fillText("Skóre: " + kbelik.pocetChycenychKapek, 5, 5);
}

Přidání obrázků a zvuků

Na závěr můžeme obyčejné obdelníky nahradit obrázkami a přidat i zvuky. Obrázky a zvuky si můžete stáhnout zde.

Než začneme vykreslovat na canvas obrázky, tak si je nejdříve musíme přidat na naši HTML stránku, jak ukazuje ukázka kódu. Poté si je v javascriptu na stránce najdeme a můžeme je na canvas vykreslit.

Ukázku canvasu jsem sem nedával, protože už je na začátku této části.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            canvas {
                border: 2px solid #f7f7f7;
                background-color: #252525;
            }
        </style>
    </head>
    <body>
        <canvas width="300" height="500" id="MujCanvas"></canvas>

        <!-- přidáme na stránku obrázky, které budeme v canvasu vykreslovat (musíme je přidat dříve než připojíme naše javascript soubory) -->
        <div style="display: none;">
            <!-- obrázky jsme si obalili do divu a tomu jsme nastavili style s display: none; aby se obrázky na stránce nezobrazili -->
            <img src="./kbelik.png" id="ObrazekKbelik">
            <img src="./kapka.png" id="ObrazekKapka">
        </div>

        <script src="./mainLoop.js"></script>
        <script src="./hra.js"></script>
    </body>
</html>
const SIRKA_KBELIKU = 50;
const VYSKA_KBELIKU = 60;
const SIRKA_KAPKY = 30;
const VYSKA_KAPKY = 40;
const RYCHLOST_POHYBU_KBELIKU = 200;

// najdeme si na html stránce obrázky pomocí metody getElementById a uložíme si je do proměnných
let kbelikImg = document.getElementById("ObrazekKbelik");
let kapkaImg = document.getElementById("ObrazekKapka");

// načteme si zvuky, které budeme ve hře přehrávat
let zvukKapky = new Audio("./water_drop.wav");
let zvukRozlitiVody = new Audio("./water_spill.wav");

let canvas = document.getElementById("MujCanvas");
let ctx = canvas.getContext("2d");

ctx.font = "20px Arial";
ctx.textBaseline = "top";
// teď už budeme používat jen bílou barvu pro text, takže ji můžeme nastavit tady
ctx.fillStyle = "white";

let kbelik = {
    xSouradnice: (canvas.width/2)-(SIRKA_KBELIKU/2),
    ySouradnice: 435,
    pocetChycenychKapek: 0
};

let kapka = {
    xSouradnice: Math.random() * (canvas.width-SIRKA_KAPKY),
    ySouradnice: 0-VYSKA_KAPKY,
    rychlost: 120
};

let hraZacala = false;

let sipkaDolevaStisknuta = false;
let sipkaDopravaStisknuta = false;

document.addEventListener("keydown", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = true;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = true;
    } else if (e.code === "Enter") {
        hraZacala = true;
        kbelik.pocetChycenychKapek = 0;
    }
});
document.addEventListener("keyup", (e) => {
    if (e.code === "ArrowLeft") {
        sipkaDolevaStisknuta = false;
    } else if (e.code === "ArrowRight") {
        sipkaDopravaStisknuta = false;
    }
});

requestAnimationFrame(mainLoop);

function update(delta) {
    if (!hraZacala) {;
        return;
    }

    if (sipkaDopravaStisknuta) {
        kbelik.xSouradnice += RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice > canvas.width-SIRKA_KBELIKU) {
            kbelik.xSouradnice = canvas.width-SIRKA_KBELIKU;
        }
    }
    if (sipkaDolevaStisknuta) {
        kbelik.xSouradnice -= RYCHLOST_POHYBU_KBELIKU * delta;
        if (kbelik.xSouradnice < 0) {
            kbelik.xSouradnice = 0;
        }
    }

    kapka.ySouradnice += kapka.rychlost * delta;

    if (!(
        (kapka.xSouradnice > kbelik.xSouradnice+SIRKA_KBELIKU || kapka.xSouradnice+SIRKA_KAPKY < kbelik.xSouradnice) ||
        (kapka.ySouradnice > kbelik.ySouradnice+VYSKA_KBELIKU || kapka.ySouradnice+VYSKA_KAPKY < kbelik.ySouradnice)
    )) {
        kbelik.pocetChycenychKapek++;

        kapka.xSouradnice = Math.random() * (canvas.width-SIRKA_KAPKY);
        kapka.ySouradnice = 0 - VYSKA_KAPKY;

        kapka.rychlost += 10;

        // když hráč chytí kapku, tak přehrajeme zvuk kapky
        zvukKapky.play();
    }

    if (kapka.ySouradnice+VYSKA_KAPKY > canvas.height) {
        hraZacala = false;
        kapka.xSouradnice = Math.random() * (canvas.width-SIRKA_KAPKY);
        kapka.ySouradnice = 0 - VYSKA_KAPKY;
        kapka.rychlost = 120;

        // když hra skončí, tak přehrajeme zvuk rozlití vody
        zvukRozlitiVody.play();
    }
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    if (hraZacala) {
        // namísto obdelníků teď vykreslíme obrázky
        ctx.drawImage(kbelikImg, kbelik.xSouradnice, kbelik.ySouradnice);
        ctx.drawImage(kapkaImg, kapka.xSouradnice, kapka.ySouradnice);
    } else {
        ctx.fillText("stiskni Enter pro start hry", 32, canvas.height/2);
    }

    ctx.fillText("Skóre: " + kbelik.pocetChycenychKapek, 5, 5);
}

Kvíz

V této části jsme si zkusili naprogramovat jednoduchou hru. Protože jsme k tomu používali věci z minulých kapitol, tak je tu pro vás nachystaný kvíz na všechny čtyři kapitoly.

Some question