Vytváříme hru v systému LÖVE

Pavel Tišnovský 9. 6. 2009

Ve čtrnácté části seriálu o programovacím jazyku Lua bude ukázán způsob vytvoření jednoduché 2D hry s využitím systému LÖVE. Vzhledem ke snadno použitelnému API nabízeného systémem LÖVE je tvorba podobných typů her snadná a rychlá (řádově desítky minut), což je ideální například pro tvorbu prototypů.

Obsah

1. Hrátky se systémem LÖVE II – jednoduchá hra
2. Zdrojový kód hry
3. Základní kostra hry
4. Načtení všech potřebných objektů a inicializace hry
5. Zpracování událostí
6. Režimy hry a způsob jejich přepínání
7. Výpočet trajektorie hráče
8. Pohyb protihráčů a detekce kolizí
9. Závěrečná animace a ukončení hry

1. Hrátky se systémem LÖVE II – jednoduchá hra

V předchozích dvou částech tohoto seriálu jsme se seznámili se základy systému LÖVE, především s aplikačním programovým rozhraním (API) jednotlivých knihoven i způsobem využití takzvaných callback funkcí, tj. funkcí, které tento systém automaticky zavolá ve chvíli, kdy nastane nějaká událost, například stisk klávesy, pohyb myši, „tik“ časovače či kdy dojde k žádosti operačního systému o překreslení okna či obrazovky. Také již víme, jakým způsobem je možné načítat některé typy objektů, například rastrové obrázky, zvuky, hudbu či fonty ze souborů umístěných buď v adresáři s rozbalenou hrou (jedná se o adresář obsahující mj. i soubory game.conf a main.lua) nebo přímo v archivu s hrou, tj. v souboru hra.love. Tento archiv vznikne komprimací všech souborů umístěných v jednom adresáři programem typu zip s následnou změnou koncovky archivu, tj. například příkazem zip –9 hra.love .. Dnes si ukážeme, jak lze s využitím systému LÖVE a taktéž některých vlastností programovacího jazyka Lua vytvořit jednoduchou 2D hru s objektem představujícím hráče a několika počítačem řízenými protihráči.

love1401

Obrázek 1: Úvodní obrazovka hry

2. Zdrojový kód hry

Zdrojový kód hry, který mj. můžete získat po rozbalení archivu s hrou, popř. ho lze studovat na níže uvedeném výpisu, má přesně 256 řádků (bez komentářů dokonce pouze 200 řádků) a byl vytvořen za cca hodinu, protože snadno použitelné API systému LÖVE i dynamický jazyk Lua skutečně dokážou poměrně výrazným způsobem ušetřit programátorův čas.

-----------------------------------------------------------------------
-- Seriál "Programovací jazyk Lua"
--
-- Jednoduchá hra vytvořená s využitím systému LÖVE napsaná na 256 řádcích
-----------------------------------------------------------------------

-- časový interval mezi jednotlivými snímky
sleepInMs = 50

-- velikost okna
window = {
    width = 800,
    height = 600
}

-- objekt představující hráče - raketu
rocket = {
    x = window.width/2,
    y = window.height/2,
    v = 0,
    rotation = 0,
    deltaAlfa = 8,
    acceleration = 1.5,
    dampingFactor = 0.95
}

-- objekt představující protivníky
stars = {
    count = 10,
    maxVelocity = 50
}

-- výbuch rakety = dvacet červených teček
redBalls = {
    count = 20,
    velocity = 6,
    numberOfFrames = 70
}

-- callback funkce zavolaná při inicializaci aplikace
function load()
    -- načíst fonty
    font1 = love.graphics.newFont("DejaVuLGCSansMono.ttf", 20)
    font2 = love.graphics.newFont("DejaVuLGCSansMono.ttf", 20*6)
    -- nastavit grafický režim
    love.graphics.setMode(window.width, window.height, false, false, 0)
    -- načíst všechny obrázky
    background = love.graphics.newImage("tile-space.jpg")
    rocket.image = love.graphics.newImage("rocket.gif")
    stars.image = love.graphics.newImage("orgstar.gif")
    redBalls.image = love.graphics.newImage("redball.gif")
    -- načíst zvuky
    crashSound = love.audio.newSound("pluck.wav")
    crashSound2 = love.audio.newSound("beam.wav")
    -- inicializace všech protivníků
    for i = 1, stars.count do
        stars[i] = {
            x = window.width/2 + 295 * math.cos(i * 2 * math.pi / stars.count),
            y = window.height/2 + 295 * math.sin(i * 2 * math.pi / stars.count),
            damping = 5 + i
        }
    end
    -- začít přehrávat úvodní zvuk
    love.audio.play(love.audio.newSound("drama.wav"))
end

-- callback funkce volaná průběžně ve chvíli, kdy je zapotřebí překreslit okno
draw = function()
    drawBeginning()
end

-- první režim hry - pouze se přehrává melodie a zobrazuje se hrací pole
function drawBeginning()
    drawScene()
    -- čekání na konec přehrání úvodní melodie
    if not love.audio.isPlaying() then
        -- změna režimu hry - bude se volat jiná překreslovací funkce
        draw = function()
            drawGame()
        end
    end
    love.graphics.setColor(0, 255, 255)
    love.graphics.setFont(font2)
    love.graphics.draw("Prepare...", 50, window.height/2-50)
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end

-- druhý režim hry - vlastní hra
function drawGame()
    -- posun hráče
    moveRocket()
    -- posun protivníků
    moveStars()
    -- vykreslení celé scény
    drawScene()
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end

-- vykreslení scény ve druhém režimu
function drawScene()
    drawBackground()
    drawRocket()
    drawStars()
    drawInfo()
end

-- třetí režim hry - výbuch s nápisem "Game over"
function drawExplosion()
    drawBackground()
    -- čekání na konec animace výbuchu, potom ukončení hry
    redBalls.numberOfFrames = redBalls.numberOfFrames - 1
    if redBalls.numberOfFrames > 0 then
        -- posun úlomků rakety
        for i = 1, redBalls.count do
            redBalls[i].x=redBalls[i].x+redBalls[i].vx
            redBalls[i].y=redBalls[i].y+redBalls[i].vy
            love.graphics.draw(redBalls.image, redBalls[i].x, redBalls[i].y)
        end
    end
    -- výpis "Game over"
    love.graphics.setFont(font2)
    love.graphics.setColor(255, 0, 255)
    love.graphics.draw("Game over", 50, window.height/2)
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end

-- výpis informací v dolní části okna
function drawInfo()
    love.graphics.setFont(font1)
    love.graphics.setColor(255, 200, 255)
    love.graphics.draw("Esc: exit, left: turn left, right: turn fight, up: accelerate", 10, window.height-30)
end

-- vykreslení hracího pole - několik na sebe navazujících dlaždic
function drawBackground()
    local y = 0;
    while y <= window.height do
        local x = 0
        while x <= window.width do
            love.graphics.draw(background, x, y)
            x = x + background:getWidth()
        end
        y = y + background:getHeight()
    end
end

-- vykreslení rakety
function drawRocket()
    local x = rocket.x
    local y = rocket.y
    love.graphics.draw(rocket.image, x, y, rocket.rotation)
end

-- vykreslení protivníků
function drawStars()
    for i = 1, stars.count do
        love.graphics.draw(stars.image, stars[i].x, stars[i].y)
    end
end

-- posun rakety a její natočení na základě kláves
-- [Left], [Right] a [Up]
function moveRocket()
    if love.keyboard.isDown(love.key_left) then
        rocket.rotation = rocket.rotation - rocket.deltaAlfa
    end
    if love.keyboard.isDown(love.key_right) then
        rocket.rotation = rocket.rotation + rocket.deltaAlfa
    end
    if love.keyboard.isDown(love.key_up) then
        rocket.v = rocket.v + rocket.acceleration
    end
    -- posun rakety na základě její rychlosti a natočení
    rocket.x = rocket.x + rocket.v * math.sin(math.rad(rocket.rotation))
    rocket.y = rocket.y - rocket.v * math.cos(math.rad(rocket.rotation))
    rocket.v = rocket.v * rocket.dampingFactor
    -- v případě opuštění hracího pole se raketa ukáže na protější straně
    if rocket.x < 0 then
        rocket.x = window.width
        love.audio.play(crashSound)
    end
    if rocket.x > window.width then
        rocket.x = 0
        love.audio.play(crashSound)
    end
    if rocket.y < 0 then
        rocket.y = window.height
        love.audio.play(crashSound)
    end
    if rocket.y > window.height then
        rocket.y = 0
        love.audio.play(crashSound)
    end
end

-- posun všech protivníků směrem ke hráči
function moveStars()
    for i = 1, stars.count do
        moveStar(stars[i])
    end
end

-- posun jednoho protivníka směrem ke hráči
function moveStar(star)
    local vx, vy
    vx = rocket.x - star.x
    vy = rocket.y - star.y
    -- zamezit příliš rychlému pohybu
    vx = math.max(math.min(vx, stars.maxVelocity), -stars.maxVelocity)
    vy = math.max(math.min(vy, stars.maxVelocity), -stars.maxVelocity)
    -- vlastní posun o vypočtený vektor
    star.x = star.x + vx / star.damping
    star.y = star.y + vy / star.damping
    -- zjistit, zda nastala kolize
    if isCollision(star) then
        processColission()
        return
    end
end

-- funkce vrátí true v případě, že dojde ke kolizi rakety s protivníkem
function isCollision(star)
    return math.abs(star.x - rocket.x) < rocket.image:getWidth()/2
       and math.abs(star.y - rocket.y) < rocket.image:getHeight()/2
end

-- pokud nastane kolize, změní se mód hry - bude se volat rutina drawExplosion()
function processColission()
    love.audio.play(crashSound2)
    -- změna překreslovací rutiny
    draw = function()
        drawExplosion()
    end
    -- inicializace objektů představujících rozbitou raketu
    for i = 1, redBalls.count do
        redBalls[i]= {
            x=rocket.x,
            y=rocket.y,
            vx=redBalls.velocity * math.cos(i * 2 * math.pi / redBalls.count),
            vy=redBalls.velocity * math.sin(i * 2 * math.pi / redBalls.count)
        }
    end
end

-- callback funkce volaná ve chvíli, kdy uživatel stlačí nějakou klávesu
function keypressed(key)
    if key == love.key_escape then
        love.system.exit()
    end
end

-- finito

3. Základní kostra hry

Herní scénář je velmi jednoduchý; vymýšlel ho můj malý syn, který si prosadil, že tam prostě musí být raketa nebo aspoň létající balon :-) (slíbená „plošinovka“ tedy bude ukázána příště, musím ještě nalézt vhodné obrázky do hry šířené pod licencí kompatibilní s LGPL). Na herní ploše se pohybuje raketka natáčená klávesami [Šipka doleva] a [Šipka doprava], jejíž motory se spouští klávesou [Šipka nahoru]. Poslední aktivní klávesou je [Esc], kterou je možné hru v jakémkoli okamžiku ukončit. Úkolem raketky je uniknout protihráčům, kteří se vždy pohybují směrem k ní (ovšem každý protihráč s jinou rychlostí). Protihráčům lze uniknout jak rychlým letem, tak i pomocí malého triku – po dosažení jednoho z okrajů herního pole se raketa ukáže na protilehlém okraji, ovšem sami protihráči nedokážou přes okraj přeletět. Po střetu raketky s některým z protihráčů dojde k výbuchu a hra je ukončena – po stisku klávesy Esc se okno hry uzavře (samozřejmě by bylo možné přidat výsledkovou tabulkou s dosaženými časy, hru pro více hráčů atd., v tomto seriálu nám však jde hlavně o vysvětlení herních principů).

love1402

Obrázek 2: Screenshot zachycený v průběhu hry

Celá hra je, jak z hlediska hráčů, tak i programátorů, rozdělena do tří režimů:

  1. Nejprve se přehraje úvodní melodie se zobrazeným hracím polem a nápisem „Prepare“ (viz první screenshot). Po přehrání úvodní melodie se hra automaticky přepíná do druhého režimu.
  2. Vlastní hra – pohyb hráče i protivníků s detekcí kolizí. Průběh hry je zachycený na druhém screenshotu.
  3. Kolize a následná exploze rakety (hráče) s ukončením hry, viz třetí screenshot.
love1403

Obrázek 3: Exploze rakety a ukončení hry

4. Načtení všech potřebných objektů a inicializace hry

Před prvním zobrazením úvodní obrazovky je nejprve nutné načíst všechny potřebné objekty do operační paměti. Průběžné načítání objektů je sice také možné, ovšem může zapříčinit prodlevy ve hře, které se projevují trháním animace. V naší jednoduché hře se používá pouze osm objektů uložených v samostatných souborech, které jsou následně zabaleny do archivu s hrou. Jedná se následující soubory:

Poř. Soubor Význam
1 drama.wav Úvodní melodie trvající cca 7 sekund
2 pluck.wav Zvuk přehraný při přeletu raketky přes okraj hrací plochy
3 beam.wav Zvuk přehraný v průběhu detonace raketky
4 tile-space.jpg Dlaždice použitá pro vytvoření pozadí hrací plochy
5 rocket.gif Obrázek raketky
6 orgstar.gif Obrázek představující protihráče
7 redball.gif Obrázek použitý při detonaci raketky
8 DejaVuLGCSansMo­no.ttf Font použitý ve hře
love1404

Obrázek 4: Dlaždice použitá pro vytvoření pozadí hrací plochy (dlaždice je obrázek, jehož okraje na sebe plynule navazují, což mj. znamená, že může být velmi malý a přesto s ním lze kontinuálně pokrýt velkou plochu). O vykreslení pozadí složeného z dlaždic se stará funkce drawBackground().

O načtení všech potřebných objektů se stará callback funkce load(), která kromě jiného provádí i inicializaci protihráčů a nakonec spustí úvodní melodii (poslední řádek). Povšimněte si například řádku začínajícího rocket.image =, v němž se do „objektu“ (zde spíše jen do datové struktury typu záznam) nazvaného rocket přidává další atribut.

-- callback funkce zavolaná při inicializaci aplikace
function load()
    -- načíst fonty
    font1 = love.graphics.newFont("DejaVuLGCSansMono.ttf", 20)
    font2 = love.graphics.newFont("DejaVuLGCSansMono.ttf", 20*6)
    -- nastavit grafický režim
    love.graphics.setMode(window.width, window.height, false, false, 0)
    -- načíst všechny obrázky
    background = love.graphics.newImage("tile-space.jpg")
    rocket.image = love.graphics.newImage("rocket.gif")
    stars.image = love.graphics.newImage("orgstar.gif")
    redBalls.image = love.graphics.newImage("redball.gif")
    -- načíst zvuky
    crashSound = love.audio.newSound("pluck.wav")
    crashSound2 = love.audio.newSound("beam.wav")
    -- inicializace všech protivníků
    for i = 1, stars.count do
        stars[i] = {
            x = window.width/2 + 295 * math.cos(i * 2 * math.pi / stars.count),
            y = window.height/2 + 295 * math.sin(i * 2 * math.pi / stars.count),
            damping = 5 + i
        }
    end
    -- začít přehrávat úvodní zvuk
    love.audio.play(love.audio.newSound("drama.wav"))
end

5. Zpracování událostí

Programové ovládání celé hry je založeno na takzvaných callback funkcích, které jsou automaticky volány systémem LÖVE po příchodu nějaké události. V demonstrační hře se používá několik callback funkcí. Aby je systém LÖVE rozpoznal, musí mít tyto funkce jednoznačná (předem daná) jména i parametry, což je rozdíl oproti jiným systémům, kde lze callback funkce či metody pojmenovat libovolně – ovšem na druhou stranu je zapotřebí tyto funkce/metody registrovat; tuto činnost není nutné v systému LÖVE provádět:

Callback funkce Význam
load() callback funkce zavolaná při inicializaci aplikace
draw() callback funkce volaná průběžně ve chvíli, kdy je zapotřebí překreslit okno se hrou
keypressed(key) callback funkce volaná ve chvíli, kdy uživatel stlačí nějakou klávesu
love1405

Obrázek 5: Obrázek představující raketku (hráče). Jedná se o rastrový obrázek uložený ve formátu GIF, jehož jedna barva je průhledná, takže se ve skutečnosti na pozadí hry nevykreslí obdélník, ale pouze ty pixely, které skutečně tvoří tělo rakety. Malý kvíz: uhodne někdo, z které (velmi staré) hry tento obrázek pochází?

Zdrojový kód callback funkce load() byl již uveden v předchozí kapitole, tělo funkce draw() je dynamicky měněno při přepínání jednotlivých režimů hry (viz další kapitoly) a callback funkce keypressed(key) je velmi jednoduchá, protože se v ní pouze testuje, zda je stlačena klávesa [Esc]. Vzhledem k tomu, že se tato callback funkce volá nezávisle na zrovna aktivním režimu hry, lze klávesou [Esc] hru ukončit takřka v kterémkoli okamžiku:

-- callback funkce volaná ve chvíli, kdy uživatel stlačí nějakou klávesu
function keypressed(key)
    if key == love.key_escape then
        love.system.exit()
    end
end

6. Režimy hry a způsob jejich přepínání

Jedním ze zajímavých konceptů, který je ve hře použit, je způsob přepínání jednotlivých režimů hry (úvodní obrazovka, vlastní hra, detonace na konci hry). Celé řízení hry je provedeno v callback funkci draw(), která je volána v relativně pravidelných intervalech, takže není nutné využívat časovač. Mohlo by se zdát, že – vzhledem k tomu, že se v každém režimu ve scéně zobrazují jiné objekty – je nutné ve funkci draw() použít nějakou formu rozhodovací struktury typu if-elseif-else popř. switch-case řízené na základě globální proměnné či proměnné uložení v uzávěru (closure). Ve skutečnosti to však není nutné, protože tělo callback funkce draw() lze dynamicky měnit za běhu aplikace. Podívejme se nyní, jakým způsobem je provedeno přepnutí mezi prvním a druhým režimem, tj. mezi úvodní obrazovkou a vlastní hrou. Již víme, že v callback funkci load() se na závěr načte a spustí úvodní melodie. V pravidelně volané callback funkci draw() se posléze provádí test, zda je melodie stále přehrávána a ve chvíli, kdy se dojde k jejímu konci, je tělo funkce draw() přepsáno, takže příští událost volaná před překreslením okna je zpracovávána již jiným programovým kódem (z funkce draw() je přímo volána funkce drawGame()):

-- callback funkce volaná průběžně ve chvíli, kdy je zapotřebí překreslit okno
draw = function()
    drawBeginning()
end

-- první režim hry - pouze se přehrává melodie a zobrazuje se hrací pole
function drawBeginning()
    drawScene()
    -- čekání na konec přehrání úvodní melodie
    if not love.audio.isPlaying() then
        -- změna režimu hry - bude se volat jiná překreslovací funkce
        draw = function()
            drawGame()
        end
    end
    love.graphics.setColor(0, 255, 255)
    love.graphics.setFont(font2)
    love.graphics.draw("Prepare...", 50, window.height/2-50)
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end
love1406

Obrázek 6: Obrázek představující protihráče. Opět se jedná o rastrový obrázek s průhledným pozadím.

7. Výpočet trajektorie hráče

Trajektorie rakety (hráče) je počítána na základě její aktuální rychlosti a stavu kláves [Šipka doprava], [Šipka doleva] a [Šipka nahoru]. Pomocí těchto kláves, jejichž stav se zjišťuje přímo v callback funkci draw(), lze raketou natáčet doprava a doleva (úhel natočení je uložen v atributu rocket.rotation) a zapnout její motory, kterými se zvyšuje její rychlost. Ta je pro jednoduchost reprezentována pouze skalární hodnotou rocket.v a nikoli vektorem, což mj. znamená, že se raketa vždy pohybuje ve směru jejího natočení, na rozdíl od her typu Asteroids!, kde se bere do úvahy i setrvačnost (úprava pro výpočet setrvačnosti je jednoduchá, ovšem ovládání hry by se ztížilo). Rychlost rakety lze zvýšit klávesou [Šipka nahoru], ovšem navíc je rychlost v každém snímku snižována o hodnotu rocket.dampin­gFactor. Pokud je tato konstanta nastavena na jedničku, bude se raketa pohybovat v prostředí, které neklade žádný odpor (viz již zmiňovaná hra Asteroids!), hodnota menší než jedna naopak vede k tomu, že je raketa neustále brzděna, což mj. omezuje (bez nutnosti použití explicitní podmínky v programu) i její maximální rychlost, která může dosáhnout hodnoty vypočtené podle vztahu:
vmax=(rocket.ac­celeration × rocket.dampin­gFactor) / (1 – rocket.dampin­gFactor)

Pro představu, jak celá trajektorie rakety může vypadat, je možné původní programový kód hry nahradit následujícím skriptem. V něm se na základě stlačených kláves (kurzorových šipek) mění orientace i poloha rakety, přičemž se do pomocného asociativního pole nazvaného rockets ukládají i všechny předchozí souřadnice rakety v ploše, které jsou následně vykresleny v callback funkci draw().

love1407

Obrázek 7: Trajektorie hráče vypočtená na základě postupného natáčení rakety a zapínání či vypínání jejích motorů. Čím pomaleji se raketa pohybuje, tím menší prostor zůstává mezi jednotlivými obrázky (sprity). Pomalejší pohyb taktéž umožňuje zatáčet v oblouku s menším poloměrem než při rychlosti větší.

-----------------------------------------------------------------------
-- Seriál "Programovací jazyk Lua"
--
-- Zobrazení trajektorie rakety
-----------------------------------------------------------------------

-- časový interval mezi jednotlivými snímky
sleepInMs = 50

-- velikost okna
window = {
    width = 800,
    height = 600
}

-- objekt představující hráče - raketu
rocket = {
    x = window.width/2,
    y = window.height/2,
    v = 0,
    rotation = 0,
    deltaAlfa = 8,
    acceleration = 1.5,
    dampingFactor = 0.95
}

-- pole pro úschovu pozice a natočení rakety v minulosti
rockets = {}

-- callback funkce zavolaná při inicializaci aplikace
function load()
    -- načíst fonty
    font1 = love.graphics.newFont("DejaVuLGCSansMono.ttf", 20)
    -- nastavit grafický režim
    love.graphics.setMode(window.width, window.height, false, false, 0)
    -- načíst všechny obrázky
    background = love.graphics.newImage("tile-space.jpg")
    rocket.image = love.graphics.newImage("rocket.gif")
end

-- callback funkce volaná průběžně ve chvíli, kdy je zapotřebí překreslit okno
draw = function()
    -- posun hráče
    moveRocket()
    -- vykreslení celé scény
    drawScene()
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end

-- vykreslení scény ve druhém režimu
function drawScene()
    drawBackground()
    drawRocket()
    drawInfo()
end

-- výpis informací v dolní části okna
function drawInfo()
    love.graphics.setFont(font1)
    love.graphics.setColor(255, 200, 255)
    love.graphics.draw("" .. #rockets, 10, window.height-30)
end

-- vykreslení hracího pole - několik na sebe navazujících dlaždic
function drawBackground()
    local y = 0;
    while y <= window.height do
        local x = 0
        while x <= window.width do
            love.graphics.draw(background, x, y)
            x = x + background:getWidth()
        end
        y = y + background:getHeight()
    end
end

-- vykreslení rakety i celé její trajektorie
function drawRocket()
    local x = rocket.x
    local y = rocket.y
    for i=1, #rockets do
        local x = rockets[i].x
        local y = rockets[i].y
        local rotation = rockets[i].rotation
        love.graphics.draw(rocket.image, x, y, rockets[i].rotation)
    end
end

-- posun rakety a její natočení na základě kláves
-- [Left], [Right] a [Up]
function moveRocket()
    if love.keyboard.isDown(love.key_left) then
        rocket.rotation = rocket.rotation - rocket.deltaAlfa
    end
    if love.keyboard.isDown(love.key_right) then
        rocket.rotation = rocket.rotation + rocket.deltaAlfa
    end
    if love.keyboard.isDown(love.key_up) then
        rocket.v = rocket.v + rocket.acceleration
    end
    -- posun rakety na základě její rychlosti a natočení
    rocket.x = rocket.x + rocket.v * math.sin(math.rad(rocket.rotation))
    rocket.y = rocket.y - rocket.v * math.cos(math.rad(rocket.rotation))
    rocket.v = rocket.v * rocket.dampingFactor
    -- v případě opuštění hracího pole se raketa ukáže na protější straně
    if rocket.x < 0 then
        rocket.x = window.width
    end
    if rocket.x > window.width then
        rocket.x = 0
    end
    if rocket.y < 0 then
        rocket.y = window.height
    end
    if rocket.y > window.height then
        rocket.y = 0
    end
    -- zapamatovat si pozici rakety i její natočení
    -- (užitečný idiom, jak se k asociativnímu poli chovat jako k seznamu)
    rockets[#rockets + 1] = {
        x = rocket.x,
        y = rocket.y,
        rotation = rocket.rotation
    }
end

-- callback funkce volaná ve chvíli, kdy uživatel stlačí nějakou klávesu
function keypressed(key)
    if key == love.key_escape then
        love.system.exit()
    end
end

-- finito

8. Pohyb protihráčů a detekce kolizí

Výpočet pohybu protihráčů je do značné míry zjednodušen. Protihráči se vždy pohybují ve směru rakety, ovšem každý s jinou rychlostí. Maximální horizontální a vertikální rychlost je omezena konstantou stars.maxVelo­city, což mj. znamená, že se protihráči nejrychleji pohybují po úhlopříčce – tuto znalost lze využít ke snazšímu úniku před nimi. Maximální rychlost rakety je však vyšší než rychlost protihráčů, pokud samozřejmě nedojde ke změně konstant stars.maxVelo­city, rocket.dampin­gFactor či rocket.accele­ration. V programovém kódu pro výpočet pohybu protihráčů se nachází i detekce kolize protihráče s raketou. Její výpočet je taktéž zjednodušen, protože se pouze testuje, zda se protihráč (přesněji jeho střed) nenachází v obdélníku o velikosti bitmapy rakety, tj. 44×34 pixelů. Jednu z možných úprav, které vedou ke zpřesnění tohoto výpočtu, si ukážeme v navazující části tohoto seriálu. Následuje výpis části kódu, který se stará o výpočet trajektorie protivníků i detekci kolize (povšimněte si, že pokud dojde ke kolizi, tak se hra ve funkci processColissi­on() přepne do třetího režimu):

-- posun všech protivníků směrem ke hráči
function moveStars()
    for i = 1, stars.count do
        moveStar(stars[i])
    end
end

-- posun jednoho protivníka směrem ke hráči
function moveStar(star)
    local vx, vy
    vx = rocket.x - star.x
    vy = rocket.y - star.y
    -- zamezit příliš rychlému pohybu
    vx = math.max(math.min(vx, stars.maxVelocity), -stars.maxVelocity)
    vy = math.max(math.min(vy, stars.maxVelocity), -stars.maxVelocity)
    -- vlastní posun o vypočtený vektor
    star.x = star.x + vx / star.damping
    star.y = star.y + vy / star.damping
    -- zjistit, zda nastala kolize
    if isCollision(star) then
        processColission()
        return
    end
end

-- funkce vrátí true v případě, že dojde ke kolizi rakety s protivníkem
function isCollision(star)
    return math.abs(star.x - rocket.x) < rocket.image:getWidth()/2
       and math.abs(star.y - rocket.y) < rocket.image:getHeight()/2
end

-- pokud nastane kolize, změní se mód hry - bude se volat rutina drawExplosion()
function processColission()
    love.audio.play(crashSound2)
    -- změna překreslovací rutiny
    draw = function()
        drawExplosion()
    end
    -- inicializace objektů představujících rozbitou raketu
    for i = 1, redBalls.count do
        redBalls[i]= {
            x=rocket.x,
            y=rocket.y,
            vx=redBalls.velocity * math.cos(i * 2 * math.pi / redBalls.count),
            vy=redBalls.velocity * math.sin(i * 2 * math.pi / redBalls.count)
        }
    end
end

widgety

9. Závěrečná animace a ukončení hry

Poslední režim hry je představovaný závěrečnou animací detonace a slavným nápisem „Game over“. Prozatím jsme si nepopsali, jakým způsobem lze v systému LOVE využít částicové systémy, proto je detonace rakety velmi primitivní – z místa kolize rakety a protihráče se rozlétnou červené balonky (další z požadavků syna :-) a současně se přehraje zvuk výbuchu. Počet snímků, které závěrečná animace obsahuje, je omezen konstantou redBalls.numbe­rOfFrames, přičemž po zobrazení těchto snímků hra pouze zobrazuje pozadí s nápisem „Game over“ a čeká na stisk klávesy [Esc].

-- třetí režim hry - výbuch s nápisem "Game over"
function drawExplosion()
    drawBackground()
    -- čekání na konec animace výbuchu, potom ukončení hry
    redBalls.numberOfFrames = redBalls.numberOfFrames - 1
    if redBalls.numberOfFrames > 0 then
        -- posun úlomků rakety
        for i = 1, redBalls.count do
            redBalls[i].x=redBalls[i].x+redBalls[i].vx
            redBalls[i].y=redBalls[i].y+redBalls[i].vy
            love.graphics.draw(redBalls.image, redBalls[i].x, redBalls[i].y)
        end
    end
    -- výpis "Game over"
    love.graphics.setFont(font2)
    love.graphics.setColor(255, 0, 255)
    love.graphics.draw("Game over", 50, window.height/2)
    -- čekání před vykreslením dalšího snímku
    love.timer.sleep(sleepInMs)
end
love1408

Obrázek 8: Obrázek s průhledným pozadím, který je použitý pro napodobení detonace raketky.

Našli jste v článku chybu?
Vitalia.cz: 5 nemocí, se kterými pomáhá urologie

5 nemocí, se kterými pomáhá urologie

Lupa.cz: Cimrman má hry na YouTube i vlastní doodle

Cimrman má hry na YouTube i vlastní doodle

Podnikatel.cz: Tyto pojmy k #EET byste měli znát

Tyto pojmy k #EET byste měli znát

120na80.cz: Co je padesátkrát sladší než cukr?

Co je padesátkrát sladší než cukr?

DigiZone.cz: Banaxi: videa kdekoli na světě

Banaxi: videa kdekoli na světě

DigiZone.cz: Světový pohár v přímém přenosu na ČT

Světový pohár v přímém přenosu na ČT

120na80.cz: Galerie: Čínští policisté testují českou minerálku

Galerie: Čínští policisté testují českou minerálku

Lupa.cz: Další Češi si nechali vložit do těla čip

Další Češi si nechali vložit do těla čip

Lupa.cz: Blíží se konec Wi-Fi sítí bez hesla?

Blíží se konec Wi-Fi sítí bez hesla?

Lupa.cz: Patička e-mailu závazná jako vlastnoruční podpis?

Patička e-mailu závazná jako vlastnoruční podpis?

Vitalia.cz: Tahák, jak vyzrát nad zápachem z úst

Tahák, jak vyzrát nad zápachem z úst

DigiZone.cz: Technisat připravuje trojici DAB

Technisat připravuje trojici DAB

Vitalia.cz: Tradiční čínská medicína a rakovina

Tradiční čínská medicína a rakovina

Vitalia.cz: Antibakteriální mýdla nepomáhají, spíš škodí

Antibakteriální mýdla nepomáhají, spíš škodí

Lupa.cz: Jak levné procesory změnily svět?

Jak levné procesory změnily svět?

Podnikatel.cz: „Lex Babiš“ Babišovi paradoxně pomůže

„Lex Babiš“ Babišovi paradoxně pomůže

DigiZone.cz: DVB-T2 ověřeno: seznam TV zveřejněn

DVB-T2 ověřeno: seznam TV zveřejněn

Podnikatel.cz: Instalatér, malíř a elektrikář. "Vymřou"?

Instalatér, malíř a elektrikář. "Vymřou"?

Lupa.cz: Jak se prodává firma za miliardu?

Jak se prodává firma za miliardu?

Vitalia.cz: dTest odhalil ten nejlepší kečup

dTest odhalil ten nejlepší kečup