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

Zdroják » JavaScript » Vytváříme nepřátele do pseudo 3D hry v HTML5 canvasu

Vytváříme nepřátele do pseudo 3D hry v HTML5 canvasu

Nedávno jsme si vytvořili pěknou implementaci Wolfensteina. Dnes do ní vedle hráče přidáme i další bytosti – strážce. Ti budou hráče vytrvale pronásledovat, pokud jim v tom nebude zrovna bránit zeď nebo jiná překážka. Jakmile hráče dostihnou, tak se na něj upřeně podívají a…

Tento článek je překladem anglického originálu vydaného na portálu Dev.Opera.

Nepřátelé

Až dosud bylo v našem hradě poměrně bezpečno, proč nepřidat trochu vzrušení? Přidáme nový typ objektu (sprite), který bude schopen pohybovat se po hrací ploše stejně jako hráč. Použijeme následující obrázek (jedná se o sadu několika obrázků v jednom fyzickém obrázku):

Guard

Obrázek se strážcem v 13 pozicích.

Typy nepřátel a jejich pozice na mapě definujeme podobně, jako jsme posledně definovali statické objekty. Každý typ nepřítele (my budeme mít jeden jeden typ – strážce) má několik vlastností, např. rychlost pohybu, rychlost rotace a celkový počet „stavů“. Stavy odpovídají jednotlivým obrázkům postavy, nepřítel ve stavu 0 v klidu stojí, zatímco nepřítel ve stavu 10 leží mrtvý na podlaze. V tomto článku použijeme jen 5 prvních stavů, aby strážci mohli pronásledovat hráče po hrací ploše. Bojové stavy si necháme na jindy.

var enemyTypes = [
    { img : "guard.png", moveSpeed : 0.05, rotSpeed : 3, totalStates : 13 }
];

var mapEnemies = [
    {type : 0, x : 17.5, y : 4.5},
    {type : 0, x : 25.5, y : 16.5}
];

Dále vytvoříme funkci initEnemies, která bude volaná z funkce init. Funguje podobně jako naše initSprites, obsahuje ale několik rozdílů. Zatímco statické objekty jsou svázány se specifickým políčkem na mapě, nepřátelé se mohou mezi nimi pohybovat, nemůžeme proto použít stejnou dvourozměrnou strukturu podobnou mapě pro uložení jejich pozic. Místo toho prostě uložíme pozice nepřátel do jednoduchého pole, i když to znamená, že při každém renderovacím cyklu budeme muset toto pole celé projít, abychom zjistili, které z nich máme zobrazit. Jelikož nebudeme mít příliš mnoho nepřátel (aspoň zatím), nebude to pro nás velký problém.

var enemies = [];
function initEnemies() {
    var screen = $("screen");
    for (var i=0;i<mapEnemies.length;i++) {
        var enemy = mapEnemies[i];
        var type = enemyTypes[enemy.type];
        var img = dc("img");
        img.src = type.img;
        img.style.display = "none";
        img.style.position = "absolute";

        enemy.state = 0;
        enemy.rot = 0;
        enemy.dir = 0;
        enemy.speed = 0;
        enemy.moveSpeed = type.moveSpeed;
        enemy.rotSpeed = type.rotSpeed;
        enemy.totalStates = type.totalStates;
        enemy.oldStyles = {
            left : 0,
            top : 0,
            width : 0,
            height : 0,
            clip : "",
            display : "none",
            zIndex : 0
        };
        enemy.img = img;
        enemies.push(enemy);
        screen.appendChild(img);
    }
}

Podobně, jak jsme to posledně dělali pro statické objekty, vytvoříme pro každého nepřítele prvek img a přidáme k objektu potřebné informace. Dále budeme potřebovat funkci renderEnemies, která bude volaná z funkce renderCycle. Budeme mít cyklus přes všechny nepřátele a rozhodneme, zda se nacházejí před hráčem, porovnáním relativního úhlu mezi nimi a směrem, kterým se hráč dívá. Kód pro jejich zobrazení je podobný jako u zobrazení statických objektů. Pokud nejsou v zorném poli hráče, náš kód jednoduše příslušný obrázek schová.

function renderEnemies() {
    for (var i=0;i<enemies.length;i++) {
        var enemy = enemies[i];
        var img = enemy.img;
        var dx = enemy.x - player.x;
        var dy = enemy.y - player.y;
        var angle = Math.atan2(dy, dx) - player.rot;    // úhel relativní ke směru pohledu hráče
        if (angle < -Math.PI) angle += 2*Math.PI; // úhel z +/- PI
        if (angle >= Math.PI) angle -= 2*Math.PI;
        // nachází se nepřítel před hráčem?
        if (angle > -Math.PI*0.5 && angle < Math.PI*0.5) {
            var distSquared = dx*dx + dy*dy;
            var dist = Math.sqrt(distSquared);
            var size = viewDist / (Math.cos(angle) * dist);
            var x = Math.tan(angle) * viewDist;
            var style = img.style;
            var oldStyles = enemy.oldStyles;

            // výška je rovna výšce spritu
            if (size != oldStyles.height) {
                style.height =  size + "px";
                oldStyles.height = size;
            }
            // šířka je rovna šířce spritu krát počet stavů
            var styleWidth = size * enemy.totalStates;
            if (styleWidth != oldStyles.width) {
                style.width = styleWidth + "px";
                oldStyles.width = styleWidth;
            }

            var styleTop = ((screenHeight-size)/2);
            if (styleTop != oldStyles.top) {
                style.top = styleTop + "px";
                oldStyles.top = styleTop;
            }

            // umístění na pozici x, upravené pro rozměr spritu
            var styleLeft = (screenWidth/2 + x - size/2 - size*enemy.state);
            if (styleLeft != oldStyles.left) {
                style.left = styleLeft + "px";
                oldStyles.left = styleLeft;
            }

            var styleZIndex = -(distSquared*1000)>>0;
            if (styleZIndex != oldStyles.zIndex) {
                style.zIndex = styleZIndex;
                oldStyles.zIndex = styleZIndex;
            }

            var styleDisplay = "block";
            if (styleDisplay != oldStyles.display) {
                style.display = styleDisplay;
                oldStyles.display = styleDisplay;
            }

            var styleClip = "rect(0, " + (size*(enemy.state+1)) + ", " + size + ", " + (size*(enemy.state)) + ")";
            if (styleClip != oldStyles.clip) {
                style.clip = styleClip;
                oldStyles.clip = styleClip;
            }
        } else {
            var styleDisplay = "none";
            if (styleDisplay != enemy.oldStyles.display) {
                img.style.display = styleDisplay;
                enemy.oldStyles.display = styleDisplay;
            }
        }
    }
}

Jak můžete vidět, objekt oldStyles opět používáme, abychom zajistili, že vlastnosti style budeme nastavovat jen v případě, že se opravdu změní. Pozici x na obrazovce určíme podobně jako by se jednalo o statický objekt, navíc bereme v úvahu jeho stav. Například, pokud je aktuální stav 3 (jeden ze stavů chůze) bude celý obrázek pozicován o trojnásobek šířky spritu doleva. Oříznutí celého obrázku pomocí CSS clippingu zajistí, že viditelný bude pouze aktuální stav objektu.

Tím jsme si vytvořili několik nepřátel, kteří zatím jen stojí a na hráče nedůvěryhodně zírají, jak vidíte na následujícím obrázku.

Stojící nepřítel

Nadešel čas na nějakou umělou inteligenci. Do funkce gameCycle přidáme volání funkce ai, která naše nepřátele oživí. Také provedeme malou úpravu ve funkci move. Zatím byla vázaná na objekt player. Upravíme ji tak, že bude přijímat dva argumenty: timeDelta, který již známe, a nový entity, což může být jakýkoliv objekt, které má vlastnosti potřebné k jeho pohybu po mapě (tj. x, y, moveSpeed, rot atd.). Funkce move bude pracovat s tímto objektem místo přímé práce s objektem player. Proto upravíme i její volání z gameCycle. Od této chvíle můžeme použít stejnou funkci pro pohyb hráče i dalších objektů (nepřátel).

function gameCycle() {

    ...

    move(player, timeDelta);
    ai(timeDelta);

    ...

}

Nyní k naší funkci ai. U každého nepřítele vypočteme jeho vzdálenost od hráče a pokud bude vyšší než mezní hodnota (v našem případě hodnota 4), bude nepřítel hráče pronásledovat. Při pronásledování nastavíme jeho rotaci, aby směřovala směrem k hráči a rychlost nastavíme na 1. Následně zavoláme stejnou funkci move, kterou jsme použili pro pohyb hráče, jen v tomto případě jí jako argument předáme objekt se strážcem. Budou se na něj tak automaticky aplikovat stejná kolizní pravidla jako na hráče.

function ai(timeDelta) {
    for (var i=0;i<enemies.length;i++) {
        var enemy = enemies[i];
        var dx = player.x - enemy.x;
        var dy = player.y - enemy.y;
        // distance from enemy to to player
        var dist = Math.sqrt(dx*dx + dy*dy);
        // pokud je vzdálenost větší než x, bude strážce pronásledovat hráče
        if (dist < 4) {
            var angle = Math.atan2(dy, dx);
            enemy.rotDeg = angle * 180 / Math.PI;
            enemy.rot = angle;
            enemy.speed = 1;
            var walkCycleTime = 1000;
            var numWalkSprites = 4;
            enemy.state = Math.floor((new Date() % walkCycleTime) / (walkCycleTime / numWalkSprites)) + 1;
        // pokud není, zastav ho.
        } else {
            enemy.state = 0;
            enemy.speed = 0;
        }
        move(enemies[i], timeDelta);
    }
}

Nastavíme zde také vlastnost state, kterou používáme ve funkci renderEnemies. Pokud se strážce nehýbe, je state roven nule 0. Pokud se pohybuje, pak probíhá cyklus stavů od 1 do 4. Použitím operátoru % (modulo) na aktuální čas s časem pro dokončení kráčejícího cyklu získáme pěkný cyklus pro pohyb strážce.

A máme hotovo. Jak vidíte na následujícím obrázku, stráže nyní pronásledují hráče, dokud se k němu nepřiblíží na jistou vzdálenost. Nejedná se sice o příliš pokročilou umělou inteligenci, ale máme nějaký začátek. Snažit se lapit stráže do rohu vás na pár minut zabaví.

Běžící stráže

Výsledná hra s pohybujícími se strážci.

Na příště

Děkuji všem, kdo to celé přečetli – doufám, že jste se bavili. V nějakém dalším článku se pravděpodobně podíváme na některá z následujících té­mat:

  • Zbraně / střílení. Když jsme vytvořili nepřátele, potřebujeme také jednoduchý a účinný způsob, jak se jich zbavit. A nejlépe s použitím střelných zbraní.
  • Sbíratelné objekty (zlato, munice atd.) a spolu s nimi přidáme hráčovi další vlastnosti jako je skóre nebo zdraví.
  • Herní rozhraní. Jakmile budeme mít čísla, budeme je chtít někde zobrazit.
  • Zvuky. Takové zastřelení nepřítele by mělo být doprovázeno tím správným zvukem.

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.