Breakout
Pall on objekt, millel on omadused:
Programmeerimiskeeltes, kus on olemas klassid, moodustatakse objektide loomiseks klass ja klassi uue objekti loob klassi konstruktorfunktsioon. Javaskriptis klasse pole, kuid (klassi) konstruktorfunktsiooni saab moodustada - klassi muutujate deklareerimisel peab muutujate ette lisama sõna this; nii deklareeritakse ka klassi meetodid.
Järgnev on palli loomise konstruktorfunktsioon:
function ball(x,y,r,col,vx,vy){ this.x = x; this.y = y; this.r = r; this.col = col; this.vx = vx; this.vy = vy; this.draw = function(){ ctx.beginPath(); ctx.arc(this.x,this.y,this.r,0,Math.PI * 2, false); ctx.strokeStyle = this.col;//hsl_col1; ctx.stroke(); ctx.fillStyle = this.col; //grd; ctx.fill(); ctx.closePath(); } this.move = function(w,h){ if (this.x+this.r+this.vx > w || this.x+-this.r+this.vx < 0) this.vx *= -1; if (this.y+this.r+this.vy > h || this.y+-this.r+this.vy < 0)//hiljem muuta - all peab põrkuma reketilt this.vy *= -1; this.x += this.vx; this.y += this.vy; } }
Palli (klassi objekti) loob ekraanile käsk new ball(x,y,r,col,vx,vy). Palli kiiruse komponendid vx, vy arvutatakse Javascripti juhusliku arvu (0..1) genereerimise funktsiooni Math.random() abil: algul leitakse juhuslik arv vahemikus 2..6 (pikslit), siis muudetakse pooltel juhtudel see negatiivseks - nii saadakse nii positiivseid kui ka negatiivseid alati nullist erinevaid väärtusi (mis juhtub, kui vx=0 või vy = 0 ?)
Et pall hakkaks liikuma, käivitatakse Javascripti taimeriga iga 30 ms järel funktsioon animate, mis algul liigutab palli uude kohta, siis puhastab kogu kanvaa ja joonistab nüüd palli juba uues kohas:
window.onload = function initgame() { var canvas = document.getElementById("mycanvas"); ctx = canvas.getContext("2d"); wc = canvas.width; hc = canvas.height; var vx = 2+Math.random()*4; //2..6 vx = Math.random()>0.5?vx:-vx; var vy = 2+Math.random()*4; //2..6 vy = Math.random()>0.5?vy:-vy; pall = new ball(wc/2,hc/2,10,'#FF0000',vx,vy) animate(); } //window.onload ! function animate(){ pall.move(wc,hc); ctx.clearRect(0,0,wc,hc); pall.draw(); setTimeout(animate,30); }
Nüüd võib juba testida - ekraanile peaks ilmuma kanvaa äärtelt põrkuv pall.
Ka mängus purustatavad tellised luuakse konstruktorfunktsiooniga.
Telliste laius, kõrgus ja ridade arv on muutujad, mis tuleb lisada kõigi teiste muutujate deklaratsioonide juurde funktsioonis initgame:
var wt = 20; //tellise laius var ht = 8; //tellise kõrgus var rt = 2; //ridade arv
Nende andmete põhjal loob tellise konstruktorfunktsioon
function tellis(x,y,w,h,col){ this.x = x; this.y = y; this.w = w; this.h = h; this.col = col; this.draw = function(){ ctx.fillStyle = this.col; ctx.fillRect(this.x,this.y,this.w,this.h); } } }
Telliseid on palju ja nende arv muutub mängu käigus, sellepärast salvestame nad massiivis tellised. Muutujate definitsioonidele tuleb funktsioonis initgame lisada selle muutuja algväärtustamine
tellised = []; var wt = 20; //tellise laius var ht = 8; //tellise kõrgus var ct = 'rgb(200,100,100)'; var rt = 2; //ridade arv lootellised(wt,ht,rt,ct);
ja kogu mängule funktsioon
function lootellised(w,h,r,c){ var n = Math.floor(wc/(1.5*w)); //iga tellise ette ja taha vaba ruumi 0.25*w var x0 = 0.25*w; var y0 = h; //üles ühe tellise paksus vaba for (var j=0;j < r;j++) for (var i=0;i < n;i++){ var x = x0+i*1.5*w; var y = y0+j*2*h; var kivi = new tellis(x,y,w,h,c); var y = y0+j*2*h; ctx.fillRect(x,y,w,h); ctx.fillRect(x,y0+j*2*h,w,h); tellised.push(kivi); //salvestame tellise massiivis } }
Siin jäetakse iga tellise ette ja taha vaba ruumi veerand tellise pikkusest. Et tellised mängu ajal näha oleks, tuleb elliste joonistamine lisada ka funktsiooni animate:
function drawtellised(){ for(var i=0;i < tellised.length;i++){ tellised[i].draw(); } }
Reketi (kõige lihtsama) parameetrid on määratud muutujatega
wr = 40; //reketi laius hr = 2; //reketi kõrgus vr = 2; //reketi kiirus kaadris
Reketi joonistab funktsioon
function reket(x,y,w,h){ this.x = x; this.y = y; this.w = w; this.h = h; this.draw = function(){ ctx.fillStyle = '#0000FF'; ctx.fillRect(this.x,this.y,this.w,this.h); } }
Reket luuakse muutujate definitsioonides (ka reket on muutuja!) käsuga
reket = new reket((wc-wr)/2,hc-hr,wr,hr);
Reketit liigutatakse nooleklahvidega. Klahvide seis salvestatkse massivis keysDown (muutujate deklaratsioonidele tuleb lisada selle loomine) ja dokumendi aknale lisatakse klahvivajutuste kuulajad:
window.addEventListener("keydown", function (e) { keysDown[e.keyCode] = true;}, false); window.addEventListener("keyup", function (e) { keysDown[e.keyCode] = false;}, false);
Mängu juhib taimeriga käivitatav kaadrivahetusfunktsioon animate. Lisame muutujate deklaratsioonide lõppus see käivitatakse ja see sooritab kõik mängu juhtivad tegevused; järgnevas on selle täiendatud tekst :
function animate(){ update(vr); pall.move(wc,hc); //update ei liiguta palli ctx.clearRect(0,0,wc,hc); pall.draw(); reket.draw(); drawtellised(); if (tellised.length > 0) setTimeout(animate,30); //mäng jätkub else showScore("Victory!!!"); //mäng on läbi }
Funktsioon update juhib reketi liikumist ja kontrollib palli kokkupõrkeid kivide ja reketiga. Kuna nüüd peab alt äärest palli tagasi põrgatama reket (mitte mänguvälja alumine äär!), tuleb palli liikumist juhtivas palli funktsioonist ball.move eemaldama kontrolli this.y+this.r+this.vy > h - see oli vaid palli liikumise esialgseks kontrolliks! :
function update(v) { if (keysDown[37]) { // Player holding left if (reket.x > 0) reket.x -= v; if (reket.x < 0) reket.x = 0; //peatub vasakus ääres } if (keysDown[39]) { // Player holding right if (reket.x + reket.w < wc ) //ei puutu paremat öäärt reket.x += v; if (reket.x > wc - reket.w) reket.x = wc - reket.w; } // kas pall puudutab tellist? for(var i=0;i < tellised.length;i++){ if (collision(pall,tellised[i])){ tellised.splice(i,1); //eemaldatakse i-s ++punkte;} } //kas pall põrkub reketilt? if (collision1(reket,pall)){ bouncingSound.cloneNode(true).play(); punkte ++; pall.vy *= -1;} //pall põrgatatakse tagasi //kas pall läks alt läbi? if (pall.y > hc) { pall.y = Math.floor(hc/4 + Math.random()*3*hc/4); //pall.h; pall.x = Math.floor(wc/4 + Math.random()*3*wc/4); punkte --;} inf.innerHTML = "Punkte: "+punkte; }; function collision(obj1,obj2){ return obj1.x <= (obj2.x + obj2.w) && obj2.x <= (obj1.x + obj1.w) && obj1.y <= (obj2.y + obj2.h) && obj2.y <= (obj1.y + obj1.h); }
Abifunktsioon showScore joonistab kanvaa keskele võiduteate:
function showScore(txt){ // Score ctx.fillStyle = "rgb(250, 50, 50)"; ctx.font = "24px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText(txt, wc/2, hc/2); //text, x,y };
Flatter - värin
Mängu jälgimisel ilmneb, et mõnikord ei põrku pall reketilt tagasi, vaid hakkab piki reketit sikk-sakiliselt liikuma. See juhtub, kui palli läheneb reketile otsast ja palli vertikaalkiirus vy on väike - sel juhul on pall ka pärast põrget (kiiruse vy suuna muutust - pall hakkab liikuma üles) uuesti reketiga kontaktis ja skript muudab taas palli suunda (alla), kuid nüüd on taas kokkupõrge ja kõik kordub, kuni pall lõpuks pääseb reketi teisest otsast välja.
Sellist liikumist võib välistada (näiteks) nii, et pärast esimest põrget palli kokkupõrget reketiga enam ei kontrollita niikaua kuni pall on jõudnud piisavalt kõrgele, näiteks kõrgemale kui reketi paksus+palli raadius. Lisame pallile veel ühe muutuja - active, mis pärast põrget saab väärtuse false, kuid kui pall on liikunud piisavalt kõrgele, saab see omadus väärtuse true (nüüd hakatakse taas palli ja reketi kokkupõrkeid kontrollima). See muutuja algväärtustatakse kohe pärast palli objekti loomist reaga
pall.active = true;Funktsioonis update kontrollitakse seda:
if (pall.active){ if (collision(reket,pall)){ ... else if (pall.y < pall.h + reket.h) pall.active = true;
Helid
Kõik mängud on meeldivamad, kui neis on ka helieffekte. HTML5-s on vahendid audioobjekti loomiseks, kuid brauserite tegijad pole veel saavutanud üksmeelt heli formaadis: näiteks Chrome ja Firefox tunnevad formaati .ogg - vaba ja hea kompressioon, s.t. väike fail ja jõuab arvutivõrgus kiiresti kohale; Internet Explorer seda ei mängi, selle jaoks peab olema .wav (suur fail) või .mp3 (väike fail, kuid formaadil on patent, s.t. koodeki eest peab maksma). Kõik brauserid on rahuldatud, kui heli pakutakse kolmes formaadis - .ogg, .wav ja .mp3 - brauser ise valib nende seast sellise, mida ta oskab mängida. Lisame mängu objektide loomisele ka kahe audioobjekti loomise; mõlemad pakuvad heli kolmes formaadis :
var bouncSnd = new Audio(); bouncSnd.src = "bounce.ogg"; bouncSnd.src = "bounce.mp3"; bouncSnd.src = "bounce.wav"; var breakSnd = new Audio(); breakSnd.src = "break.ogg"; breakSnd.src = "break.mp3"; breakSnd.src = "break.wav";
Mängus käivitatakse palli põrkumisel reketilt esimene heliobjekt bouncSnd; tellise purunemisel breakSnd; et heli saaks korduvalt kasutada (vajaduse korral ka mitu heli samaaegselt), tehakse sellest koopia, seega näiteks palli põrkumisele reketilt lisatakse:
bouncSnd.cloneNode(true).play();
Ülalesitatud skript joonistab palli lameda, tasapinnalise (2D) ringina. Pall näeb välja palju parem, kui see koonistatakse ruumilisena (3D), kasutades radiaalset gradienti - palli ülal-vasakul on veidi heledam laik (valguse peegeldus). Gradientide (liugvärvide - üks värv läheb sujuval üle teiseks) kirjeldamiseks peab määrama mitu värvi, siin vähemalt 2 - värv valguslaigu keskel ja värv palli äärel. Et need värvid oleks täpselt sama tooni (erinevus on vaid heleduses ja värviküllasuses), määrame värvitooni hue ja seda kasutades heledama-tumedama versioon HLS (Hue-Lightness-Saturation) värvisüsteemi abil:; sellise 3D palli joonistab abifunktsioon
function circle(x,y,r,c){ // create radial gradient var grd = ctx.createRadialGradient(x-r/5, y-r/5, r/4, x, y, r); var hsl_col1 = "hsl(" + c + ", 100%, 40%)"; var hsl_col2 = "hsl(" + c + ", 60%, 80%)"; grd.addColorStop(0, hsl_col2); //"#8ED6FF"); grd.addColorStop(1, hsl_col1); //"#004CB3"); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2, true); ctx.fillStyle = grd; //"rgb(0, 0, 255)"; ctx.fill(); ctx.closePath(); // add stroke ctx.lineWidth = 1; ctx.strokeStyle = hsl_col1; ctx.stroke(); //return ctx.getImageData(0,0,2*r,2*r); }
Palli konstruktorfunktsioonis muutub nüüd komponent draw() lihtsamaks:
this.draw = function(){ circle(this.x,this.y,this.r-1,this.col)};
Ülalvaadeldud mängus võib kergesti tekkida olukord, kus pall hakkab liikuma mööda üht ja sama trajektoori ja mängija ei saa lapiku reketiga palli trajektoori muuta.
Täiustame reketit - teeme sellele kaldu olevad otsad, millega saab palli trajektoori muuta; lihtsustamiseks on objektil reket endiselt vaid laius w ja kõrgus h, kaldu olevate otste pikkus on w/3, keskmise tasase osa pikkus - w/3. Sellise reketi konstruktorfunktsioon on:
function reket1(x,y,w,h){ this.x = x; this.y = y; this.w = w; this.h = h; this.draw = function(){ ctx.fillStyle = '#FF0000'; ctx.beginPath(); ctx.moveTo(this.x,this.y+this.h); ctx.lineTo(this.x+this.w/3,this.y); ctx.lineTo(this.x+2*this.w/3,this.y); ctx.lineTo(this.x+this.w,this.y+this.h); ctx.lineTo(this.x,this.y+this.h); ctx.closePath(); ctx.fill(); } }
Täiendame ka põrkumisfunktsiooni nii, et kaldu olevatelt otstelt pall põrkuks füüsikaseaduste järgi - langemisnurk võrdub peegeldumisnurgaga.
Kui põrkumistasapind on horisontaalne ja palli suund enne põrkumist oli tasapinna suhtes nurgaga a, siis pärast põrkumist see on - a; nurki loetakse X-telje positiivsest suunast alates kuni palli liikumissunani (vektorid!) .
Kui aga põrkumistasapind on horisontaaltasapinna (x-telg) suhtes pööratud nurga b võrra, pöördub selle võrra ka palli suund pärast põrkumist, s.t. see tuleb π - a + b. Suunanurga a ja x-y telgede suunaliste projektsioonide vx, vy vahel on lihtsad seosed:
a = Math.atan2(vy,vx); vx = Math.cos(a); vy = Math.sin(a);
Sellise kujuga reketi korral piirdekaste kasutav kokkupõrkefunktsioon enam ei toimi kaldotstel korrektselt, kasutame kokkupõrke leidmiseks reketi piksleid. Selleks lisame reketile pärast objekti defineerimist omaduse dat, kus on salvestatud reketi pikslid:
reket = new reket1((wc-wr)/2,hc-hr,wr,hr); reket.dat = getImDat(reket); function getImDat(obj){ ctx.clearRect(obj.x,obj.y,obj.w,obj.h); obj.draw(); return ctx.getImageData(obj.x,obj.y,obj.w,obj.h);}ja kirjeldame uue kokkupõrkefunktsiooni, mida nüüd funktsioonis update tuleb reketi ja palli kokkupõrke leidmiseks kasutada:
function collision1(obj1,obj2){ //obj1 - reket, obj2 - pall if (collision(obj1,obj2)){ //muidu pole mõtet piksleid kontrollida! var p = Math.floor(4*(obj2.x+obj2.vx-obj1.x + obj1.w * (obj2.y+obj2.vy-obj1.y))); // punkt (täisarv!) kus obj2 oleks järgmisel sammul return (obj1.dat.data[p+3] > 0);} else return false; }
Funktsioonis update tuleb nüüd arvutada palli uued kiiruse komponendid vx,vy. Keskmisel horisontaalsel osal saab endiselt kasutada vy *= -1, kuid kaldosadel tuleb veidi arvutada:
function update(v) { ... if (collision1(reket,pall)){ bouncingSound.cloneNode(true).play(); punkte ++; if (pall.x > reket.x+reket.w/4 && pall.x < reket.x+3*reket.w/4) //keskmine tasane osa pall.vy *= -1; else { if (pall.x < reket.x+reket.w/4 ) { //esimene kaldu osa
var a1 = Math.atan2(pall.vy,pall.vx);
var b = Math.atan2(-reket.h,reket.w/4);
var a2 = (Math.PI - a1 + b);
pall.vx = Math.cos(a2);
pall.vy = Math.sin(a2);
}
else { //viimane kaldu osa
var a1 = Math.atan2(pall.vy,pall.vx);
var b = Math.atan2(-reket.h,-reket.w/4); //
var a2 = (Math.PI - a1 + b);
pall.vx = Math.round(Math.cos(a2));
pall.vy = Math.round(Math.sin(a2));
}
}
Ülesandeid
1. Selle mängu võiks realiseerida mobiilile, millel on kallutusetundlikkus; reketi liigutamiseks tuleb mobiili vastavas suunas kallutada. Kuna reket nüüd liigub raskusjõu mõjul, hakkab see kallutuse suunas üha kiiremini liikuma ja selle kontrollimine on märksa raskem. Realiseeri versioon, kus kallutust imiteerivad nooleklahvid - klahvi all hoidmisel reketi liikumine kiireneb, vabastamisel hakkab aeglustuma.
2. Modifitseeri reketi põrgatamisfunktsiooni nii, et palli suunda mõjutaks ka reketi kiirus põrkumishetkel!
3. Tee reket, mida saab pöörata - näiteks klahvidega "z", "x" (proovi kõrval!)