Korja kiiresti 10 seent, aga hoidu kärbseseentest eemale!
Liikumine: nooleklahvid või AWDS

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 ?