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:

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
V 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.
Přehled komentářů