Täiendame ja parandame varem loodud mängu Aliens Attack, Aliens Attack - tasemetega.
Mängu struktuur
Peale tegeliku mänguekraani on mängudel ka tiitelekraan, taseme valiku ekraan jne. See struktuur võib olla üsna keeruline (kaart, kogutud esemete varamu jne), kuid selle mängu jaoks piisab üsna lihtsast kolmetasemelisest hierarhiast: tiitel, mängu selgitus koos "Start"-nupuga ja tegelik mänguekraan; mängu lõppedes avaneb taas ekraan "Start"-nupuga mängu uuesti proovimiseks.
Sellise struktuuri loomisel tuleb korduvalt kirjutada ekraanile teksti, luua nuppe ja neile sündmuste (mousemove, mousedown) kuulajaid. Nende loomist lihtsustavad korduvalt kasutatavad utiliidid.
gameText
Utiliit gameText paigutab ekraanile etteantud kohta teksti ja teeb automaatselt reavahetused (word wrap), kui tekst ei mahu ettenähtud laiusega maxWidth alasse. Kahjuks puuduvad need omadused (ka reavahetus '\n' kasutamine) praegu html5 funktsioonist ctx.fillText. Teksti mahutamiseks ettenähtud laiusega alasse (uuele reale viimised) tehakse väljastatud teksti laiuse mõõtmisega (laius on erinevatel ekraaniajutel erinev): measureText(text).width; tekstirea kõrgus on määratud fondiga, mis antakse nagu CSS-is, näit 36pt Arial.
function gameText(text,fnt,x,y,sz,col,align,baseln,maxWidth) { //sz - px //align - start, end, left, center, right // baseln - top, bottom, middle, alphabetic, hanging //font - fontface, var words = text.split(' '); var line = ''; ctx.textAlign = align; //"center"; ctx.textBaseline = baseln; //"top"; ctx.fillStyle = col; ctx.font = sz+"px "+fnt; //16px Verdana, for(var n = 0; n < words.length; n++) { if (words[n] == '\n') { ctx.fillText(line, x, y); line = ''; y +=sz; } else { var testLine = line + words[n] + ' '; var metrics = ctx.measureText(testLine); var testWidth = metrics.width; if (testWidth > maxWidth) { ctx.fillText(line, x, y); line = words[n] + ' '; y += sz; //uus rida; } else { line = testLine; } } } ctx.fillText(line, x, y); //viimane rida return y; //viimase rea kõrgus }
Kanvaal on kaks meetodit teksti väljastamiseks: fillText - joonistab vaid glüfide piirjooned, strokeText joonistab värviga täidetud glüfid, erinevust võib näha siin.
Oma fondid
Kuna font kuulub arvutile ja seda ei või vabalt levitada (fondiärimeeste, eelkõige Adobe äri!), kui kasutajal pole fondile piisavalt õigusi, on kogu senine fontide kasutamine html-dokumentides olnud üsna udune - kunagi pole kindel, millised fondid kasutaja arvutis on installeeritud ja millised mitte, sellepärast määrataksegi html/CSS font sageli vaid üldise klassina: serif, san-serif; pealegi võtab fontide laadimine aega. Kuid kui kasutajal on fondile pissavalt õigusi ('Installable Embedding allowed'), võib praegu proovida ka oma fondi kasutamist.
Google pakub lingitatavaid (mitte mahalaaditavaid!) web-fonte, kuid nende kasutamist on iseloomustatud kommentaariga: "What a BS waste of time. F HTML 5... F Web Fonts... F Google Fonts. Coding should be much easier than this" .
Oma fontide jaoks võib proovida CSS3 omadust @font-face (enamuses brauseritest juba realiseeritud). See nõuab fondi esitamist mitmes erinevas formaadis - .otf/.ttf, .woff, .eot, .svg. Need formaadid saab genereerida .ttf formaadis fondist (kui selle õigused lubavad - "Installable Embedding allowed" - Windows näitab fondi omadustes) näiteks saidil FontSquirrel (http://www.fontsquirrel.com/tools/webfont-generator) või saidl Font2Web (http://www.font2web.com/). Neied saidid loovad .ttf formaadis esitatud fondile ka .woff, .eos, .svg formaadid ja väikese stiililehe, kus on loodud @font-face märgend; selles esitatud font-family omaduse väärtust võib kasutada fondi nimena.
Veidi erinev on saidi Cufón (http://cufon.shoqolate.com/generate/) meetod - siin luuakse fondi kirjeldus glüfide SVG/VML (VML - IE jaoks) kõveratena; see kirjeldus salvestatakse javascript-failis.
Siin on mängu tiitel ja tekst nupul esitatud vaba fondiga TNG_Title, mis on konverteeritud saidil Font2Web vajalikese formaatidesse (.woff,.eos,.svg); konverteeritud fontidega koos saadud stiilileht fonts.css
/** Generated by FG **/ @font-face { font-family: 'Conv_TNG_Title'; src: url('TNG_Title.eot'); src: local('☺'), url('TNG_Title.woff') format('woff'), url('TNG_Title.ttf') format('truetype'), url('TNG_Title.svg') format('svg'); font-weight: normal; font-style: normal; }on selle html-dokumendi päises lingitud ja sinna on lisatud stiil:
canvas { display:block; margin:1em auto; background:url("images/space0.png"); @font-face { font-family: 'Conv_TNG_Title'; src: url('TNG Title.eot?') format('eot'), url('TNG Title.woff') format('woff'), url('TNG Title.ttf') format('truetype'); } }
Mängu tiitli kirjutamisel funktsiooniga gameText of fondiks määratud Conv_TNG_Title.
gameButton
Utiliit gameButton loob ekraanile nupu. Et see näiks ruumilise(ma)na (3D), joonistatakse värvilise ristküliku vasak ja ülaserv heleda värviga (sealt langeb valgus), parem ja alaserv tumedalt (varjus); joone paksuse muutmisega saab effekti suurendada/vähendada.
function gameButton(text,fnt,sz,backCol,textCol,x,y,w,h){ ctx.fillStyle = backCol; ctx.fillRect(x,y,w,h); ctx.fillStyle = textCol; ctx.textAlign = "center"; ctx.textBaseline = "middle"; //"top"; ctx.font = sz+"pt "+fnt; //Verdana"; ctx.fillText(text,x+w/2,y+h/2); ctx.lineWidth = 2; //3D effect ctx.beginPath(); ctx.strokeStyle = '#DDDDFF';//light ctx.moveTo(x,y+h); ctx.lineTo(x,y); ctx.lineTo(x+w,y); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = '#222266';//dark ctx.moveTo(x,y+h); ctx.lineTo(x+w,y+h); ctx.lineTo(x+w,y); ctx.stroke(); }
Tiitelekraani loomine
Tiitelekraani loob funktsioon titleScreen. Selle näitamiseks arvutatakse (suhtes mängu ekraani/kaadri mõõtmetega - see peab töötama erineva suurusega ekraanidel) mängu tiitli näitamise ala laius ja ülaserv (kõrguse annab tulemusena gametext), kuvatakse tiitel ja selle alla nupp jätkamiseks.
Et nupp midagi teeks, peab talle määrama sellel klikkamise (sündmuse mousedown) kuulaja (funktsiooni) ja kontrollima klikkamisel, et see toimus nupul. Sellise funktsionaalsuse võib raliseerida näiteks nii:
canvas.addEventListener('mousedown', function(evt) { if (mouse_in) { //siia tuleb kontroll, kas klikk toimus nupul canvas.removeEventListener('mousedown', arguments.callee, false); //anonüümse/nimeta funktsiooni puhul startScreen(story,'') //avaleht! } }, false);
Kaasajal ootavad mängijad nuppudelt rohkem tagasisidet:
- hiire viimisel nupule peaks kursor muutuma
- ja nupp tavaliselt näitab väikese alla-paremale nihkega, et see on klikatav.
Esimese funktsionaalsuse (kursori muutumise) saab realiseerida sündmuse mousemove kuulaja lisamisega. Kuna siit edasi liikumisel peab ka kõik sündmuste kuulajad eemaldama, siis siin tuleb sündmusele rageerimiseks kasutada juba nimega funktsiooni. Et selles määratud muutuja mouse_in väärtus oleks kättesaadav ka hiljem, tuleb see muutuja enne deklareerida (Javaskripti muutujate skoop on funktsioon, mitte blokk, nagu C-tüüpi keeltes!); parameeter offset on nupu paremale-alla nihe, x1,y1,w1,h1 - nupu (nelinurga9 attribuudid):
var mouse_in = false; canvas.addEventListener("mousemove", function(evt){ var mousePos = getMousePos(evt); if ((mousePos.x > x1) && (mousePos.x < x1+w1) && (mousePos.y > y1) && (mousePos.y < y1 + h1)) { mouse_in = true; offset = 1; document.body.style.cursor = "pointer"; } else { mouse_in = false; offset = 0; document.body.style.cursor = ""; } }, false);
Hiirekliki koordinaadid
Klikkamisel genereerib brauser kliki koordinaadid (vasakult/ülalt) kogu ekraani suhtes; et neist saada kanvaa koordinaate (kanvaa vasaku ülanurga suhtes), peab teadma kanvaa asukohta ekraanil. Selleks on mitu võimalust
- otsida kanvaa asukoht stiilide/sisestamise põhjal (kiire, kuid mõnikord ebakindel)
- kasutada enamuses kaasaja brauserites juba realiseeritud Javaskripti funktsiooni obj.getBoundingClientRect(), kus obj on mingi HTML-lehe struktuuri (DOM - Domain Object Model) objekt.
function getMousePos(evt) { var rect = canvas.getBoundingClientRect(); var mousePos = { x: evt.clientX - rect.left,y: evt.clientY - rect.top}; return mousePos; }Küsimus: ülalesitatud kood pole päris optimaalne - miks ja kuidas seda parandada ?
Kuna hiire asukoht on sündmuse mousemove kuulajaga juba kindlaks tektud, on klikkamise, s.t. sündmuse mousedown kuulaja lihtne; kuulaja võib realiseerida anonüümse (nimeta) funktsioonina, sest kuulaja eemaldamisel saab selle funktsiooni viida muutujast arguments.callee:
canvas.addEventListener('mousedown', function(evt) { if (mouse_in) { canvas.removeEventListener('mousedown', arguments.callee, false); startScreen(story,'') //avaleht! } }, false);
Visuaalne effekt - ruumiline (3D) tekst
Mängu tiitel (nimi) on mängu väga tähtis omadus, ilmekama tiitelekraani saab oma fondi kasutamisega ja tiitli teksti näitamisel ruumilisena - tumedama (vari) teksti peale on paar pikslit üles-vasemale nihutamisega kirjutatud heledam tekst - tekib ruumilisuse (3D) effekt.function titleScreen(title){ var marginX = Math.floor(w0/8); var marginY = Math.floor(h0/6); var fnt = 'Conv_TNG_Title'; //"Verdana"; var sz = 58; var x1 = w0/2 - 2*marginX; var colTitle = "rgb(255, 0, 0)"; var colTitle1 = "rgb(128, 0, 0)"; var colBtn = 'rgba(150, 150, 250,0.5)'; gameText(title,fnt,w0/2+2,marginY+2,sz,colTitle1,"center","top",6*marginX); var y1 = gameText(title,fnt,w0/2,marginY,sz,colTitle,"center","top",6*marginX)+2*sz; ...
See skript kirjutab muutuja title (string!) väärtuse kaks korda - algul tumedamalt alla (rgb(128, 0, 0) - tumepunane) ja siis 2 pikslit üles ja savakule nihutatuna selle peale (rgb(255, 0, 0)- helepunane).
Visuaalne effekt - nupu nihe
Et saavutada ka nupu visuaalset paremale-alla nihet hiire peale viimisel, tuleb kogu avaekraani lõpus käivitada kaadrivahetus - muidu seda visuaalset effekti 8animatsiooni) ei teki, hiire liikumisel tekkivat nupu liikumist pole võimalik näidata. Animatsioonifunktsioon on kogu avaekraani genereeriva funktsiooni titleScreen alamsunktsioon, s.t. kirjeldatud selle sees. Kuna Javaskripti muutujate skoop on funktsioon (mitte blokk!), on sellel funktisoonile kärtesaadavad kõik emafunktsioonis titleScreen väärtustatud muutujad, seega selle funktsiooni lõpus:
function titleShow(){ ctx.clearRect(0, 0, w0, h0); gameText(title,fnt,w0/2+3,marginY+3,sz,colTitle1,"center","top",6*marginX); gameText(title,fnt,w0/2,marginY,sz,colTitle,"center","top",6*marginX); gameButton(playText,fnt,sz1,colBtn,'#662222',x1+offset,y1+offset,w1,h1); if (showStart){ // klikk !!! canvas.removeEventListener('mousemove', mouseMv, false); canvas.removeEventListener('mousedown', arguments.callee, false); startScreen(story,'') //avaleht! } else setTimeout(titleShow,FT); } }
Muutuja showStart (käivitab mängu) saab väärtuse klikkamisel:
canvas.addEventListener('mousedown', function(evt) { if (mouse_in) { showStart = true; /* canvas.removeEventListener('mousemove', mouseMv, false); canvas.removeEventListener('mousedown', arguments.callee, false); startScreen(story,'') //avaleht! */ } }, false); titleShow();
Tiitelekraan
var gameTitle = "Juuljus ja Juudalased"; titleScreen(gameTitle); function titleScreen(title){ var marginX = Math.floor(w0/8); var marginY = Math.floor(h0/6); var fnt = 'Conv_TNG_Title'; //"Verdana"; var sz = 58; var x1 = w0/2 - 2*marginX; var colTitle = "rgb(255, 0, 0)"; var colTitle1 = "rgb(128, 0, 0)"; var colBtn = 'rgba(150, 150, 250,0.5)'; gameText(title,fnt,w0/2+2,marginY+2,sz,colTitle1,"center","top",6*marginX); var y1 = gameText(title,fnt,w0/2,marginY,sz,colTitle,"center","top",6*marginX)+2*sz; var w1 = 4*marginX; var offset = 0; //allavajutamise nihe var playText = "Play!"; var sz1 = 24; var h1 = 4*sz1; gameButton(playText,fnt,sz1,colBtn,'#662222',x1,y1,w1,h1); var mouse_in = false; var showStart = false; canvas.addEventListener("mousemove", mouseMv, false); function mouseMv(evt){ var mousePos = getMousePos( evt); if ((mousePos.x > x1) && (mousePos.x < x1+w1) && (mousePos.y > y1) && (mousePos.y < y1 + h1)) { mouse_in = true; offset = 1; document.body.style.cursor = "pointer"; } else { mouse_in = false; offset = 0; document.body.style.cursor = ""; } } canvas.addEventListener('mousedown', function(evt) { if (mouse_in) { showStart = true; canvas.removeEventListener('mousemove', mouseMv, false); canvas.removeEventListener('mousedown', arguments.callee, false); startScreen(story,'') //avaleht! } }, false); titleShow(); function titleShow(){ ctx.clearRect(0, 0, w0, h0); gameText(title,fnt,w0/2+3,marginY+3,sz,colTitle1,"center","top",6*marginX); gameText(title,fnt,w0/2,marginY,sz,colTitle,"center","top",6*marginX); gameButton(playText,fnt,sz1,colBtn,'#662222',x1+offset,y1+offset,w1,h1); if (showStart) startScreen(story,''); else setTimeout(titleShow,FT); } }
Alustamine
Mängu alustamiseks vajab mängija -
- motivatsiooni - see on selle mängu alguses kursiivis esitatud lugu, mis on salvestatud eraldi Javaskripti failis story.js muutuja story väärusena (string), s.t. seal var story = "Talibanid on teada saanud,..."
- reegleid; siin on need väga lihtsad: liikumiseks nooleklahvid, tulistamiseks "X"
Selle loob ekraanile funktsioon startScreen, mille põhimõte on täiesti analoogiline, kuid selle loomisel peab arvestama, et mängu lõppedes esitatakse uuesti mängimiseks taas see ekraan, kus on ka eelnenud mängu tulemus funktsiooni parameetris msg (uuesti käivitamisel mängu lugu ei korrata, s.t. funktsiooni startScreen esimene formaalne parameeter txt = '' :
//käivitame! function startScreen (txt,msg){ var text = txt+"\nLiigu: nooleklahvid; tulista: 'X'"; var msg = msg; var marginX = Math.floor(w0/10); var marginY = Math.floor(h0/10); var fnt = "Verdana"; var sz = 20; var x1 = w0/2 - 2*marginX; var y1 = gameText(text,fnt,w0/2,marginY,sz,"rgb(250, 150, 150)","center","top",8*marginX)+4*sz; announce(msg); var w1 = 4*marginX; var h1 = 2*sz; var offset = 0; //allavajutamise nihe gameButton('Start!',fnt,sz,'rgba(150, 150, 250,0.5)','#662222',x1,y1,w1,h1); level = 0; enemies = []; munad = []; var mouse_in = false; canvas.addEventListener("mousemove", mouseMv, false); function mouseMv(evt){ var mousePos = getMousePos( evt); if ((mousePos.x > x1) && (mousePos.x < x1+w1) && (mousePos.y > y1) && (mousePos.y < y1 + h1)) { mouse_in = true; offset = 1; document.body.style.cursor = "pointer"; } else { mouse_in = false; offset = 0; document.body.style.cursor = ""; } } canvas.addEventListener('mousedown', function(evt) { if (mouse_in) { canvas.removeEventListener('mousemove', mouseMv, false); canvas.removeEventListener('mousedown', arguments.callee, false); player.kills = 0; player.health = 100; gameOver = false; addKeyListeners(); gm = setInterval(gameLoop,FT); //käivitame! } }, false); startShow(); function startShow(){ ctx.clearRect(0, 0, w0, h0); gameText(text,fnt,w0/2,marginY,sz,"rgb(250, 150, 150)","center","top",8*marginX)+4*sz; gameButton('Start!',fnt,sz,'rgba(150, 150, 250,0.5)','#662222',x1+offset,y1+offset,w1,h1); announce(msg); if (!gameOver) gm = setInterval(gameLoop,FT); else setTimeout(startShow,FT); } }
Lõpetamine
Mängu lõpetamisel peab eemaldama klaviatuurisündmuste kuulajad ja enne start-ekraanile tagasi pöördumist ootama, kuni kõik plahvatused on lõpuni mänginud. Sõltuvalt tulemusest (kaotus/võit) antakse muutuja msg-le tulemust esitav väärtus ja see esitatakse taas stardiekraanil:
function gameLoop(){ update(); draw(); if (player.health > 0) { if (player.kills > (level+1)*nEnemies) { level++ ; if (level <= maxLevels) { enemies.forEach(function(e) { e.sprite = enemySprite(); e.vy += player.kills * e.increase;}); } else { msg = "Võit!"; stopGame(msg); } } } else { msg = 'Oled surnud...' stopGame(msg); } } function stopGame(msg){ clearInterval(gm); gameOver = true; var msg = msg draw(); announce(msg); removeKeyListeners(); bullets.length = 0; paused(); //plahvatuste lõpetamine function paused(){ announce(msg); draw(); activeExplosions = explosions.filter(function(e) { return e.state == "active"; }); //console.log(activeExplosions.length); if (activeExplosions.length == 0){ startScreen(' ',msg);} else setTimeout(paused,FT); } }
© Jaak Henno