Optimalizujeme pseudo 3D hru v HTML5 canvasu

Wolfenstein

Možná si ještě vzpomenete na článek, ve kterém jsme vytvářeli Wolfensteina pomocí JavaScriptu přímo v prohlížeči. Dnes budeme pokračovat. Nejprve zlepšíme rychlost a pak začneme vylepšovat hru samotnou. Opět budeme používat pouze HTML, JavaScript a kaskádové styly.

Seriál: Wolfenstein v prohlížeči (4 díly)

  1. Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem 16.12.2008
  2. Optimalizujeme pseudo 3D hru v HTML5 canvasu 18.5.2009
  3. Přidáváme objekty do pseudo 3D hry v HTML5 canvasu 21.5.2009
  4. Vytváříme nepřátele do pseudo 3D hry v HTML5 canvasu 27.5.2009

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

Úvod

Toho je druhý článek o tvorbě her po vzoru Wolfenstein pomocí JavaScriptu, DOMu a HTML5 canvasu; diskutované techniky jsou podobné těm v autorově projektu WolfenFlickr. V předchozím článku Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem jsme vytvořili základní mapu, na které se mohl hráč pohybovat v pseudo 3D vyrenderovaném prostředí pomocí techniky zvané raycasting.

V tomto článku nejprve vylepšíme kód, který máme připravený z minula, zoptimalizujeme renderovací proces, abychom získali lepší výkon a vylepšíme detekci kolizí mezi hráčem a zdí. V druhé části implementujeme statické sprity, které dodají hradu tu správnou atmosféru, a vytvoříme jednoho nebo dva nepřítele. Takto bude vypadat hotová hra:

Screenshot hotové hry po vzoru Wolfenstein
Kompletní zdrojový kód (pod MIT licencí) je k dispozici ke stažení.

Optimalizace

Zanechme řečnění a pojďme se podívat na optimalizaci našeho původního kódu.

Oddělení renderování a herní logiky

prvním článku bylo z důvodu zjednodušení propojeno renderování s herní logikou. První věcí, kterou teď uděláme, je jejich rozdělení. To znamená vyčlenit raycasting a renderování mimo funkci gameCycle a vytvoření nové funkce renderCycle. Renderování je náročná činnost, která bude vždy ovlivňovat výslednou rychlost celé hry, ale pokud renderování vyčleníme, můžeme mít lepší kontrolu nad rychlostí a v případě potřeby můžeme obě komponenty spouštět s rozdílnou frekvencí. Kupříkladu funkce gameCycle může probíhat s konstantní rychlostí, zatímco renderovací cyklus může běžet „tak často, jak to půjde“. My se pokusíme obě spouštět 30× za vteřinu.

var lastGameCycleTime = 0;
var gameCycleDelay = 1000 / 30; // 30 fps - žádoucí frekvence volání

function gameCycle() {
    var now = new Date().getTime();
    // čas od posledního spuštění
    var timeDelta = now - lastGameCycleTime;
    move(timeDelta);

    var cycleDelay = gameCycleDelay;
    // časovač pravděpodobně nepoběží tak rychle
    // zjisti, kolik času uplynulo od posledního spuštění
    if (timeDelta > cycleDelay) {
        cycleDelay = Math.max(1, cycleDelay - (timeDelta - cycleDelay))
    }

    lastGameCycleTime = now;
    setTimeout(gameCycle, cycleDelay);
}

Ve funkci gameCycle kompenzujeme zpoždění způsobené renderovací funkcí porovnáním času posledního volání funkce gameCycle s ideálním časem gameCycleDelay. Podle výsledku porovnání pak upravíme čas dalšího volání skrze  setTimeout.

Tento časový rozdíl nyní používáme při volání funkce move, která se stará o pohyb hráče.

function move(timeDelta) {
    // čas timeDelta uplynul od posledního pohybu.
    // K pohybu mělo dojít po uplynytí času gameCycleDelay,
    // spočítej proto, čím máme pohyb vynásobit
    // aby byla rychlost hry konstantní
    var mul = timeDelta / gameCycleDelay;
    var moveStep = mul * player.speed * player.moveSpeed; // o kolik se hráč posune v daném směru
    player.rotDeg += mul * player.dir * player.rotSpeed; // připočti otočení, pokud se hráč otáčí (player.dir != 0)
    player.rotDeg %= 360;
    var snap = (player.rotDeg+360) % 90
    if (snap < 2 || snap > 88) {
        player.rotDeg = Math.round(player.rotDeg / 90) * 90;
    }

    player.rot = player.rotDeg * Math.PI / 180;
    ...
}

Nyní můžeme využít času timeDelta, abychom porovnali, kolik času uplynulo, s tím, kolik času mělo uplynout. Pokud tímto faktorem vynásobíme pohyb a rotaci, bude se hráč pohybovat rovnoměrně i v případě, že hra nepoběží přesně v 30 fps. Existuje jedna nevýhoda tohoto přístupu, a sice, pokud by bylo zpoždění opravdu velké, je tu riziko, že hráč bude schopen projít zdí, dokud nevytvoříme lepší detekci kolizí nebo nezměníme gameCycle, aby funkce move byla v závislosti na zpoždění volaná několikrát.

Jelikož funkce gameCycle nyní řeší pouze herní logiku (tj. zatím jen pohyb hráče), bylo nutné vytvořit novou funkci renderCycle, která obsahuje podobné měření času. Najdete ji v příloze.

Optimalizujeme renderování

Nyní trochu zoptimalizujeme renderovací proces. Pro každý svislý proužek (strip) nyní používáme prvek div s nastavenou hodnotou overflow:hidden pro skrytí těch částí textury, které nemusí být u každého bodu zobrazeny. Když místo toho použijeme CSS clipping, můžeme se nadbytečných div ů zbavit a budeme tak v každém renderovacím cyklu pracovat s polovičním množstvím prvků DOM.

U některých prohlížečů (Opera) se o něco zlepší výkon, pokud velký obrázek s texturami rozdělíme na několik malých obrázků, každý s jednou texturou zdi. Vytvoříme přepínač mezi používáním velkoobrázkové textury a oddělených obrázků. Rozdělením textury na menší obrázky můžeme v Opeře použít hezčí textury bez překonání limitu 19 barev, který jsme diskutovali v předchozím článku, protože textura nemusí sdílet stejnou paletu barev. Textury z původního Wolfensteina 3D používaly každá jen 16 barev, máme tedy dostatek místa. Firefox funguje rychleji, když použijeme jednu velkou monolitickou texturu, náš kód proto bude obsahovat obě možnosti, mezi kterými budeme automaticky přepínat pomocí detekce prohlížeče.

Trochu na rychlosti získáme, když budeme upravovat vlastnost style proužku jen v případě, že se opravdu změní. Jak se pohybuje hráč po herní ploše, všechny proužky mění své pozice, dimenze a oříznutí (clipping), ale nemusí se všechny měnit, pokud se hráč od posledního renderování posunul (nebo otočil) jen o malou hodnotu. Proto každému proužku přiřadíme objekt oldStyles, abychom mohli během renderování porovnat nové hodnoty s původními, než nastavíme nové hodnoty kaskádovým stylům.

Nejprve musíme upravit naši funkci initScreen, která se stará o vytvoření prvků pro všechny naše proužky (stripy). Místo vytváření prvků div spolu s prvky img, vytvoříme jen prvky img. Nová funkce initScreen bude vypadat následovně:

function initScreen() {
    var screen = $("screen");
    for (var i=0;i<screenWidth;i+=stripWidth) {
        var strip = dc("img");
        strip.style.position = "absolute";
        strip.style.height = "0px";
        strip.style.left = strip.style.top = "0px";
        if (useSingleTexture) {
            strip.src = (window.opera ? "walls_19color.png" : "walls.png");
        }

        strip.oldStyles = {
            left : 0,
            top : 0,
            width : 0,
            height : 0,
            clip : "",
            src : ""
        };

        screenStrips.push(strip);
        screen.appendChild(strip);
    }
}

Nyní můžete vidět, že pro každý proužek je vytvořen pouze jeden prvek DOMu ( img). Vytváříme také pseudo-style objekt k uchování aktuálních hodnot každého proužku .

Nyní upravíme funkci castSingleRay, aby dokázala pracovat s našimi novými proužky. Pro použití CSS clippingu namísto maskování div ů nemusíme měnit žádné hodnoty; použijeme je prostě pro jiné vlastnosti kaskádových stylů. Namísto vytváření obdélníkové masky pomocí div u, nyní nastavíme vlastnost clip pro vytvoření patřičné masky. Obrázek bude nyní pozicován relativně k naší obrazovce ( div s id=„screen“) namísto k příslušnému divu.

V kódu uvedeném níže najdete i kontrolu hodnot oproti oldStyles:

function castSingleRay(rayAngle, stripIdx) {
    ...
    if (dist) {
        ...
        var styleHeight;
        if (useSingleTexture) {
            // posun vršek na patřičnou texturu stěny
            imgTop = Math.floor(height * (wallType-1));
            var styleHeight = Math.floor(height * numTextures);
        } else {
            var styleSrc = wallTextures[wallType-1];
            if (strip.oldStyles.src != styleSrc) {
                strip.src = styleSrc;
                strip.oldStyles.src = styleSrc
            }
            var styleHeight = height;
        }
        if (strip.oldStyles.height != styleHeight) {
            strip.style.height = styleHeight + "px";
            strip.oldStyles.height = styleHeight
        }

        var texX = Math.round(textureX*width);
        if (texX > width - stripWidth)
            texX = width - stripWidth;
        var styleWidth = Math.floor(width*2);
        if (strip.oldStyles.width != styleWidth) {
            strip.style.width = styleWidth +"px";
            strip.oldStyles.width = styleWidth;
        }

        var styleTop = top - imgTop;
        if (strip.oldStyles.top != styleTop) {
            strip.style.top = styleTop + "px";
            strip.oldStyles.top = styleTop;
        }

        var styleLeft = stripIdx*stripWidth - texX;
        if (strip.oldStyles.left != styleLeft) {
            strip.style.left = styleLeft + "px";
            strip.oldStyles.left = styleLeft;
        }

        var styleClip = "rect(" + imgTop + ", " + (texX + stripWidth)  + ", " + (imgTop + height) + ", " + texX + ")";
        if (strip.oldStyles.clip != styleClip) {
            strip.style.clip = styleClip;
            strip.oldStyles.clip = styleClip;
        }
        ...
    }
    ...
}

Nyní můžete vyzkoušet naše optimalizované demo a schálně si je porovnejte s ukázkou před optimalizací.

Pokračování příště

Příště navážeme lepší detekcí kolizí a umístíme na naši herní plochu nějaké předměty (stoly, lampy), které jí dodají tu správnou atmosféru. A neměli bychom zapomenout na živé nepřátele.

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.

Vystudoval jsem biochemii. Vymyslel jsem a založil Zdroják. Jsem vyhlášeným expertem na likvidaci komentářů.

Nejsem váš hodný tatínek, který vás bude brát za ručičku, já jsem zlý moderátor diskusí. Smiřte se s tím!

Komentáře: 18

Přehled komentářů

lobo smutne
Anonymní Re: smutne
Brunelus Re: smutne
Anonymní Re: smutne
Karel Re: smutne
pas Re: smutne
Martin Hassman Re: smutne
Raven Re: smutne
pas Re: smutne
aprilchild Re: smutne
Karel Re: smutne
Anonymní Re: smutne
dc Re: smutne
Martin Hassman Re: smutne
Anonymní wtf?
m Re: wtf?
Martin Hassman Re: wtf?
petrkrcmar Firefox vs Chrome
Zdroj: http://www.zdrojak.cz/?p=3010