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

Zdroják » JavaScript » Jak vytvořit pseudo 3D hry v HTML5 canvasu s raycastingem

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

Články JavaScript, Webdesign

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.

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?

Komentáře

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

Jú, funguje to i v Konqueroru. No to je úlet ;)

helb

Jo, přesně tohle jsem se chystal napsat. :D

..!

Muzu se na neco zeptat? K cemu je to dobre?

Bruce

Bude to standard a nativne podporovane v prehliadacoch, na rozdiel treba od Flashu. Ja sa na toto velmi tesim, pretoze s Flashom mam len same problemy na 64 bit Linuxe a verim, ze to raz bude nahrada za Flash. Okrem ineho, Flash je na stranke ako black-box objekt, kdezto toto je mozne pekne previazat s HTML prvkami a JS, co dava nesporne vyhody. Flash stranka s textami nieje indexovatelna robotmi, kdezto tu je mozne docielit to same a byt pekne viditelny

..!

Nemyslel jsem Canvas. Ptam se k cemu je dobre implementovat pseudo 3D engine, ktery je brutalne pomaly v necem jako je html (at uz s canvasem nebo JS, to je jedno).

y

Me to neprijde prakticky vyuzitelne tak jak to je, ale z toho kodu jsem se napriklad ja osobne hodne poucil, zjistil spoustu novych veci, ktere jsem pred tim delal zbytecne sloziteji a take zjistil par zajimavosti kolem matematiky. Takze zbytecne to urcite neni, otazkou je na co to kdo pouzije.

tm

Ja si nemyslím, že to má mať praktické využitie, skôr ide o akési "technology demo", teda ukážku toho, čo všetko sa s canvas-om dá spraviť.

andrejk

pekna ukazka ako sa to robi. optimalizacie to cele len skomplikuju a na optimalizovanom kode sa tazko uci.

mne sa clanok velmi pacil, je super mat moznost pozriet si takyto 3d engine v niecom tak jednoduchom a bez problemov modifikovatelnom ako je javascript!

html sa tiez clovek uci na hello world napisanom vo vimku a nie na ajaxovych strankach generovanych sialenymi regexpami v perle.

Sten

To je evoluce. Počítače sice máme stále rychlejší, ale všechny běžné úkoly trvají stejně dlouho a i ten Word se spouští pořád stejně rychle, jako před patnácti lety :)

DevelX

Síce tiež nemám moc rád Flash, ale dovoľte kolega, aby som Vás trochu poopravil :-).
Flash dokáže volať v prostredí dokumentu JS funkcie, čím je možné prostredníctvom JS teoreticky manimulovať so stránkou… strašne neohrabané, viem… ale je to možné. Samo o sebe to ale hovorí v prospech JS :).
Flash je indexovateľný robotmi za istých okolností… ale v takmer 100% prípadov na potrebné veci vývojári nedbajú, takže v konečnom dôsledku v podstate nie je.
A mimochodom – neviem čo presne myslíte tým "docieliť to isté a byť pekne viditeľný". Ak by som vytvoril stránku tvorenú len pomocou JS, ktorý by generoval všetok obsah, tak by roboti toho moc nezaindexovali :). JS je dobré používať, ale človek musí vedieť ako ho používať ;-).

Keltek

Flash nikdy nenahradí nic tak divného jako Canvas + HTML5…

Sten

Nahrazovat se bude Flash nebo to divné? :)

Clock

K dokonalosti by to jeste chtelo tvrzeni ze 3D hra implementovana v Javascriptu je rychlejsi nez rucne napsana v assembleru, protoze dnes mame prece JIT kompilatory.

Mti.

ano, tomu wolfovi sveho casu stacila 286ka :)

xx

Proc delat neco efektivne, kdyz to jde i neefektivne. Nechapu lidi, co tohle delaji. K cemu je to dobre? Misto aby se prosazovalo programovani efektivnich aplikaci, tak se takto plytva vypocetnimi zdroji.

pexxi

Ono ide o to, ze efektivita je casto nepriamo umerna zrozumitelnosti kodu. A pri takychto prikladoch, samozrejme, nejde o to urobit to co najefektivnejsie, ale co najprehladnejsie a zrozumitelne. A podla mna je clanok fajn…

Takisto by ma potesilo, keby sa Canvas a HTML5 presadili na ukor Flash-u. Avsak je pravda, ze keby sme mali nieco taketo uz skor, Flash by davno nebol tak rozsireny (ak vobec), ako je teraz.

xx

Mam na mysli neefektivitu jakychkoliv aplikaci bezicich v prohlizeci napsanych v javascriptu.

Anonymní

protoze v tom JS to bezi vsude? – kdezto kdyz udelam tlustyho klienta tak je problem ze si clovek musi neco doinstalovavat, a na spouste systemu to nejede? – takhle mam aplikaci pristupnou i z kdejake i-kavarny a o tom to je.

xx

Prave ze to jste uplne vedle — vemte si, jak to bezi na starsich prohlizecich, jak pomalu to bezi na starsich pocitacich s novymi prohlizeci a vemte si, jak se plytva vykonem pocitace, a jak se plytva i vasim casem, kdyz na tu aplikaci musite cekat. Mnohem efektivnejsi by bylo stahnout si binarku a spoustet primo tu.

Vy rikate, ze o tom to je, ale soucasne prohlizece maji vazne problemy s vykonem, tudiz je otazkou, zda-li je dobry napad implementovat do nich aplikace.

Anonymní

v podstate reakcia na vsetky prispevky vyssie.K comu je to dobre ? To sa potom mozeme rovno pytat k comu je dobra java & .net (ked to pritiahnem za vlasy tak je to interpretovane ako VB, cize uz to tu bolo).Argument ze java bezi vsade tiez moc neobstoji pretoze s kompatbilitou je to tiez biedne.Tak isto potom mozeme polozit otazku k comu je dobry jaxer,adobe air,google gears a podobne hovadiny.Inak povedane naco sa snazime robit desktopove aplikacie cez prehliadac ked aj tak su z toho len same problemy (bezpecnostne,vykonnostne) ked tie desktopove aplikacie tu uz mame ? Proste PR,marketing a zopar minoritnych vyhod (moznost pristupovat k svojim datam vsade neobstoji).
Co sa tyka canavsu+html5 a ostatneho dalsieho bordelu (js,css atd.) tak to neni ziadna vyhra.Flash sice neni najlepsie riesenie ale urice lepsie ako tie aktualne zlepence vsetkeho mozneho.Na vygenerovanie stranky potrebujem server side scripting,js,css,html+dom,nejake obrazky a este nemam zarucene ze to v kazdom prehliadaci bude korektne fungovat. To uz je fakt lepsi uzavrety flash.

xx

Osobne bych rekl, ze Java i .NET urcyhluji vyvoj aplikaci a zvysuji jejich bezpecnost, zatimco javascript nedela ani jedno.

S kompatibilitou je na tom asi stale nejlepe C.

S tim Flashem s vami naprosto souhlasim.

dc

To je otazne.Java aj .NET akurat presunul problematiku bezpecnosti z programatora na deployment a prostredie.Inak povedane bezpecne nastaveny system s c/c++ aplikaciou (ktora je maximalne safe ako moze byt) je rovnaka ako nejaky VM.Ostatne v Unixoch napriklad chroot existuje dlho.Problem s verziami a kniznicami je aj pod Javou/NETom len sa to ako som napisal presuva inde.Co sa tyka urychlenia vyvoju, to je zasa otazne.Klikacie generatory kodu/gui sa daju spravit na kazdy jazyk.Davno pred nejakym Swingom alebo C# gui editorom v MSVC tu bolo napriklad Delphi, ktore co sa tyka RADu aj dnes nema moc konkurencie (bohuzial ale iba co sa radu tyka,ostatne taky VB tiez dost veci kopiroval,alebo naopak).Obcas mam pocit ze sa objavuje objavene a este mame MS/Adobe/Google a ostatnym za to asi tileskat.
Mimochodom z aktualneho web developmentu a trendov je mi dost nevolno.Jedina vec z ktorej v poslednej dobe mam aspon trochu pozitivny pocit je Flex.

Petr

Problém s verzemi a knihovnami v .NETu prakticky není – vše je jasně definováno, knihovny se podepisují. Bezpečnost je násobně vyšší oproti nativním programům (např. buffer overflow útokům atd.). Jestli si myslíte, že efektivita .netu je v naklikávání, tak jste v tom asi moc nenapsal.
Javascript a další technologie uměle naroubované na html – to je slepá ulička, kdo to pochopí, ušetří neuvřitelné množství peněz a času.

andrejk

zacal som to citat a pozerat v praci na winxp/chrome, docital a dopozeral doma na fedora/firefox. o tom to je.

nikto vas nenuti robit v javascripte simulaciu vybuchu atomovky, ale na ucel prezentacie algoritmu je javascript celkom fajn.

xx

Jeste bych doplnil, ze ne vsechny prohlizece podporuji javascript a ne vsichni uzivatele ho maji zapnuty.

Anonymní

Mno, moc tomu nerozumím, ale například gmail vykopnul všechny moje e-mail klienty a funguje prozatim všude, kde jsem se k němu připojil. Dtto picasa nebo google documents. Totéž nemohu říci o flash, který musím vždycky pracně instalovat a když pošlu někomu odkaz, odpoví mi, že "mu to nejede".

Jan Jelínek

U JS bych trochu brzdil. V tuhle chvíli sice neláme rychlostní rekordy, ale počkejme si nějaký pátek a možná se ještě všichni budeme divit. Mnoho společností investuje mnoho peněz do vývoje JS. Proč? protože mu věří. Takže s JS tutoriáli – jen víc a houšť.

raist

Zakladnim duvodem je zpristupnit programovani/ovladani prostredi vice uzivatelum
(Je otazkou jak moc je to zvrhla myslenka – osobne myslim, ze neni spatna – jelikoz dnes a denne vidime na internetu priklady velmi zajimavych reseni, myslenek, projektu, ktere by nevznikly byt tu jen assembler).

Zprehlednovanim kodu, snadnejsimi vyvojovymi nastroji, a upoutanim uzivatele tech.demem jako je toto, se laka uzivatel k tomu aby se do programovani pustil. To je neci marketing. Ale zaroven je to prinosne(viz text v zavorkach vyse).

Proto panove nepiseme v assebleru a stranky si neprohlizime v textovem modu na zelenocernem monitoru.

raist

Tim chci take rict, jen houst s takovymi tutorialy! Root vede!

xx

Myslim, ze divit se nebudeme, protoze pokud se JS radikalne nezmeni (nezavede silne typovani), bude stale umorne pomaly. Uvedomte si prosim fakt, ze kdyz mnoho spolecnosti do neceho investuje a necemu veri, tak to neznamena, ze neexistuje neco lepsiho.

xx

chtel jsem napsat silne staticke typovani

David Majda

Se statickým typováním nemáte úplně pravdu. Zrovna můj dnešní článek tady na Zdrojáku ukazuje, jak se s absencí typových deklarací poměrně úspěšně vyrovnává interpret SquirrelFish (WebKit/Safari) – jednoduše si potřebná data posbírá v runtime. Podobně to dělá V8 (Google Chrome). SpiderMonkey/TraceMoneky (Firefox) zas používá techniku trace trees, která nedostatek informací o typch také v určitých případech dovede obejít.

Pokud se interpret napíše opravdu dobře (což ale stojí poměrně hodně úsilí, proto se to moc nedělá), dá se s ním dosáhnout rychlosti blízké rychlosti céčka. Viz zmínka Avi Bryanta o Strongtalku, dialektu SmallTalku:

Strongtalk is that it does allow optional static type annotations, but the optimizer completely ignores them: your code runs exactly as fast if you duck type everything as if you statically type everything. That’s not because the implementors were lazy – their goal was to produce the fastest system possible, it’s just that their dynamic implementation was already fast enough that the static information didn’t help.

Podobně také viz jazyk Self:

Despite this complete devotion to objects, Self code runs at up to half the speed of
optimized C, the high performance being achieved through dynamic profile-driven optimization.

JavaScript na tom sice nyní tak dobře, aby běžel polovinou rychlosti céčkového kódu, není, ale není nepravděpodobné, že brzy někde okolo těchto hodnot bude.

xx

Optimalizace ve vasem clanku uvedene v castech "Pristup k vlastnostem objektu" nebo "Typova inference" se prakticky jen snazi zmirnit fakt, ze chybi typove informace — resp. kdyby tam typy byly tak k clenskym datum objektu muzete pristupovat v case konstantnim a bez nejake rezie ci optimalizace.

Jazyk Self neznam, nicmene srovnavate, ze rychlost jeho interpretru se blizi rychlosti kompilovaneho C, coz je sice chvalihodne, ale je otazka, zda je vystup kompilatoru C skutecne ultimatni cil, zda nejde jit jeste dale.

Problem jazyka C je zejm. prima manipulace s pameti, kde kompilator nemuze nic moc delat. Napriklad jazyky jako Clean, Haskell maji potencialne moznost toto vyresit mnohem lepe — navic uz ted provadeji optimalizace celych programu.

Vemte si napriklad kompilator omezene varianty jazyka Scheme jmenem Stalin, ten je schopen vyplodit rychlejsi kod (preklada do C), nez je rucne psany kod v C. Porad ale plati, ze kompilatory dynamickych jazyku se musi spolehat predevsim na urcitou intuici (nebo na statistiky z predeslych behu programu) — tudiz vysledek neni jisty. Zato kompilatory staticky typovanych jazyku nemusi pouzivat intuici k detekci typu, protoze je maji predem (jasne) dane.

Je treba uznat, ze kompilator staticky typovaneho jazyka je proste ve vyhode oproti kompilatoru dynamicky typovaneho jazyka, nebot ma informace navic.

David Majda

Je treba uznat, ze kompilator staticky typovaneho jazyka je proste ve vyhode oproti kompilatoru dynamicky typovaneho jazyka, nebot ma informace navic.

S tím souhlasím. Jen sem chtěl vyvrátit vaše tvrzení z komentáře, že JavaScript bude bez statického typování „úmorně pomalý“. Ztráta informace absencí typových deklarací vadí, ale nevadí tolik, jak se může na první pohled zdát.

Máte samozřejmě pravdu, že céčko není ultimátní cíl; zmiňované problémy s ukazateli se vysvětlují na každé přednášce z kompilátorů. Bral jsem to zde spíš jako referenci (vágní, ale často používanou).

Anonymní

Neumíš napsat normální titulek?

SoudruH

Článek se mi zalíbil a jen tak dál, rozhodl jsem se do tohoto enginu upravit galeri.. téměř všechm se zalíbilo že by se mohli procházet po místnosti s fotkama… to se mi vše podařilo pomocí php generovat z Db, ale hlavní problém je s dekoracemi. Chtěl jsem přidat stůl vytvořený pomocí poloprůhledného PNG. Zobrazí se, ale místo průhledné barvy se zobrazí růžová… mělo by to být někde v nastavení, ale nikde se mi to nepodařilo najít. Poradí nekdo?

UnavenSluncem

Mozna jsem uplne mimo, ale do zakladu dost IE bojuje s pngckama;)

SoudruH

Pouřívám linux a ies4linux mi nejde. Poloprůhledné PNG se zobrazí ale pod ním je růžová barva ( #FF00FF ). Problém je, že je někde v css (nastavuje to nejspíš js) tato barva na pozadí, mě se to ovšem nepodařilo nikde najít.

w3m
DevelX

No áno, pekné, ale – toto využíva DOM, nie canvas :). Takže by to išlo aj v IE… moment, skúsim… ide ;). Canvas verzia by samozrejme išla rýchlejšie :).


Špeciálne pre vrtákov a rypákov: áno, minimapu vykresluje canvasom…

honza111

mám jednu potíž dříve když jsem měl ty zdrojáčky věděl jsem kam asi patří který ze zdrojáků ale teď mi to hlásí něco že opera fórum bylo zrušené. Když to co je zde opíši do souboru s příponou html objeví se mi příkazy jako text na stránce. to znamená že to má mít nějakou hlavičku ale nevím jak na to.

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.