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

Zdroják » JavaScript » Optimalizujeme pseudo 3D hru v HTML5 canvasu

Optimalizujeme pseudo 3D hru v HTML5 canvasu

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.

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.

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.

Komentáře

Subscribe
Upozornit na
guest
18 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
lobo

cele toto prenasanie aplikacii do browseru je uz dost strasne, ale to aby sa v tom grafika robila je este o dost horsie…

myslim ze jednou z dolezitych veci pri tvorbe hocicoho je vyber vhodneho jazyka a prostredia – a to podla mna rozhodne nie je smer ktorym sa uberaju 'vsetko v browseri aplikacie'

staci ako hnusne je napisany samotny flashplayer a nehovorim o 'aplikaciach(hrach)' v nom – predsa mi nemoze 'hra' ktora je remake 1:1 nejakej hry zo ZX Spectra z roku 1987 zozrat na notebooku komplet vykon 1.2GHz procesora s tym ze sa procak hreje na 67C

Anonymní

Já myslím, že podceňujete důležitý faktor – uživatel to chce mít co nejjednodušší. Nechce nikam nic instalovat např. kvůli problému s právy na stanicích. Spousta lidí (např. v práci, ve škole) si chce zahrát nějakou hru (jestli o pauze nebo jindy, to neřešme). Jim je nějaká grafika víceméně ukradená, stejně jim půjde o hratelnost.
Dalším případem můžou být různé aplikace např.u realitních kanceláří – virtuální prohlídky prostor apod. – kvůli tomu si (téměř) nikdo aplikaci do kompu instalovat nebude.
Jste podle mě dost konzervativní a nemyslím, že vám vývoj dá za pravdu.

Brunelus

A není to selhání všeho programátorstva, že to uživatel nemůže mít jednoduché, aniž by se to dělalo takhle namáhavě a zoufale neefektivně?

Anonymní

To je evoluce… Za par let bude javascript optimalizovany i pro grafiku. Pred par lety taky nikoho nenapadlo delat vypocetne slozite aplikace v Jave… a dnes?

Karel

To není evoluce, to je lenost a nenažranost. Díky tomu dnes mám počítač stonásobně rychlejší než v roce 1995 (486DX2, 40MHz), ale přesto bootuje dvakrát déle než tenkrát a i OOo startuje výrazně pomaleji než AmiPro tenkrát. A to ještě nemluvíme o tom, jak krásně na té 486 běžel Wolfenstein.

Prý evoluce :-)

pas

V době, kdy byl počítač izolovaným přístrojem, na kterém se psalo a počítalo, by byly v pořádku vaše námitky o nenažranosti, pokud by tam existovaly nějaké zbytečné zpomalovací vrstvy. Ale dnes vstupuje do hry spousta dalších faktorů, už to ani náhodou není o schopnosti nebo neschopnosti někoho naprogramovat něco efektivně.

Raven

Tohle mi dost připomíná vývoj na demoscéně. Například nástup grafických karet s 3D akcelerací vedl k šoku, s kterým se scéna velmi dlouho vyrovnávala. A paradoxně se s tím vyrovnala právě dík lidem, kteří 3D akcelerátory a jiné nové technologie odmítali. Vlastně úplně stejné to bylo třeba s fotografií a malířstvím.

pas

Web je jedna velká laboratoř. Kdyby se před 20ti lety vědělo, k čemu se bude dnes používat, byl by navržen úplně jinak. To se ale nestalo, tak se musí jít cestou postupné evoluce. Flash byl původně určený k něčemu jinému než k tvorbě aplikací. Totéž platí pro HTML. Proč se dnes (podle někoho násilně) tyto technologie mění, aby se v nich dalo vyvíjet? Nejspíš proto, že je po tom poptávka ze strany vývojářů. A ta je nejspíš proto, že to má nějaké výhody, které nejspíš převažují nad nevýhodami.

aprilchild

Zkratka se smirte s tim, ze budouci OS je jen okno prohlizece. Vracime se oklikou do 60-tych let. Zacali jsme dumb textovymi terminaly, prosli si ulety na vsechny strany (desktop OS) a vratime se pokojne k "ne tak dumb" terminalum. Vsechno ostatni bude hereze pro par vyvrhelu:), mainstream bude spolehat na vypocetni silu mracku;). Muzete s tim nesouhlasit, ale to je asi tak vsechno, co s tim nadelate.

Karel

No a nebo počkat na dobu, kdy se zase začne prosazovat "lokální počítač". Navíc to podle aktuálního stavu IT nevypadá jako až tak vzdálená budoucnost. Podle některých ukazatelů to dokonce vypadá, že éra cloudcomputingu a RIA už pomalu končí a přichází doba malých autonomních systémů, které se snadno zasíťují, ale síť k vlastnímu běhu aplikací nepotřebují.

Anonymní

Rofl, no to určite…

dc

dovolim si nesuhlasit v jednej veci.To ze momentalne frci ria a web app nema nic spolocne s tym ze je tato technologia nejak zasadne vyhodnejsia.Na kopec miestach sa napchaju web app pritom to nema vobec logiku a na druhu stranu firma funguje s zivotne dolezitymi datami v excely pritom by sa im hodil pomali aj nejaky cluster.Cele je to nie o zdravom rozume ale o politike,financiach atd. A ria/web devel teraz frci lebo je to nieco nove a da sa to dobre predat.Daju sa na to nabalit nove marketingove zvasty typu klient moze byt hocikto (jedno aky prehliadac/os),dostupnost odvsadial, jednoducha udrzba a podobne.Pritom vsetko su to take polopravdy a maju svoje pre a proti, len este nie kazda firma to ma obslapane tak sa da ukecat.

Anonymní

Čo sa stalo s diskusiou k tomuto článku? Všetky príspevky zmizli…

m

Ještě mám z rána článek v prohlížeči:

smutne lobo Dnes 4:18 Nový
└ Re: smutne anonymní uživatel Dnes 6:53 Nový
└ Re: smutne Brunelus Dnes 7:06 Nový
└ Re: smutne anonymní uživatel Dnes 7:54 Nový

U starších článků taky zmizely diskuse. (Něco jsem hledal asi 2 týdny zpět)

petrkrcmar

Je zajímavé, že Firefox podává lepší výsledky. Zatímco optimalizovaná varianta na něm kolísá mezi 2x a 3x FPS, na Chrome to lítá o desítku níž, čili mezi 1x a 2x (2x = 20 až 29). Přesně se to nedá určit, litá to moc rychle.

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.