Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem

Wolfenstein

Stále častěji se objevují hry napsané pomocí HTML a JavaScriptu. Ale ne každý umí pomocí nich naprogramovat hru, která používá 3D zobrazení. V článku najdete návod k napsání herního enginu připomínajícího známou hru Wolfenstein 3D. Použijeme k tomu 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

S rostoucím výkonem webových prohlížečů v poslední době je jednodušší implementovat v JavaScriptu i hry komplikovanější než jsou třeba takové piškvorky. Už nepotřebujeme Flash, abychom docílili kýžených efektů, a s pomocí značky canvas z HTML5 je vytváření pěkných her se slušnou grafikou snazší než kdy předtím. Jednou takovou hrou, resp. herním enginem, kterou jsem chtěl již nějaký čas implementovat, byl pseudo-3D engine, který byl použit ve starém Wolfensteinovi od iD Software. Zkusil jsem dva odlišné postupy, prvním bylo vytvoření skutečného 3D enginu za pomoci canvasu, druhým pak raycasting pomocí DOM.

V tomto článku popíšu detaily z toho druhého projektu a ukážu, jak si můžete vytvořit vlastní pseudo-3D engine. Píšu pseudo-3D, protože napřed vytvoříme 2D mapu bludiště, kterou ale budeme hráči s jistým omezením zobrazovat jako 3D. Nemůžeme ale např. povolit kameře otáčet se podle jiné než svislé osy. Tím zajistíme, že všechny svislé čáry z herního světa budou svislé i na obrazovce, což je pro nás důležité, protože budeme pracovat v pravoúhlém světě DHTML. Nepovolíme hráčovi ani skákat nebo se krčit, ačkoliv něco takového by šlo implementovat bez velkých potíží. Nepůjdu příliš hluboko do teoretických aspektů raycastingu, ačkoliv se jedná o relativně jednoduchý koncept. Místo toho vás odkážu na výborný tutoriál, který napsal F. Permadi, a který problematiku popisuje mnohem detailněji, než bych si zde mohl dovolit já.

V tomto článku předpokládáme u čtenáře slušnou znalost JavaScriptu, letmé obeznámení se značkou canvas z HTML5 a znalost základních pravidel trigonometrie. Některé věci nejlépe vysvětlíme v ukázkách kódu, které v článku najdete, ovšem v článku nerozebíráme zdrojový kód kompletní. Pro další detaily si stáhněte kompletní (komentovaný) kód.

První kroky

Jak jsme zmínili, základem našeho enginu bude 2D mapa, takže prozatím zapomeneme na 3. dimenzi a soustředíme se na tvorbu 2D bludiště, kterým bude možné procházet. O vykreslení bludiště se nám postará značka canvas a ve výsledku ji použijeme i pro zobrazení orientační mapy v naší hře. Samotná hra bude obsahovat manipulaci se skutečnými prvky DOM. To znamená, že budeme podporovat všechny prohlížeče (tj. Firefox 2+, Operu 9+, Safari 3, IE7). Značku canvas v současnosti podporuje Firefox, Opera, Safari, ale nikoliv Internet Explorer. Naštěstí to dokážeme obejít pomocí projektu ExCanvas, což je malá javascriptová knihovna, která canvas částečně emuluje pomocí VML.

Mapa

Nejdřív potřebujeme formát pro uložení mapy. Jednoduchou možností je použít vnořená pole. Každý prvek ve vnořeném poli bude obsahovat celé číslo odpovídající kamenité zdi (2), stěně (1) – v podstatě každé číslo vetší než jednička je nějakou zdí nebo překážkou – nebo volnému prostoru (0). Rozlišení typu zdi využijeme později při používání textur.

// mapa 32x24
var map = [
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
    ...
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];

Naše základní mapa vypadá jako obrázek 1.

Mapa bludiště

Obrázek 1: Statická minimapa (ukázka vykreslení pomocí canvasu).

Můžeme procházet našimi vnořenými poli. Kdykoliv potřebujeme zjistit typ zdi, snadno tak učiníme pomocí map[y][x].

Nyní vytvoříme inicializační funkci, která vše připraví pro spuštění hry. Nejprve projde naše data a do minimapy v canvasu  vykreslí barevný čtverec, kdykoliv narazí na pevnou zeď. Tím vytvoříme pohled shora jako na našem obrázku 1. Klikněte na odkaz pod obrázkem, pokud chcete vidět vygenerovanou minimapu v akci.

var mapWidth = 0;  // počet bloků mapy ve směru osy x
var mapHeight = 0;  // počet bloků mapy ve směru osy y
var miniMapScale = 8;  // počet pixelů na jeden blok mapy

function init() {
  mapWidth = map[0].length;
  mapHeight = map.length;

  drawMiniMap();
}

function drawMiniMap() {
  // nakresli pohled shora na mapu
  var miniMap = $("minimap");
  miniMap.width = mapWidth * miniMapScale;  // přepočet vnitřních rozměrů canvasu
  miniMap.height = mapHeight * miniMapScale;
  miniMap.style.width = (mapWidth * miniMapScale) + "px";  // přepočet CSS rozměrů canvasu
  miniMap.style.height = (mapHeight * miniMapScale) + "px";

  // projdi všechny bloky na mapě
  var ctx = miniMap.getContext("2d");
  for (var y=0;y<mapHeight;y++) {
    for (var x=0;x<mapWidth;x++) {
      var wall = map[y][x];
      if (wall > 0) {  // když je na (x,y) zeď ...
        ctx.fillStyle = "rgb(200,200,200)";
        ctx.fillRect(  // ... nakresli ji
          x * miniMapScale,
          y * miniMapScale,
          miniMapScale,miniMapScale
        );
      }
    }
  }
}

Pohyb hráče

Tím máme hotovo vykreslení mapy, ale nic moc se na ní neděje, protože se na ní nemáme hráče. Vytvoříme další funkci gameCycle(). Ta se bude opakovaně volat, aby průběžně aktualizovala zobrazení mapy. Přidáme proměnné udržující aktuální polohu hráče (x,y) v našem herním světě a směr, kterým se právě dívá, tj. úhel. Pak rozšíříme náš herní cyklus o volání funkce move(), která obstará pohyb hráče.

function gameCycle() {
  move();
  updateMiniMap();
  setTimeout(gameCycle,1000/30); // 30 FPS
}

Proměnné týkající se hráče uložíme do jednoho objektu. To nám usnadní další rozšiřování pohybové funkce v budoucnu o další položky. Stejně by fungovala i pro další „entity“, pokud by obsahovaly stejné rozhraní, tj. měly stejné vlastnosti.

var player = {
  x : 16,  // aktuální souřadnice hráče
  y : 10,
  dir : 0,  // směr, kterým se hráč otáčí, buď -1 doleva nebo 1 doprava.
  rot : 0,  // aktuální úhel otočení
  speed : 0,  // pohybuje se hráč dopředu (speed = 1) nebo dozadu (speed = -1).
  moveSpeed : 0.18,  // jak daleko (v jednotkách mapy) se hráč každým krokem posune
  rotSpeed : 6 * Math.PI / 180  // o kolik se hráč v jednom kroku otočí (v radiánech)
}

function move() {
  var moveStep = player.speed * player.moveSpeed; // o kolik se hráč posune v daném směru

  player.rot += player.dir * player.rotSpeed; // připočti otočení, pokud se hráč otáčí (player.dir != 0)

  var newX = player.x + Math.cos(player.rot) * moveStep; // spočti novou pozici hráče pomocí trigonometrie
  var newY = player.y + Math.sin(player.rot) * moveStep;

  player.x = newX; // nastav novou pozici
  player.y = newY;
}

Jak můžete vidět, pohyb a rotace je založena na tom, zda jsou player.dir a player.speed nastaveny, tj. nejsou nulové. Aby se hráč skutečně pohyboval, musíme s těmito proměnnými svázat některé klávesy. Svážeme klávesy šipka nahoru, šipka dolů se změnou směru.

function init() {
  ...
  bindKeys();
}

// svážeme události klávesnice s herními funkcemi (pohybem apod.)
function bindKeys() {
  document.onkeydown = function(e) {
    e = e || window.event;
    switch (e.keyCode) { // která klávesa byla stisknuta?
      case 38: // šipka nahoru, posun hráče dopředu, nastav rychlost
        player.speed = 1; break;
      case 40: // šipka dolu, posud hráče zpět, nastav zápornou rychlost
        player.speed = -1; break;
      case 37: // šipka vlevo, otoč hráče vlevo
        player.dir = -1; break;
      case 39: // šipka vpravo, otoč hráče vpravo
        player.dir = 1; break;
    }
  }
  // zastav pohyb a otáčení hráče, když je klávesa uvolněna
  document.onkeyup = function(e) {
    e = e || window.event;
    switch (e.keyCode) {
      case 38:
      case 40:
        player.speed = 0; break;
      case 37:
      case 39:
        player.dir = 0; break;
    }
  }
}

Jak můžete vidět na obrázku 2 (podívejte se i na odkázanou živou ukázku), máme na mapě pohybujícího se hráče.

Pohyb hráče, zatím bez detekce kolizí

Obrázek 2: Pohyb hráče, zatím bez detekce kolizí (živá ukázka).

Až sem to bylo snadné. Hráč se může nyní pohybovat po celé mapě, pravděpodobně jste si ale všimli jednoho problému. Tím jsou zdi. Potřebujeme nějaký mechanismus pro detekci kolizí, který by zajistil, že hráč nebude procházet zdí jako duch. Zvolíme nejsnazší řešení, protože ta opravdová detekce kolize by vystačila na samotný článek. Nám postačí ověřit, zda se bod, na který se snažíme hráče přemístit, nenachází uvnitř zdi. Pokud je uvnitř zdi, pak pohyb hráče zastavíme.

function move() {
    ...
  if (isBlocking(newX, newY)) { // můžeme přemístit hráče na novou pozici?
    return; // ne, tak vyskoč.
  }
    ...
}

function isBlocking(x,y) {
  // napřed zajistíme, aby hráč neprošel hranicemi mapy
  if (y < 0 || y >= mapHeight || x < 0 || x >= mapWidth)
    return true;
  // vrať true, pokud daný blok mapy není roven 0, tj. pokud obsahuje zeď.
  return (map[Math.floor(y)][Math.floor(x)] != 0);
}

Jak můžete vidět, nekontrolujeme pouze, zda je daný bod uvnitř zdi, ale také bráníme hráči opustit mapu herního pole. Pokud povede zeď okolo celé mapy, tak bychom si tuto kontrolu mohli odpustit, ale my ji tam necháme. Nyní můžete zkusit třetí ukázku obsahující detekci kolizí. Pokuste se pojít zdí.

Vrhání paprsků

Když už jsme dovolili hráči pohybovat se po našem herním světě, začneme pracovat na třetí dimenzi naší hry. K tomu potřebujeme zjistit, co je z hráčova úhlu pohledu viditelné a co nikoliv; použijeme k tomu techniku nazývanou raycasting. Abyste ji pochopili, představte si paprsky, které vychází z hráče do všech směrů jeho zorného pole. Jakmile paprsek narazí na překážku (zeď), víme, kterou zeď máme v daném směru vykreslit.

Pokud vám tento výklad nedává velký smysl, doporučuji vám Permadiho skvělý tutorial o raycastingu.

Naše herní obrazovka bude mít rozměry 320×240. Pokud by zobrazovala zorný úhel 120° a pokud bychom vyslali paprsek pro každé 2px, budeme potřebovat 160 paprsků (tj. 320/2), neboli 80 na levou a 80 na pravou hráčovu stranu. Tímto způsobem se nám obrazovka rozdělí na svislé proužky (strips) o šířce 2 pixelů. V naší hře použijeme pro zorný úhel jen 60° a rozlišení 4 px na jeden proužek, ale tyto parametry je snadné změnit.

V každém herním cyklu projdeme všechny proužky, spočítáme směr podle rotace hráče a vyšleme paprsek, abychom našli nejbližší zeď, kterou máme zobrazit. Úhel paprsku je určen úhlem spojnice hráče k bodu na obrazovce.

Opravdový raycasting není úplně snadný, ale my využijeme toho, že je naše mapa tak jednoduchá. Jelikož vše na naší mapě je stejnoměrně rozmístěno v stejnoměrné mřížce vodorovných a svislých čar, vystačíme si k řešení našeho úkolu s jednoduchou matematikou. Nejjednodušším způsobem je provést dvojí testování, v prvním najdeme kolize paprsku se „svislými“ zdmi a poté kolize s těmi „vodorovnými“.

Nejprve projdeme svislé proužky na obrazovce. Počet paprsků, které potřebujeme, je stejný jako počet našich proužků.

function castRays() {
  var stripIdx = 0;
  for (var i=0;i<numRays;i++) {
    // kterou částí obrazovky paprsek prochází?
    var rayScreenPos = (-numRays/2 + i) * stripWidth;

    // vzdálenost od pozorovatele k bodu na obrazovce (stará známá Pythagorova věta).
    var rayViewDist = Math.sqrt(rayScreenPos*rayScreenPos + viewDist*viewDist);

    // úhel paprsku relativní ke směru pohledu.
    // pravoúhlý trojúhelník: a = sin(A) * c
    var rayAngle = Math.asin(rayScreenPos / rayViewDist);
    castSingleRay(
      player.rot + rayAngle,    // připočti směr pohledu hráče, abys získal skutečný úhel v herním světě
      stripIdx++
    );
  }
}

Kód funkce castRays() je zavolán jednou pro každý herní cyklus po obstarání základní logiky hry. Nyní přijde skutečné vrhání paprsků, jak jsme si je popsali.

function castSingleRay(rayAngle) {
  // nechť je úhel mezi 0 and 360 stupni
  rayAngle %= twoPI;
  if (rayAngle > 0) rayAngle += twoPI;

  // vysíláme vpravo/vlevo? nahoru/dolů? Určíme kvadrant.
  var right = (rayAngle > twoPI * 0.75 || rayAngle < twoPI * 0.25);
  var up = (rayAngle < 0 || rayAngle > Math.PI);

  var angleSin = Math.sin(rayAngle), angleCos = Math.cos(rayAngle);

  var dist = 0;  // vzdálenost ke zdi, na kterou jsme narazili
  var xHit = 0, yHit = 0  // souřadnice x, y, na kterých došlo k nárazu na zeď
  var textureX;  // souřadnice x zobrazované textury
  var wallX;  // souřadnice (x,y) zdi na mapě
  var wallY;

  // napřed ověřujeme proti svislým překážkám na mapě
  // proto se posuneme na pravou nebo na levou hranici bloku, na kterém stojíme,
  // a poté se posuneme o 1 mapovou jednotku vodorovně.
  // Sklon paprsku definovaný jako sin(angle) / cos(angle)
  // určí, o kolik se musíme posunout svisle.

  var slope = angleSin / angleCos;  // naklonění
  var dX = right ? 1 : -1;  // posuneme se o jednu mapovou jednotku vlevo nebo vpravo
  var dY = dX * slope;  // o kolik se máme posunout nahoru nebo dolů

  var x = right ? Math.ceil(player.x) : Math.floor(player.x);  // startovní vodorovná pozice na jedné hraně aktuálního bloku mapy
  var y = player.y + (x - player.x) * slope;  // startovní svislá pozice. Připočteme horizontální krok, který jsme udělali, vynásobený nakloněním.

  while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
    var wallX = Math.floor(x + (right ? 0 : -1));
    var wallY = Math.floor(y);

    // je tento bod uvnitř zdi
    if (map[wallY][wallX] > 0) {
      var distX = x - player.x;
      var distY = y - player.y;
      dist = distX*distX + distY*distY;  // druhá mocnina vzdálenosti hráče k tomuto bodu.

      xHit = x;  // ulož souřadnice průniku. Použijeme je zatím jen k vykreslení paprsku na minimapě.
      yHit = y;
      break;
    }
    x += dX;
    y += dY;
  }

  // kód pro vodorovné překážky přeskočíme, je v podstatě stejný
    ...

  if (dist)
    drawRay(xHit, yHit);
}

Test vodorovných stěn je téměř identický s testem stěn svislých, proto jsem jej neuvedl. Pokud na stěnu narazíme v obou směrech, budeme počítat s tou bližší. Na konci raycastingu nakreslíme paprsek na naši minimapu. Jen prozatím a čistě za účelem testování. V některých prohlížečích je tato operace příliš náročná, proto ji zrušíme, jakmile začneme se zobrazováním 3D. Kód vám zde neukážu, ale najdete jej v příloze. Výsledek bude vypadat jako na obrázku 3.

2d raycasting na mapce

Obrázek 3: 2D raycasting na minimapě (živá ukázka).

Textury

Než budeme v našem příkladu pokračovat, podívejme se na textury, které budeme používat. Protože moje předchozí projekty byly inspirované hrou Wolfenstein 3D, budeme se jí držet a použijeme několik textur z této hry. Každá textura pro stěnu má 64×64 pixelů a jelikož typ stěny máme uložen v naší mapě, je snadné vybrat správnou texturu pro jakýkoliv blok mapy, např. pokud náš blok mapy obsahuje dvojku, hledáme část obrázku, která je mezi 64 px a 128 px svisle měřeno. Později budeme texturu natahovat, abychom simulovali vzdálenost a výšku, což bude trochu komplikovanější, ale princip je stejný. Jak můžete vidět na obrázku 4, máme dvě verze pro každou texturu, jednu obyčejnou a jednu o něco tmavší. Můžeme jednoduše vyvolat dojem stínu tím, že stěny směřující k severu nebo východu použijí jednu sadu textur a stěny směřující k jihu a západu tu druhou. Ale to ponechám jako cvičení pro čtenáře.

Ukázka textur zdí

Obrázek 4: Textury pro stěny použité v naší implementaci.

Opera a interpolace obrázků

V Opeře existuje malý problém týkající se zobrazování a natahování textur. Zdá se, že Opera používá interní Windows GDI+ metody k zobrazování obrázků a z kdovíjakého důvodu jsou proto neprůhledné obrázky s více jak 19 barvami interpolované (pomocí bikubického nebo bilineárního algoritmu předpokládám). To výrazně zpomalí náš engine, protože pracuje s mnoha obrázky několikrát za vteřinu. Naštěstí lze tuhle funkci vypnout v opera:config pod „Multimedia“ (odškrtněte „Show Animation“ a uložte). Alternativně můžete vaše textury uložit s paletou o méně než 20 barvách nebo vytvořit v textuře alespoň jeden průhledný pixel. Každopádně i tak se zdá, že zůstane jisté zpomalení, než když vypnete interpolaci úplně. Jelikož to může výrazně snížit kvalitu textur, tak by taková oprava neměla být nabízena jiným prohlížečům.

function initScreen() {
    ...
  img.src = (window.opera ? "walls_19color.png" : "walls.png");
    ...
}

Míříme do 3D!

Zdá se, že jste toho už udělali dost, ale stále ještě nemáme položen základ pro zobrazení v pseudo-3D formě. Každý paprsek odpovídá svislé čáře na herní obrazovce a známe vzdálenost ke zdi, na kterou se tímto směrem díváme. Nyní bychom měli na tyto stěny nalepit tapetu, ale než to uděláme, musíme si připravit naši herní obrazovku. Nejprve vytvoříme prvek div o požadovaných rozměrech.

<div id="screen"></div>

Pak vytvoříme proužky jako potomky tohoto prvku. Budou to také prvky div o šířce, na které jsme se dohodli výše a napozicované vedle sebe, aby vyplnily celou herní obrazovku. Je důležité, aby naše proužky měly nastavenu vlastnost overflow na hidden, aby byly schovány části textur mimo proužek. Jako potomka každého proužku přidáme obrázek s texturou. To vše provedeme ve funkci init(), kterou jsme vytvořili na začátku tohoto článku.

var screenStrips = []; // strips = proužky

function initScreen() {
  var screen = $("screen");
  for (var i=0;i<screenWidth;i+=stripWidth) {
    var strip = dc("div");
    strip.style.position = "absolute";
    strip.style.left = i + "px";
    strip.style.width = stripWidth+"px";
    strip.style.height = "0px";
    strip.style.overflow = "hidden";

    var img = new Image();
    img.src = "walls.png";
    img.style.position = "absolute";
    img.style.left = "0px";

    strip.appendChild(img);
    strip.img = img;    // nastav obrázek jako vlastnost proužku, ať jej máme později po ruce

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

Nastavení, která textura se má objevit v jakém proužku, dosáhneme pouhým posunutím texturového obrázku nahoru a dolů. Jeho posunem doleva a doprava zajistíme vykreslení správné části textury. Změnou výšky proužku a svislým posunutím obrázku pak natáhneme texturu, abychom vyvolali zdání vzdálenosti ke zdi. Horizont hráče zůstane ve středu obrazovky, proto zbývá posunout prvek s proužkem dolů na střed mínus půl výšky našeho proužku.

Prvky všech proužků a jejich obrázky máme uloženy v poli, proto k nim můžeme pomocí indexu snadno přistupovat.

Vraťme se ale k zobrazovací smyčce. V té si nyní musíme zapamatovat o stěně o něco víc, a to přesný bod, který proťal paprsek a typ zdi. Tyto informace rozhodnou, jak posuneme obrázek s texturami uvnitř našeho proužku, abychom zobrazili tu správnou část. Vyhodíme kód pro zobrazování paprsků na minimapě a nahradíme jej kódem pro práci s proužky.

Druhou mocninu vzdálenosti ke stěně máme již změřenu, stačí ji odmocnit a získáme skutečnou vzdálenost ke zdi. Ačkoliv se jedná o skutečnou vzdálenost k bodu, který byl proťat paprskem, musíme ji lehce zkorigovat, abychom předešli tomu, co se často nazývá efekt rybího oka (fish-eye effect). Ten nejlépe pochopíte pomocí obrázku 5.

Rendering bez korekce na fish-eye efekt

Obrázek 5: Zobrazení bez korekce rybího oka

Všimněte si, jak se stěna zdá být prohnutá. Naštěstí je oprava snadná – potřebujeme znát kolmou vzdálenost k naší stěně. Tu získáme vynásobením vzdálenosti ke stěně cosinem relativního úhlu paprsku. Více se dočtete v sekci Finding distance to walls Permadiho tutorialu.

  ...
  if (dist) {
    var strip = screenStrips[stripIdx];

    dist = Math.sqrt(dist);

    // použijeme kolmou vzdálenost pro korekci rybího oka
    // distorted_dist = correct_dist / cos(relative_angle_of_ray)
    dist = dist * Math.cos(player.rot - rayAngle);

Nyní spočítáme výšku stěny v projekci. Jelikož naše bloky stěn jsou krychle, šířka zdi bude v našem proužku stejná, ačkoliv musíme texturu natáhnout o faktor ekvivalentní šířce proužku, aby se správně zobrazil. Když v raycasting smyčce narazíme na zeď, uložíme si také typ zdi, což nám řekne, jak moc musíme posunout obrázek s texturou. Tohle číslo vynásobíme výškou stěny v projekci a je to. A konečně, jak jsme již popsali, jednoduše posuneme prvek proužku i s obrázkem na správné místo.

    // nyní spočítáme pozici, výšku a šířku proužku stěny
    // skutečná výška stěny je v herním světě rovna 1, vzdálenost od obrazovky od hráče je viewDist,
    // proto výška obrazovky je stejná s wall_height_real * viewDist / dist
    var height = Math.round(viewDist / dist);

    // šířka je stejná, ale musíme texturu natáhnout o faktor stripWidth, aby správně vyplnila proužek
    var width = height * stripWidth;

    // top je snadný, protože vše se centruje okolo osy x, proto jednoduše posuneme
    // dolů o polovinu výšky obrazovky a zpět nahoru o polovinu výšky stěny
    var top = Math.round((screenHeight - height) / 2);

    strip.style.height = height+"px";
    strip.style.top = top+"px";

    strip.img.style.height = Math.floor(height * numTextures) + "px";
    strip.img.style.width = Math.floor(width*2) +"px";
    strip.img.style.top = -Math.floor(height * (wallType-1)) + "px";

    var texX = Math.round(textureX*width);

    if (texX > width - stripWidth)   // pozor, abychom neposunuli texturu příliš a nevznikly mezery.
      texX = width - stripWidth;

    strip.img.style.left = -texX + "px";

  }

A to je všechno, na obrázku 6 najdete výsledek. Zbývá ještě dodělat řada věcí, než bychom to mohli nazývat hrou, ale nejtěžší úkol je hotov a před námi se otevřel 3D svět. Zbývá nám poslední věc, a tou je přidat strop a podlahu, což je triviální úkol, pokud budou oba jednobarevné. Stačí přidat dva prvky div, každému přidělit polovinu obrazovky, umístit je pod proužky pomocí vlastnosti z-index a nastavit jim patřičnou barvu.

Pseudo 3D raycasting s texturami zdí

Obrázek 6: Pseudo-3D raycasting s texturami na stěnách (živá ukázka).

Nápady na zlepšení

  • Oddělit zobrazení od herní logiky (pohybu apod.). Pohyb by měl být nezávislý na rychlosti (framerate) vykreslování.
  • Optimalizace – na několika místech můžeme optimalizovat a získat menší zlepšení výkonu, např. nastavovat pouze CSS vlastnosti proužku, které chceme opravdu měnit apod.
  • Statické sprity – přidat možnost zobrazování statických objektů (sprites), např. lamp, stolů apod., které učiní náš 3D svět mnohem zajímavějším.
  • Nepřátelé/NPC – jakmile bude engine schopen zobrazit statické sprity a pohybovat jimi, můžeme vytvořit jednoduchou umělou inteligenci, která bude náš svět obývat.
  • Lepší obsluha pohybu a detekce kolizí – pohyby hráče jsou hrubé, tj. hráč se zastaví okamžitě, jak uvolní klávesu. Zapojení malého zrychlení do pohybu a rotace vytvoří mnohem hladší zážitek. Současná detekce kolizí za moc nestojí. Hráč se jednoduše zastaví. Mnohem lepší by bylo, kdyby se posouval podél stěny.
  • Zvuky – pomocí Flashe nebo JavaScriptu, např. s nástrojem SoundManager2 snadno přidáte zvukové efekty řadě událostí.

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

Použili jste někdy canvas?

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!

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 43

Přehled komentářů

Sten RE: Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem
helb RE: Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem
..! Otazka
Bruce Re: Otazka
..! Re: Otazka
y Re: Otazka
tm Re: Otazka
andrejk Re: Otazka
Sten Re: Otazka
DevelX Re: Otazka
Keltek Re: Otazka
Sten Re: Otazka
Clock Jak je to s rychlosti?
Mti. Re: Jak je to s rychlosti?
xx ??
pexxi Re: ??
xx Re: ??
Anonym Re: ??
xx Re: ??
Anonym Re: ??
xx Re: ??
dc Re: ??
Petr Re: ??
andrejk Re: ??
xx Re: ??
Anonym Re: ??
Jan Jelínek Re: ??
raist Proc JS, proc RAD, proc ne assembler?
raist Re: Proc JS, proc RAD, proc ne assembler?
Martin Hassman Re: Proc JS, proc RAD, proc ne assembler?
xx Re: ??
xx Re: ??
David Majda Re: ??
xx Re: ??
David Majda Re: ??
Anonym Re: ??
SoudruH Poloprůhledné PNG
UnavenSluncem Re: Poloprůhledné PNG
SoudruH Re: Poloprůhledné PNG
w3m Re: Poloprůhledné PNG
DevelX RE: Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem
honza111 Nelze už stáhnout ty ukázkové zdrojáčky a já to neumím správně napsat
Martin Hassman Re: Nelze už stáhnout ty ukázkové zdrojáčky a já to neumím správně napsat
Zdroj: https://www.zdrojak.cz/?p=2895