Přejít k navigační liště

Zdroják » JavaScript » Přidáváme objekty do pseudo 3D hry v HTML5 canvasu

Přidáváme objekty do pseudo 3D hry v HTML5 canvasu

Posledně jsme naši implementaci Wolfensteina zrychlili, dnes ji pro změnu zkrášlíme. Přidáme do herní plochy (do našeho hradu) několik objektů, s pomocí kterých herní prostředí vytvoří tu správnou atmosféru. Prováděné změny v kódu budeme komentovat, abyste jim porozuměli a příště to zvládli sami.

Tento článek je překladem anglického originálu vydaného na portálu Dev.Opera. První část překladu jsme si mohli přečíst začátkem týdne.

Detekce kolizí

Přišel čas podívat se na detekci kolizí. V úplně první implementaci jsme problém vyřešili jednoduše zastavením hráče, pokud by se měl přemístit dovnitř zdi. Ačkoliv to zajistí, aby hráč neprocházel zdí, nejedná se o elegantní řešení. V prvé řadě by bylo rozumné zachovat mezi hráčem a zdí jistý odstup, jinak se může přiblížit k texturám až příliš, což nevypadá ve výsledku moc dobře. Za druhé by měl být hráč schopen se posouvat podél zdi namísto toho, aby „zamrznul“ kdykoliv se zdi dotkne.

Abychom vyřešili problém se vzdáleností, budeme potřebovat něco víc, než jen porovnávat pozici hráče s mapou. Jedním řešením by mohlo být uvažovat o hráči jako o kruhu a o stěnách jako o úsečkách. Pokud zajistíme, aby úsečky neprotly kruh, zachová si hráč vždy minimální vzdálenost rovnou poloměru svého kruhu.

Naštěstí je naše mapa tvořena jen jednoduchou mřížkou, proto budou i naše výpočty jednoduché. Stačí zajistit, aby vzdálenost mezi hráčem a nejbližším bodem každé zdi, jež ho obklopuje, byl minimálně tak velká jako je tento poloměr. A jelikož jsou všechny naše stěny orientovány buď vodorovně nebo svisle podle mřížky, bude výpočet vzdálenosti triviální.

Nahradím proto volání staré funkce isBlocking novou funkcí checkCollision. Místo návratové hodnoty true/ false, která říkala, zda se hráč může posunout na danou pozici, bude funkce vracet novou korigovanou pozici. Funkci isBlocking budeme i nadále používat v naší nové funkci checkCollision, abychom si ověřili, zda je dané pole volné.

function checkCollision(fromX, fromY, toX, toY, radius) {
    var pos = {
        x : fromX,
        y : fromY
    };

    if (toY < 0 || toY >= mapHeight || toX < 0 || toX >= mapWidth)
        return pos;
    var blockX = Math.floor(toX);
    var blockY = Math.floor(toY);

    if (isBlocking(blockX,blockY)) {
        return pos;
    }

    pos.x = toX;
    pos.y = toY;

    var blockTop = isBlocking(blockX,blockY-1);
    var blockBottom = isBlocking(blockX,blockY+1);
    var blockLeft = isBlocking(blockX-1,blockY);
    var blockRight = isBlocking(blockX+1,blockY);

    if (blockTop != 0 && toY - blockY < radius) {
        toY = pos.y = blockY + radius;
    }

    ...

    // .. vykonej totéž pro pozici napravo, nalevo a dole

    if (isBlocking(blockX-1,blockY-1) != 0 && !(blockTop != 0 && blockLeft != 0)) {
        var dx = toX - blockX;
        var dy = toY - blockY;
        if (dx*dx+dy*dy < radius*radius) {
            if (dx*dx > dy*dy)
                toX = pos.x = blockX + radius;
            else
                toY = pos.y = blockY + radius;
        }
    }

    // .. vykonej totéž pro pozici napravo nahoře, nalevo dole a napravo dole

    ...

    return pos;
}

Hráč se nyní může hladce pohybovat podél stěn a zachová si od nich minimální odstup. Vyzkoušejte si náš nový kolizní mechanismus: Demo 2 – vylepšení detekce kolizí.

Objekty (sprites)

Nyní náš herní svět trochu obohatíme přidáním detailů. Zatím se jednalo jen o otevřený prostor a stěny, pojďme ten interiér trochu vylepšit. Přidáme několik objektů:

Stůl
Brnění
Kytička
Lampa

Nejprve si deklarujeme dostupné typy objektů. Učiníme tak pomocí jednoduchého pole objektů obsahujícíbo dva typy informací: cestu k obrázku a logickou hodnotu definující, zda objekt brání hráči v pohybu nebo ne.

var itemTypes = [
    { img : "sprites/tablechairs.png", block : true },  // 0
    { img : "sprites/armor.png", block : true },        // 1
    { img : "sprites/plantgreen.png", block : true },   // 2
    { img : "sprites/lamp.png", block : false }     // 3
];

Umístíme několik takových objektů do naší mapy. Učiníme tak pomocí jednoduché datové struktury:

var mapItems = [
    // lampy v prostredni mistnosti
    {type:3, x:10, y:7},
    {type:3, x:15, y:7},
    // lampy ve spodni chodbe
    {type:3, x:5, y:22},
    {type:3, x:12, y:22},
    {type:3, x:19, y:22},
    // stoly ve spodni misnosti
    {type:0, x:10, y:18},
    {type:0, x:15, y:18},
    // lampy ve spodni mistnosti
    {type:3, x:8, y:18},
    {type:3, x:17, y:18}
];

Rozmístili jsme po hradu několik lamp a vybavili jsme jídelnu ve spodní části mapy stoly. Úpravou zdrojového kódu si můžete vyzkoušet rozmístění kytek a brnění podle vašich představ.

Nyní vytvoříme funkci initSprites, která bude volána z funkce init spolu s funkcí initScreen a dalším inicializačním kódem. Tato funkce vytvoří dvourozměrné pole odpovídající naší herní mapce a naplní je jednotlivými objekty tak, jak jsme definovali v poli mapItems array. Objekty dostanou několik vlastností: prvek img, příznak visible a příznak blokování, který jsme již zmiňovali.

var spriteMap;
function initSprites() {
    spriteMap = [];
    for (var y=0;y<map.length;y++) {
        spriteMap[y] = [];
    }

    var screen = $("screen");
    for (var i=0;i<mapItems.length;i++) {
        var sprite = mapItems[i];
        var itemType = itemTypes[sprite.type];
        var img = dc("img");
        img.src = itemType.img;
        img.style.display = "none";
        img.style.position = "absolute";
        sprite.visible = false;
        sprite.block = itemType.block;
        sprite.img = img;
        spriteMap[sprite.y][sprite.x] = sprite;
        screen.appendChild(img);
    }
}

Pomocí jednoduchého dotazu spriteMap[y][x] zjistíme, zda dané pole na mapě obsahuje nějaký objekt. Jak jste mohli vidět v kódu výše, přidali jsme všechny prvky img jako dceřiné prvky naší „obrazovky“. Trik už jen spočívá v tom zjistit, které z nich jsou viditelné a kde je na obrazovce zobrazit. K tomu trochu upravíme naši funkci  castSingleRay:

var visibleSprites = [];
function castSingleRay(rayAngle, stripIdx) {
    ...
    while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
        var wallX = Math.floor(x + (right ? 0 : -1));
        var wallY = Math.floor(y);
        // nový kód pro kontrolu objektů
        if (spriteMap[wallY][wallX] && !spriteMap[wallY][wallX].visible) {
            spriteMap[wallY][wallX].visible = true;
            visibleSprites.push(spriteMap[wallY][wallX]);
        }
        ...
    ...
}

Jak si možná pamatujete, tato funkce se volá vždy jednou pro každý svislý proužek (strip) na obrazovce. Když jsou paprsky vrženy, pohybuje se vně po krocích, aby zahrnula všechna herní pole, skrze které paprsek projde. Vždy zkontrolujeme, zda na daném poli nemá být objekt. Pokud má, nastavíme jeho viditelnost (pokud již není nastavena) a přidáme jej do pole visibleSprites. To učiníme pro svislý i vodorovný průběh.

Do funkce renderCycle přidáme dvě nová volání, jedno pro smazání seznamu viditelných objetů a druhé pro zobrazení nově označených viditelných objektů.

function renderCycle() {
    ...
    clearSprites();
    castRays();
    renderSprites();
    ...
}

Funkce clearSprites ve celkem jednoduchá:

function clearSprites() {
    // smaž pole s viditelnými objekty, ale uchovej si jeho kopii v oldVisibleSprites.
    // zruš příznak visible u všech objektů, aby mohly být
    // během vrhání paprsků znovu přidány.
    oldVisibleSprites = [];
    for (var i=0;i<visibleSprites.length;i++) {
        var sprite = visibleSprites[i];
        oldVisibleSprites[i] = sprite;
        sprite.visible = false;
    }
    visibleSprites = [];
}

Tím se konečně dostáváme k vlastnímu zobrazení objektů. Projdeme všechny objekty v poli visibleSprites. Pro každý z nich přepočítáme jeho pozici relativně k pozici hráče. Přidáme 0,5 k souřadnicím x a y, abychom dostali střed. Pouhé x a y směřuje do levého horního rohu mapové pozice. Pak již jen zjistíme natočení hráče a zbytek je jednoduchá trigonometrie.

function renderSprites() {
    for (var i=0;i<visibleSprites.length;i++) {
        var sprite = visibleSprites[i];
        var img = sprite.img;
        img.style.display = "block";

        var dx = sprite.x + 0.5 - player.x;
        var dy = sprite.y + 0.5 - player.y;

        // vzdálenost k objektu
        var dist = Math.sqrt(dx*dx + dy*dy);

        // úhel k objektu relativní k úhlu pohledu hráče
        var spriteAngle = Math.atan2(dy, dx) - player.rot;

        // velikost objektu
        var size = viewDist / (Math.cos(spriteAngle) * dist);

        // pozice x na obrazovce
        var x = Math.tan(spriteAngle) * viewDist;
        img.style.left = (screenWidth/2 + x - size/2) + "px";

        // y je konstantní, protože všechny naše objekty
        // mají stejnou velikost a svislou pozici
        img.style.top = ((screenHeight-size)/2)+"px";
        var dbx = sprite.x - player.x;
        var dby = sprite.y - player.y;
        img.style.width = size + "px";
        img.style.height =  size + "px";
        var blockDist = dbx*dbx + dby*dby;
        img.style.zIndex = -Math.floor(blockDist*1000);
    }

    // skryj objekty, které již nejsou viditelné
    for (var i=0;i<oldVisibleSprites.length;i++) {
        var sprite = oldVisibleSprites[i];
        if (visibleSprites.indexOf(sprite) < 0) {
            sprite.visible = false;
            sprite.img.style.display = "none";
        }
    }
}

Nyní jsou objekty umístěny na obrazovce na správných pozicích. Ovšem, jak můžete vidět na následujícím obrázku, je v tom trochu zmatek, protože jsme zatím nevěnovali pozornost souřadnici „z“.

Rozmistene objekty

Pokud bychom kreslili stěny a objekty poctivě pixel po pixelu, museli bychom je seřadit podle toho, jak daleko od hráče jsou, nakreslit napřed ty vzdálenější a přes ně ty bližší. V našem případě je situace výrazně snazší, protože používáme prvky HTML. Máme po ruce mocný nástroj, a tím je vlastnost zIndex z kaskádových stylů. Stačí, když nastavíme vlastnost zIndex proporcionálně ke vzdálenosti objektu nebo zdi. Prohlížeč se postará o zbytek.

function renderSprites() {
    for (var i=0;i<visibleSprites.length;i++) {
        ...
        var blockDist = dbx*dbx + dby*dby;
        img.style.zIndex = -Math.floor(blockDist*1000);
    }
}

function castSingleRay(rayAngle, stripIdx) {
    ...
    if (dist) {
        ...
        var wallDist = dwx*dwx + dwy*dwy;
        strip.style.zIndex = -Math.floor(wallDist*1000);
    }
}

Nyní jsou všechny objekty a zdi zobrazené ve správném pořadí, jak můžete vidět na obrázku níže. Jelikož vyšší hodnota vlastnosti zIndex znamená, že prvek DOMu bude umístěn nad prvky s nižší hodnotou, používáme zápornou hodnotu vzdálenosti. Jelikož jsou naše vzdálenosti malá čísla, vynásobíme je tisícem (nebo jiným velkým číslem), abychom dostali rozumně velkou řadu celočíselných hodnot.

Rozmístěné objekty, nyní již správně

Online ukázka s rozmístěnými objekty.

Nakonec nám zbývá upravit funkci isBlocking, aby vzala v úvahu blokující objekty a hráč tak nemohl procházet skrze stoly.

function isBlocking(x,y) {
    ...
    if (spriteMap[iy][ix] && spriteMap[iy][ix].block)
        return true;
    return false;
}

Dokončení příště

Když jsme do naší hry přidali nepohyblivé objekty, zbývá nám přidat objety pohyblivé (nepřátele, které budou našeho hráče pronásledovat). K těm se dostaneme příště.

Tento článek je překladem textu Creating pseudo 3D games with HTML 5 canvas and raycasting: Part 2, jehož autorem je Jacob Seidelin a je zde zveřejněn s laskavým svolením Opera Software.

Komentáře

Subscribe
Upozornit na
guest
0 Komentářů
Inline Feedbacks
View all comments

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.