Bejelentkezés/Regisztráció: Live Facebook

Segédlet HTML 5 minijátékok készítéséhez

Bevezető

A HTML5 elterjedése számos új lehetőséget teremtett a webes minijátékok terén. Olyan dolgokat hozhatunk létre segítségével, melyekhez eddig valamilyen más, kevésbé platformfüggetlen, vagy bonyolultabb technológiát kellett alkalmaznunk (pl. Flash). A játékfejlesztés kapcsán a legfontosabb újdonságok a canvas (rajzvászon), illetve az audio tag, melyek segítségével rajzolhatunk, és hangokat tudunk lejátszani weboldalunkon. Ez a segédlet ezeket az újdonságokat mutatja be egy játék elkészítésén keresztül. A leírás elsősorban a HTML5 újdonságaira épül, de szó esik számos olyan megoldásról vagy ötletről is, amik hasznosak egy minijáték elkészítése során. A fejlesztés JavaScript nyelven folyik.

A példaprogram

A példaprogram egy olyan egyszerű játék lesz, ahol egy adott méretű, mezőkből álló pályán csőelemeket helyezhetünk el, hogy egy csapot összekössünk egy lefolyóval. A játék célja, hogy a visszaszámlálás lejártakor elinduló láva a letett csőelemeken keresztül eljusson a lefolyóig.

A tervezés

Első lépésként fontos eldönteni a játékkal kapcsolatos elvárásokat. Ez a mi esetünkben egy rajz, amin szerepelnek a fontos kezelőelemek és a pálya. Ebből már az is látszik valamennyire, hogy hogyan fog működni a játék.

A felhasználói felület terve

A képen jól látható a kezelőelemek és a játéktér elrendezése. A játéktér a képernyő bal felső részén helyezkedik el. Jobb oldalon szerepelnek a kezelőszervek, alul pedig a visszaszámláló. A későbbiekben eltérhetünk a tervtől (és el is fogunk), de érdemes egy olyan rajzot készíteni, ami lehetőleg jól átgondolt alapokon nyugszik. A program feladata az lesz, hogy a felhasználó az egérrel el tudja helyezni az elemeket a pályán, majd az idő lejártakor elinduljon a folyadék.

A program struktúrája

A fejlesztés tényleges megkezdése a program struktúrájának kialakításával kezdődik. A JavaScript adottságait is figyelembe véve érdemes kialakítani, hogy mit hogyan tárolunk el, és mik a fontosabb metódusok.

A program magvát egy rajzoló függvény adja, ami bizonyos időközönként meghívódik, és kirajzolja az aktuális állapotot. Ehhez szükség van olyan változókra, illetve tömbökre, vagy egyéb objektumokra, amik folyamatosan tárolják az állapotot leíró adatokat. A játék során fellépő eseményeket kezelő metódusokkal ezeket a változókat fogjuk átírni, melynek hatására a következő kirajzolásnál már a képernyőn is látszik a változás.

A rajzoló függvény neve draw lesz, és a JavaScript setInterval függvényét használjuk az időközönkénti (itt 50 ms) meghívásra:

var timer = window.setInterval(draw,50);

A pálya 10x7 mezőből áll, a mezőkön különféle elemek helyezkedhetnek el. Ezt célszerű számon tartani egy kétdimenziós tömbben, melyet a játékmenetnek megfelelően módosíthatunk. A különféle elemeknek érdemes valamilyen kódot adni, ezekkel a kódokkal töltjük fel a tömböt, mint egy mátrixot. Ehhez a kódoláshoz számokat használunk: a csőelemek 1-6-ig kapnak egy számot, az üres mező kódja 0, az egyéb segédmezőké (pl. csap, akadály vagy lefolyó) pedig negatív. Egy egységes kódolás használatával később könnyen létrehozhatunk előre definiált kezdőállapotú pályákat is.

Az idő múlását is érdemes számon tartani, hiszen arra nincsen semmi garancia, hogy az általunk pl. 50 ms gyakorisággal meghívni szánt draw függvény valóban ilyen sűrűn fut le. Érdemes tehát a kirajzoláskor az eltelt idő alapján kiszámolni az aktuális állapotot és nem az időzítő pontosságára hagyatkozni. Ehhez egy prevtime és egy time nevű változót használunk, melyekből meg tudjuk határozni mindig az előző lefutás óta eltelt időt. Fontos még, hogy a játék állapotait eltároljuk: hányadik pályán járunk, hány pontot szereztünk, hányat robbantottunk, stb.

Mivel a játékot egér és billentyűzet segítségével lehet majd irányítani, meg kell írnunk az onclick és onkeydown eseményeket kezelő függvényeket is. Ezeket a szokásos módon megírhatjuk, az egyetlen fontos dolog, amire érdemes figyelni, hogy az egér pozíciója a canvason belül értelmezett pozíció legyen (tehát a rajzvászon bal felső sarka legyen a (0,0) pont). Ezt érdemes már az eseménykezelő függvényben átszámolni, mert a rajzolás során mindig a canvas bal felső sarkához viszonyítunk majd.

Rajzolás canvasra

A legérdekesebb rész maga a rajzolás. A canvasra több féle alakzatot (pl. téglalap, körcikk, stb.) rajzolhatunk, szöveget írhatunk, illetve képeket is betölthetünk külső fájlból. Első lépésként az alábbi két sor segítségével el kell kérnünk a canvas elemünk kontextusát, ami esetünkben két dimenziós:

var canvas = document.getElementById('game');
var context = canvas.getContext('2d');

A példaprogramban előre elkészített képeket fogunk kirajzolni, néhány szöveggel kiegészítve. Képek rajzolásához először létre kell hozni egy Image objektumot, majd beállítani a kép forrását, azaz a fájl elérési útját:

var bg = new Image();
bg.src="bg.png";

Ezután a drawImage függvénnyel tudjuk kirajzolni a képet a canvasra:

context.drawImage(bg,0,0);

A drawImage függvénynek több változata is van, itt a legegyszerűbbet használtuk, ahol csak a kirajzolni kívánt Image objektumot és a kívánt pozíciót kell megadni. Szöveg a következőképpen írható ki a canvasra:

context.fillStyle = "white";
context.font = "bold 10pt Verdana";
context.fillText('szoveg', 715, 42);

A fillStyle tulajdonság a szöveg színe (illetve ha alakzatot rajzolunk ki, akkor annak a színe), a font pedig a betű stílusát határozza meg. Használhatunk külső fájlból betöltött betűtípust is, ehhez a CSS @font-face tulajdonságát ajánlott használni. A fillText függvénynek meg kell adni a kiírni kívánt szöveget, és a pozíciót. Ha át szeretnénk transzformálni az alap koordinátarendszerünket, pl. hogy a (0,0) pont máshol legyen, vagy elforgatva szeretnénk kirajzolni valamit, akkor a következő lehetőségeink vannak:

context.translate(710,260);
context.rotate(-Math.PI);

A fenti két függvény egymásutánja azt eredményezi, hogy az ezután kirajzolt elemek esetén a (0,0) pont az eddigi (710,260) pontba tolódik, és 180°-kal elforgatva rajzolódik ki minden (a forgatás a (0,0) pont körül történik, ami az eltolásnak köszönhetően a canvas (710,260) pontja). Ahhoz, hogy ezt visszaállítsuk az eredeti állapotra, nem szükséges ugyanezeket a transzformációkat visszafelé elvégezni, erre a célra használhatjuk a context.save() és context.restore() függvényeket. A transzformációk előtt hívott save() függvény elmenti az aktuális állapotot és a restore()-ral visszaállíthatjuk azt.

A kirajzolás előtt lehetőségünk van a kirajzolandó elem átlátszóságát is átállítani, ezzel nagyon látványossá tehetjük programunkat:

context.globalAlpha = 0.5;

Ez a sor az átlátszóságot 50%-osra állítja, és mindaddig annyi lesz, amíg meg nem változtatjuk ismét. Több elem rajzolása esetén akár mindegyik előtt változtathatjuk a globalAlpha értékét, a korábban kirajzolt elemek átlátszóságát nem befolyásolja.

A példaprogram draw() függvénye

A példaprogram rajzoló függvénye az előző rajzolás óta eltelt idő meghatározásával kezdődik:

now = new Date().getTime();
deltat = now - prevtime;
prevtime = now;

Ez ahhoz szükséges, hogy az automatikusan változó részek, például az idő múlását jelző csík, vagy a vízfolyás folytonos legyen, és mindig az aktuális időpontnak megfelelő állapot rajzolódjon ki.

Először a hátteret rajzoljuk ki a drawImage függvénnyel. Ha ez megvan, akkor a pálya mezőinek állapotát tároló kétdimenziós tömb feldolgozása következik. Ezt egy swich-case szerkezettel érdemes megoldani. A példaprogramban start és cél elem, akadály, kanyarodó és egyenes csőelem szerepelhet. A csőelemekből azért elegendő egy egyenes és egy kanyarodó, mert a rotate transzformációval ezekből könnyen előállítható a többi is.

A kanyarodó csőelem Az egyenes csőelem

A pálya kirajzolása után a szövegek és a kezelőfelületek ikonjainak kirajzolása következik. Ebben nincs semmi trükk, egyszerűen megadjuk a pozíciót és a kiírni vagy kirajzolni kívánt elemet a tervünk alapján. Az idő múlását jelző csík elkészítéséhez a drawImage függvény egy bonyolultabb változatát érdemes használni. A csík két képből áll: egy teljesen üres és egy teljesen teli csőből. Az egyiket teljesen kirajzoljuk, a másikat pedig az első fölé, de csak az eltelt idővel arányos részét.

A csík

A bonyolultabb drawImage függvény használata:

context.drawImage(tube_filled, 0, 0, width, 25, 260, 458, width, 25);

Először itt is az Image objektumot kell megadni, majd a következő 4 paraméterrel megválaszthatjuk, hogy a kép melyik részét szeretnénk kirajzolni. Az első két paraméterrel a bal felső sarkot, a második kettővel a kivágandó képrész szélességét és magasságát jelölhetjük ki. Az utolsó négy paraméterrel ugyanígy a bal felső sarkot, a szélességet és a magasságot adhatjuk meg, de ezek már a canvas koordinátarendszerében értelmezettek. A fenti kód tehát a tube_filled kép bal felső sarkától számítva egy width széles és 25 pixel magas téglalapot vág ki, melyet a rajzvászon (260,458) pontjától ugyanilyen méretben (width x 25) kirajzol. A width változó az eltelt idő függvényében nő, így egyre szélesebb részét rajzoljuk ki a teli csőnek. Ezzel olyan illúziót érhetünk el, ha elég sűrűn hívódik a draw függvényünk, mintha folyamatosan folyna a láva a csőben.

Ahhoz, hogy látványosabb legyen a programunk, érdemes az egérrel kattintható részeket kiemelni. A példaprogramban a csere gombot emeljük ki úgy, hogy ha a játékos ráviszi az egeret, akkor benyomódik, mint egy igazi gomb. Ehhez el kell készítenünk a gomb alap és benyomott állapotát külön képfájlban. Az onmousemove eseménykezelőben írunk egy feltételt: ha a gombon van az egér, akkor egy erre a célra létrehozott változót 1-re állítunk. Ha nincs a gombon az egér, akkor pedig 0-ra:

if( ( (x-710)*(x-710)+(y-345)*(y-345) ) <= 900 )
{
  onmouseover_change = 1;
}
else
{
  onmouseover_change = 0;
}

A feltételben a gomb által meghatározott körlap egyenlőtlensége van. A körlap sugara 30 pixel, középpontjának pozíciója pedig (710,345). Így elérhető az, hogy a játékosnak ne tűnjön fel, hogy a gombhoz használt képek valójában téglalap alakúak, melyeken a kör alakú gomb körül átlátszó a háttér.

A gomb működését ehhez teljesen hasonlóan az onclick eseménykezelő függvényben kell megírni.

A jobb játszhatóság kedvéért az egér alá halványan ki szeretnénk rajzolni a soron következő csőelemet. Ehhez létre kell hozni két változót, melyekben az egér aktuális pozícióját tároljuk. Ezeket a változókat az onmousemove függvényben mindig aktualizáljuk, így a draw függvényünk mindig tudni fogja az egér pozícióját.

context.globalAlpha = 0.5;
context.save();
context.translate( mouse_x, mouse_y );
switch(nextitem)
{
  case '1': 
    context.rotate(-Math.PI); 
    context.drawImage(element1,-30,-30); 
    break;
    // ...
}
context.restore();

A fenti kódrészletben először 50%-os átlátszóságot állítunk be, majd az egér aktuális pozíciójába ugrunk. Itt elforgatjuk a kirajzolni kívánt csőelemet, ha szükséges, majd az egérpozícióhoz képesti (-30,-30) pontba kirajzoljuk. Mindenképpen fontos, hogy az egér pozíciója körül forgassunk, és figyelembe vegyük, hogy a drawImage függvény a bal felső sarok koordinátáit várja.

A láva elindul (kiegészítés)

Ez a rész nem tartozik szorosan a HTML5 technológiához, viszont a példaprogram egyik legösszetettebb része: a láva folyásának megírása. Lehet, hogy túlságosan bonyolultra sikerült, de univerzális és jól használható megoldás. Egyetlen HTML5 specifikus elemet használ, a fillRect függvényt, ezért csak érdekességként esik szó a megoldás menetéről.

A láva folyásának megvalósításához egy külön függvényt készítünk flow néven. A függvényt a draw-ból hívjuk akkor, ha már lejárt az idő, de nincs vége a játéknak, tehát a lávának folynia kell.

Kell egy változó, ami a lávafolyam aktuális hosszát tárolja. Ezt folyamatosan növeljük az eltelt idővel arányosan.

flowing += 4 * deltat / 100;
remaining = flowing;

(A remaining változó azért kell, mert ezt fogjuk mindig csökkenteni, ha kirajzoltunk egy részt.)

Minden csőelemnél máshogyan folyik a láva, ezért érdemes két részre bontani a folyás kezelését: első körben a kiindulási ponttól az elem feléig folyjon (kanyarodó csőelem esetén a kanyarig), majd a felétől a kimenetig. Így elég 4 különböző esetet venni: a középső pontot fel, le, balra vagy jobbra kell összekötni az elem szélével. Fontos, hogy csak abban az elemben folyjon láva, ahova tényleg be is tud jutni, tehát annak meghatározásához, hogy „helyes-e a kapcsolás” az adott csőelemnél, szükséges az előző elem vizsgálata is. Az irányokat a következőképpen kódoljuk: 1: felfelé, 2: jobbra, 3: lefelé, 4: balra. Ezek közül adjuk majd valamelyik értéket a type változónak az aktuális csőelemnél, ami azt fogja megmutatni, hogy merre folyik ki a láva az elemből.

A flow függvény egy nagy while ciklusra épül, amelynek minden köre után levonjuk a kirajzolt láva hosszát az idő függvényében megállapított maximális lávahosszból (remaining változó) és addig fut a ciklus, amíg el nem fogyasztjuk teljesen ezt a változót.

Mindig számon tartjuk, hogy éppen melyik koordinátájú elemnél járunk (x és y). A ciklus elején az előző elem típusának megfelelően (azaz hogy onnan merre folyt a láva) beállítjuk, hogy melyik koordinátába jutottunk.

switch(prevtype)
{
  case '1': y--; break;
  case '2': x++; break;
  case '3': y++; break;
  case '4': x--; break;
}

Ha ez a koordináta a pályán kívül van, vagy akadály van rajta, akkor azonnal ki is írhatjuk, hogy vesztett a játékos. Ha nem, akkor az ezen a koordinátán lévő csőelemet össze kell vetnünk az előző csőelemmel. Ez egy bonyolult switch-case szerkezet, lényege, hogy a jelenlegi elem típusát meghatározzuk. Ha nem kompatibilis a két elem, akkor -1-et adunk típusnak, ha kompatibilis, akkor a 4 irány közül azt, amerre kifelé folyik a láva az aktuális csőelemből. Innentől már csak annyi a feladatunk, hogy kirajzoljuk a lávát az elemre. Miután beállítottuk a színét a fillStyle attribútumban, egyszerű téglalapok formájában kirajzoljuk a megfelelő méretű csíkokat. Mivel csak akkor rajzolunk, ha az elem típusa nem -1, a rajzolás részhez csak úgy tudunk eljutni, hogy kompatibilis elemeket rakunk össze. Ekkor megengedhetjük, hogy először az előző elem típusa alapján a láva első felét rajzoljuk ki, majd az aktuális elem típusa alapján a második felét. Pl. ha az előző elemből függőlegesen lefelé jön ki a láva, akkor az elemünk tetejétől kell a közepéig húzni a csíkot. Ha az elemünk pedig jobbra fordul, akkor innen a jobb széléig kell húzni a csík második felét. Mindkét részlet végén le kell vonnunk a kirajzolt hosszt az időarányos változónkból és csak akkor folytathatjuk a rajzolást, ha az még nem negatív.

Példa arra az esetre, ha az előző elemből függőlegesen lefelé jön ki a láva:

context.fillRect( 48+x*60, 20+y*60, 4, remaining < 30 ? remaining : 30 ); 
break;

Egy téglalapot rajzolunk, ami az aktuális elem tetejétől középről indul. Ehhez a pontos koordinátákat ki kell számolni, esetünkben egy elem 60x60 pixel méretű, ezért a 60-nal való szorzás, és a pálya a canvas (20,20) koordinátájában kezdődik. 4 pixel széles a lávacsík. Ezek alapján a fent látható eredményt kapjuk. A remaining < 30 ? remaining : 30 rész feladata, hogy ha még nem telt el annyi idő, hogy az adott csíkrészletet végig kirajzoljuk, akkor csak egy részét rajzoljuk ki. A többi eset is teljesen hasonlóan néz ki, az elv ugyanez.

Ezt a műveletsort végezzük el tehát minden elemre, amíg azok helyesen vannak illesztve, illetve amíg tart a remaining változó. Amint hiba lép fel, kiírjuk, hogy vesztett a játékos. Ha az aktuális koordinátákat tároló változók azt mutatják, hogy a lefolyónál járunk, akkor pedig kiírjuk a játékosnak, hogy sikeresen teljesítette a pályát.

A hangok

A játékoknál megszokott, hogy van valamiféle háttérzene, illetve különböző hangeffektek. A HTML5 ezen a téren is sokat segít, hiszen az audio tag segítségével nagyon egyszerűen tudunk hangokat lejátszani. Audio taget létrehozhatunk javascriptből is, ezt alkalmazzuk a példaprogram esetében is. A módszer:

var main_theme = document.createElement('audio');
main_theme.src = 'main_theme.mp3';
main_theme.volume = 0.2;
main_theme.loop = true;
main_theme.play();

Először tehát létrehozzuk az audio taget, majd beállítjuk a hang forrását. Állíthatunk a hangerőn is a 0-1 intervallumban, illetve mivel a háttérzenét folyamatosan szeretnénk hallani, végteleníthetjük is azt a loop attribútum segítségével. Ezután a play() függvénnyel tudjuk elindítani a zenét és ha szeretnénk, a pause() függvénnyel le is állíthatjuk.

A játékban alkalmazunk különböző hangeffekteket, pl. a csőlerakás esetén. Először létrehozzuk a kívánt audio objektumokat, majd a program azon részén, ahol maga a csőlerakás történik, meghívjuk a play() függvényt. Mivel ezek az effektek rövidek, és csak egyszer szeretnénk mindig lejátszani őket, nem kell használni a loop lehetőséget. Az egyetlen probléma ezzel a megoldással az, hogy az effektek 1-2 másodperces hossza alatt több csőelemet is le tudunk tenni. Viszont ha egy már lejátszás állapotban lévő audio tagre play()-t hívunk, akkor nem fog semmi történni, ami ahhoz a hibához vezet, hogy nem minden kattintáskor játszódik le a hang. Ennek kijátszására létrehozhatunk több audio tag-et ugyanahhoz a hangfájlhoz és sorban elindíthatjuk őket. Nem szükséges túl sokat létrehozni, mert miután véget ért az elsőn a lejátszás, utána már újra használhatjuk azt is, érdemes tehát körbe-körbe haladni a lejátszóink között, számon tartva mindig, hogy épp melyiknél tartunk.

Állapotmentések

A HTML5 lehetővé teszi, hogy kulcs-érték párokat tárolhassunk úgy, hogy a böngésző egy későbbi futtatásakor is elérhetőek maradjanak. Ezt a megoldást localStorage-nek nevezzük. Segítségével elmenthetünk olyan adatokat, melyeket a játék jövőbeni futtatásakor szeretnénk még használni. A localStorage-ben mindaddig megmarad az adat, amíg nem töröljük a böngészési adatokat, vagy nem töröljük kézileg a kulcsot.

A példaprogramban számos pálya van, melyeket teljesítve lehetőségünk van felszabadítani a soron következő pályát. Ha azonban kilépünk a játékból, elvész minden, kezdhetjük előről az első pályával. Ezt könnyen át tudjuk hidalni, ha elmentjük a localStorage-be, hogy melyik az utolsó teljesített pálya, majd a következő futtatáskor visszaolvassuk azt. Ugyanezt a módszert a hang ki-be állapotának tárolására is érdemes lehet alkalmazni, így nem kell minden frissítés után kikapcsolni a hangot, ha éppen csendes módban szeretnénk játszani.

A módszer használata nagyon egyszerű:

localStorage.setItem('kulcs', 'Érték');

A setItem függvény segítségével tudunk felvenni egy kulcs-érték párt. Létezik másik megoldás is, ezt használtuk a példaprogramban:

localStorage['kulcs'] = 'Érték';

Természetesen törölni is tudunk:

localStorage.removeItem('kulcs');

Az értékek visszaolvasása szintén egyszerű, és több féleképpen is történhet:

localStorage.getItem('kulcs')
localStorage.kulcs
localStorage["kulcs"]

Mielőtt használjuk a localStorage-et, érdemes valamilyen módon biztosítani azt, hogy ne történjen gond, ha a böngésző nem támogatja. Ezt egy egyszerű try-catch szerkezettel érdemes megtenni.

A portál megkötései

Mielőtt kész játékunkat feltöltjük a portálra, érdemes megbizonyosodni arról, hogy a feltöltési szabályoknak és a portál egyéb megkötéseinek megfelel-e a programunk.

A portál szabályzatának ide vonatkozó része a következőket követeli meg:

  • Egyetlen zip fájlban kell feltölteni a kész játékot.
  • A zip-ben bármilyen mappa lehet, de a gyökérben kell lennie egy index.html fájlnak, ami magát a játékot tartalmazza. Ebben a fájlban semmilyen más látszódó elem nem lehet a canvason kívül.
  • A zipben lévő fájlok és mappák neveiben csak az angol ABC betűi és a . (pont) valamit a - (kötőjel) szerepelhet. Tehát ne használj szóközt, kettőskeresztet, vagy más speciális karaktert.
  • Az index.html fájlban lévő canvas mérete pontosan 800x500 pixel legyen.
  • A zip-ben a következő kiterjesztésű fájlok lehetnek: bmp, css, eot, gif, htm, html, jpg, jpeg, js, mp3, ogg, png, ttf, txt, wav, xml, woff, svg.
  • A zip fájl mérete legfeljebb 15 MB lehet.
  • Az index.html-ben a <head> és </head> közé, a saját javascript-ek elé el kell helyezni a következő sort:
    <script src="http://www.html5jatekok.hu/Static/js/protect.min.js"></script>

Fontos dolog még, hogy az oldalon a feltöltött játékok egy iframe-be ágyazva jelennek meg, ezért ha például főnök gombot szeretnénk beállítani, akkor az átirányításnál ügyelni kell arra, hogy ne az iframe-be töltsük be a főnök-oldalt. Ezt a problémát könnyen elkerülhetjük, ha a document.onkeydown eseménykezelő függvény megfelelő részében a következő kódrészlet mintájára valósítjuk meg az átirányítást:

window.top.location.href = "http://...";

A játékok fejlesztése során érdemes még szem előtt tartani azt is, hogy a portálon egy oldal nem biztos, hogy kifér teljes egészében a képernyőre. Ilyenkor ha a játék a nyílbillentyűket vagy a space gombot is használja, akkor könnyen játszhatatlanná válhat a hatásukra bekövetkező görgetés miatt. A problémát el lehet kerülni úgy, hogy a következő sort beszúrjuk a document.onkeydown függvény végére:

if( e.keyCode == 32 || (e.keyCode >= 37 && e.keyCode <= 40) ) return false;

Így a document.onkeydown függvény nem adja tovább a billentyűlenyomás eseményét a böngészőnek, ha a space vagy valamelyik nyílbillentyű lett lenyomva. Érdemes csak ezekre a billentyűkre szűrni, hiszen ha semmilyen eseményt nem adna tovább, akkor például frissíteni sem tudna a felhasználó az F5-tel. Vigyázat, a space gomb lenyomásának eseménye a document.onkeypress eseménykezelő függvény felé sem adódik így tovább, ezért ha szeretnénk ott is felhasználni, akkor tovább kell engedni a document.onkeydown-ból és csak a document.onkeypress függvényben kiszűrni.

A kész játék

Az elkészült példaprogram az alábbi képen látható, illetve le is tölthető.

Az elkészült játék.