Esimene seenekorjamise mäng on üksluine - vaid üks seen ja ühesugused puud, mingeid muid objekte pole. Täiendame mängu:
- eesmärk on kiiresti korjata teatud hulk seeni (ajakontroll ja korjatud seente kontroll);
- mänguväljal on ka takistusi - kivid ja kännud, neist ei saa üle minna;
- mängijal on animeeritud liikumine kõigis suundades: vasakule, paremale, siia, sinna;
- mänguväljale ilmuvad seened pikkamööda (kasvavad) ja kui mõni seen vanaks saab, siis ta kaob;
- mõned seened (kärbseseened) on mürgised, nendega kokku puutudes mängija kukub pikali ja lebab veidi aega (kaotab aega).
Ajakontroll
Javaskripti objekt Date() salvestab millisekundites aja, mis on möödunud 01.01.1970 keskööst. Mängu jooksul möödunud aja saamiseks salvestame mängu initsialiseerimisel praeguse ajahetke
var startTime = Date.now();
ja mängu käigus annab kasutatud aja sekundites muutuja
var time=Math.round(((Date.now()-startTime)/1000));
Takistused
Mängija (hero) ei pääse takistustest mööda, seege tuleb kontrollida kokkupõrkeid kõigi takistustega. Selleks peab teadma takistuse koordinaate x,y ja takistuse laiust-kõrgust. Sellepärast ei piisa takistuse joonistamisest kanvaale käsuga ctx.drawImage() - see ei salvesta koordinaate ega mõõte. Kuna takistustel võib olla ka muid omadusi, peaks nii koordinaadid, mõõtmed ja omadused (erinevad spriidid) olema salvestatud takistuse objektis. Takistuste klassi loob funktsioon
function takistus(){ this.sprite = (Math.random()>0.5)?(images.kiviSprite):(images.kandSprite); //kaks spriiti this.width = this.sprite.width; this.height = this.sprite.height; }
Uue takistuse loob (näiteks) käsk
var kivi = new takistus();
Javaskripti objektidele võib alati lisada uusi omadusi - kuigi takistuse loomise funktsioonis ei määrata taksitusele koordinaate this.x, this.y, võib igale klassifailiga takistus() loodud objektile alati lisada ka koordinaadid, s.t. omadused kivi.x, kivi.y
Seened
Ka seened peavad olema objektid, kuid neil on peale spriidi (mitu erinevat seent) , asukoha ja mõõtmete veel omadus hea (kärbseseened ei ole head!). Seened ei ole väljas kogu aeg - nad ilmuvad mingil hetkel väga väikestena, kasvavad suureks (spriidi täielikud mõõtmed - pikselkujutisi ei tohi suurendada!) ja kui nad on teatud aja maapinnal olnud, siis kaovad (nende vanus saab suuremaks kui neile määratud iga). Seega seente klassifail on
function seen_obj() { if (Math.random() < 0.3){ this.sprite = images.seenSprite0; //kärbseseen ! this.hea = false;} else { this.sprite = (Math.random() > 0.5)?(images.seenSprite1):(images.seenSprite2); this.hea = true;} this.size = this.sprite.width; //maksimaalne suurus, milleni kasvab this.suurus = 0; //pole veel hakanud kasvama this.vanus = 0; //pole veel maa peal this.speed = 0.05; //kui palju igas kaadris suureneb this.iga = 500; //frames! this.vana = false; //kui vanaks saab, siis kaob! this.width = this.sprite.width; this.height = this.sprite.height; this.x = images.seenSprite1.width + (Math.random() * (w - 2*images.seenSprite1.width)); this.y = images.seenSprite1.height + (Math.random() * (h - 2*images.seenSprite1.height)); this.draw = function(){ if (this.vanus > 0 ) { //on hakanud kasvama if (this.suurus < this.size){ this.suurus += this.speed; ctx.drawImage(this.sprite,this.x,this.y,this.suurus,this.suurus); this.vanus ++; } else { if (this.vanus < this.iga){ ctx.drawImage(this.sprite,this.x,this.y); this.vanus ++; } else { this.vana = true; this.suurus = 0; //sureb ! this.vanus = 0;} } } } }
Kokkupõrked
Kahe objekti vahelised kokkupõrked arvutatakse taas nende piirdenelinurkade abil:
function collision(obj1,obj2){ return obj2.x <= (obj1.x + obj1.width) && obj1.x <= (obj2.x + obj2.width) && obj2.y <= (obj1.y + obj1.height) && obj1.y <= (obj2.y + obj2.height); }
Loomulikult peavad kõigil kokkupõrgetes osalevatel objektidel (kangelane, seened, takistused) olema omadused .width, .height määratud, v.t. näiteks funktsiooni takistus().
Mänguväljale paigutatud takistused ja seened salvestatkse massiivides (array), seega on tarvis ka funktsiooni, mis kontrollib ühe objekti kokkupõrkeid kõigi massiivi objektidega. Kuna mängija kokkupõrkel seenega tuleb seen mänguväljalt eemaldada (see korjati!), peab massiviga kokkupõrkeid kontrolliv funktsioon väljastama massivi elemendi indeksi; kui kokkupõrkeid polnud, väljastatakse -1 :
function collisions(o,obs){ for(var i=0; i < obs.length; i++){ if (collision(o,obs[i])) return i; } return -1; }
Mänguvälja objektide loomine
Takistused ja seened tuleb paigutada mänguväljale nii, et nad ei kataks üksteist, s.t. et nende vahel poleks kokkupõrkeid. See kontroll on üsna aeglane (tuleb kontrollida juhuslikke kohti), seepärast paigutatakse alati vaid teatud (suhteliselt väike) arv seeni ja taksitusi; kui on tarvis rohkem seeni, siis taksituste asukohta enam ei muudeta (metsas ei hüppa kivid ja kännud uude kohta, kuid seened võivad ilmuda ükskõik kus):
function createObjs(seeni,kive){ seened.length = 0; yleval = 0; //clear old objs for(var i=0; i < seeni;i++){ var seen = new seen_obj(); //(Math.random()>0.5)?seen1:seen2; do{ seen.x = d0+seen.sprite.width + (Math.random() * (w - d0-2*seen.sprite.width)); seen.y = d0+seen.sprite.height + (Math.random() * (h - d0-2*seen.sprite.height)); } while (collision(hero,seen) || (collisions(seen,seened) > -1));// || collisions(seen,kivid) ); seened.push(seen); } if(kive > 0){ kivid.length = 0; for(i=0; i < kive; i++){ var kivi = new takistus(); do{ kivi.x = kivi.sprite.width + (Math.random() * (w - 2*d0-2*kivi.sprite.width)); kivi.y = kivi.sprite.height + (Math.random() * (h - 2*d0-2*kivi.sprite.height)); } while (collision(hero,kivi) || (collisions(kivi,seened) > -1) || (collisions(kivi,kivid) > -1)); ctx.drawImage(kivi.sprite, kivi.x, kivi.y); kivid.push(kivi); } } }
Objektid (takistused ja seened) joonistab igas kaadris ekraanile järgmine funktsioon drawObjs(). See aktiveerib (paneb kasvama) juhuslikul hetkel uue seene (kui loodud seente hulgas on veel selliseid, mis pole kasvama hakanud), eemaldab vanad ja kui eesmärk pole veel saavutatud (pole korjatud piisav arv seeni), käivitab uuesti seente loomise funktsiooni (kuid takistused jäävad vanad). Vanade seente eemaldamisel tuleb hakata liikuma seente massiivi lõpust, sest iga seene eemaldamisel seente massiiv lüheneb :
function drawObjs(){ if ( (yleval < seeni) && (Math.random() < 0.01)) { do { var i = Math.floor(Math.random()*seened.length); } while (seened[i].vanus > 0); seened[i].vanus = 1; yleval ++; } for(var i = seened.length -1; i >= 0; i--){ if (seened[i].vana){ //viskame välja ! seened.splice(i,1); } else seened[i].draw(); } for( i=0; i < kivid.length;i++){ ctx.drawImage(kivid[i].sprite, kivid[i].x, kivid[i].y); } }
Animeeritud Punamütsike
Punamütsikese spriidileht kirjeldab nelja liikumist - vasakule, paremale, sinna, siia;
iga liikumine on kirjeldatud viie samas reas oleva kaadriga, kõik suurusega 50x72px. Seega spriidilehel olevate animatsioonide koordinaadid (algused) võib kirjeldada objektiga
{"vasakule":[0,0],"paremale":[0,72],"sinna":[0,144],"siia":[0,216]}
Animeeritud objekti klassi kirjeldav funktsioon animated on peaaegu sama kui lihtsa kolmest kaadrist koosneva kangelase klassifunktsioon, erinevused selle funktsioonis (klassi meetodis) draw() tulevad kaadrite erinevast paigutusest (enne: ülalt alla, nüüd samas reas)
function animated(img,dx,dy,states, n_fr,state,pos,sp){ //states - array of coordinates this.image = img; this.width = dx; //kaadri laius this.height = dy; this.states = states; this.dx = dx; //this.width; this.dy = dy; //this.height/3; //32 this.n_frames = n_fr; //number of frames this.actualFrame = 0; this.speed = sp; //how many *game* frames a frame is shown this.step = 0; this.y = 0; this.x = 0; this.state = state; this.pos = pos; this.setPosition = function(X, Y){ this.x = X; this.y = Y; } this.draw = function(){ if (this.pos == 0) //still ctx.drawImage(this.image,this.states[this.state][0],this.states[this.state][1],this.dx,this.dy,this.x,this.y,this.dx,this.dy); else { // walking ctx.drawImage(this.image,this.states[this.state][0] + this.dx*this.actualFrame,this.states[this.state][1] ,this.dx,this.dy,this.x,this.y,this.dx,this.dy); this.step ++; if (this.step == this.speed) { this.actualFrame++; //this.actualFrame = this.actualFrame%this.n_frames 1; if (this.actualFrame == this.n_frames) this.actualFrame = 1; this.step = 0; } } } }
Selle klassifunktsiooni põhjal luuaks animeeritud tegelane hero ja antakse talle veel mõned omadused (mis on määratud klassifunktsiooni animated kirjelduses eesliitega this. ...) ja funktsioonid/meetodid (klassifunktsiooni alamfunktsioonid, näit draw() - klassi abil loodud objekti hero (loodud käsuga hero = new animated...) joonistab ekraanile edasi alati vaid objekti hero meetod draw, s.t. käsk hero.draw():
hero = new animated(images.heroSprite,50,72,{"vasakule":[0,0],"paremale":[0,72],"sinna":[0,144],"siia":[0,216]},5,"siia",1,4); hero.myrgitatud = false; hero.pikali = 0; //aeg kui kaua on juba olnud hero.paraneb = 200; /aeg paranemiseks hero.setPosition(w/2,h/2); //hero.draw(); //testimiseks !
Kaadri uuendamine
Enne kaadri ekraanile joonistamist tuleb kanvaa uuendada (värskendada) - arvutada objektide uued positsioonid.
function update() { if (!hero.myrgitatud){ //kangelane saab liikuda ! if (38 in keysDown || 87 in keysDown) { // Player holding up if (hero.y > 0 ){ hero.state = "sinna"; hero.pos = 1; //liigub hero.y -= hero.speed ;} if (collisions(hero,kivid)>-1){ hero.y += hero.speed; //tagasi! hero.pos = 1;} if (hero.y < d0){ //can not go through trees! hero.y = d0; hero.pos = 1;} } else if (40 in keysDown || 83 in keysDown) { // Player holding down if (hero.y < h - d0-hero.height){ hero.state = "siia"; hero.pos = 1; hero.y += hero.speed ;} if (collisions(hero,kivid)>-1) hero.y -= hero.speed; //tagasi! if (hero.y > h - d0-hero.height) hero.y = h - d0-hero.height; } else if (37 in keysDown || 65 in keysDown) { // Player holding left if (hero.x > 0){ hero.state = "vasakule"; hero.pos = 1; hero.x -= hero.speed ;} if (collisions(hero,kivid) > -1) hero.x += hero.speed; //tagasi! if (hero.x < d0) hero.x = d0; } else if (39 in keysDown || 68 in keysDown) { // Player holding right if (hero.x < w - d0-hero.width){ hero.state = "paremale"; hero.pos = 1; hero.x += hero.speed ;} if (collisions(hero,kivid) > -1) hero.x -= hero.speed; //tagasi! if (hero.x > w - d0-hero.width) hero.x = w - d0-hero.width; } else //ükski klahv pole alla vajutatud - kangelane seisab hero.pos = 0; //ei liigu // Kas korjas seene? var ind = collisions(hero,seened); if ((ind > -1) && (seened[ind].suurus > 0)){ ++collected; if(!(seened[ind].hea)){ hero.myrgitatud = true; hero.pikali = 1; //kukub pikali } seened.splice(ind,1); //seen eemaldatakse } if (seened.length == 0 && collected < seeni_vaja) createObjs(seeni,0); } };
Animatsiooni spriidileht
Tüüpiliselt kirjeldab animeeritud (mitmeid liikumisi omava) tegelase spriidileht, kus on kaadrid kõigi tema liikumiste jaoks. Tavaliselt on ühe liikumise ('vasakule', 'paremale', 'siia', 'sinna' - nimed on stringid!) kaadrid on samas reas ja kõige lihtsam on, kui kõigi kaadrite laius-pikkus on samad. WWW-st laetud spriidilehtede puhul esineb sageli probleeme, sellepärast peaks spriidilehe kaadreid enne Photoshop/Gimp abil kontrollima (nõit Photoshop-i joonlaude abil, nagu kõrval pildil) ja vajaduse korral asendeid nihutama või kokku/laiali suruma; kõige parem muidugi on teha (joonistada) oma, originaalne spriidileht.
Kaadri väljastamine ekraanile
Kõik mängu objektid (puud, takistused, seened, kangelane) joonistatakse ekraanile ühe funktsiooniga. Kui kangelane on korjanud mürgise kärbseseene, joonistatakse ta pikali olekus (animeeritud tegelase klassifailis ja tegelase objektis sellist funktsiooni pole). Kanvaale joonistamisel saab kanvaad transformeerida - muuta kanvaa alguspunkti (nullpunkti) asukohta ka pöörata kanvaad, seega pikali oleva kangelase joonistamiseks:
- salvestame kangelase praegused koordinaadid (neid on hiljem tarvis);
- liigutame kanvaa kangelase alguspunkti;
- pöörame kanvaad 90 kraadi;
- joonistame kangelase kanvaa alguspunkti;
- pöörame kanvaa tagasi, viime ka kanvaa alguspunkti tagasi ja omistame kangelasele taas selle õiged koordinaadid.
function render() { ctx.clearRect(0,0,w,h); drawTrees(); if (!(hero.myrgitatud)) hero.draw(); else { hero.pikali += 1; if (hero.pikali < hero.paraneb) { //hero on pikali var x0 = hero.x; var y0 = hero.y; ctx.translate(x0, y0); ctx.rotate(Math.PI / 2); hero.setPosition(0,0); hero.draw(); ctx.rotate(-Math.PI / 2); ctx.translate(-x0, -y0); hero.setPosition(x0,y0); } else { hero.myrgitatud = false; } } drawObjs(); }
Initsialiseerimine
Mängu initsialiseerimine (peaaegu) ei muutu - laetakse pildid, initsialiseeritakse kanvaa kontekst ja abimuutujad, luuakse kangelane, seened, takistused ja sündmuste (klahvivajutuste) kuulajad, seejärel käivitatakse mäng:
... hero = new animated(images.heroSprite,50,72,{"vasakule":[0,0],"paremale":[0,72],"sinna":[0,144],"siia":[0,216]},5,"siia",1,4); hero.myrgitatud = false; hero.pikali = 0; hero.paraneb = 200; hero.setPosition(w/2,h/2); //hero.draw(); //testimiseks !! seeni = 10; yleval = 0; kive = 10; seened = []; kivid = []; collected = 0; seeni_vaja = 10; ... createObjs(seeni,kive); //drawObjs(); //testimiseks ! game();
Mäng
function game() { if (collected < seeni_vaja) { update(); render(); showScore("Seeni: " + collected); } else { var time=Math.round(((Date.now()-startTime)/1000)); render(); showScore("Said "+time+ " sekundiga " + collected+" seent!"); } setTimeout(game,30); //järgmine kaader //requestAnimationFrame(game); };
Funktsioon setTimeout(game,30); käivitab 30ms järel uuesti funktsiooni game(), s.t. joonistatakse järgmine kaader; teise argumendi muutmisega võib reguleerida mängu kaadrivahetamise kiirust (kui mängus on palju objekte, võib kaadrivaetuse kiirust vähendada). Kui tahetakse maksimaalse kiirusega kaadrivahetust, võib setTimeout() asemel kasutada funktsiooni requestAnimationFrame(game); - see püüab teha kaadrivahetust koos brauseriga, s.t. alati kui brauser hakkab uut kaadrit ekraanile joonistama.
Kaadrivahetuse kiirust on lihtne kontrollida. Määrame mängu algul uue globaalse muutuja
var then = Date.now()ja lisame funktsioonile game() kasutatud aja arvutamise, ühtlasi näitame seda ka ekraanil:
var now = Date.now(); var delta = now - then; showScore("Aeg: "+delta+" Seeni: " + collected); then = now; // järgmise kaadri jaoks
Ülesandeid
1. Ülalkirjeldatud mäng on veel kiiresti ja kohati räpakalt programmeeritud ('quick and dirty'). Kui kangelane näiteks korjab kärbseseene, ei jää ta lebama, vaid hakkab (kord näoli, kord taevasse vaadates) maas siplema - mürgitatud tegelane peaks lamama vaikselt (ja tavaliselt selili).
2. Kui tegelane kukub pikali (mürgitatud) ja temast vasemal on kivi või känd, jääb ta nende alla - väga ebaloomulik, ta peaks lamama tühjal kohal.
3. Kui kangelane liigub paremale ja peatub, jääb tal üks jalg õhku (teistes suundades liikumisel on peatumispoos loomulik) - kuidas seda parandada ?