Kõige lihtsam HTML5 mäng: seenekorjamine!

NB! Kui IE ei näita HTML5 dokumenti standartsel viisil (vajuta F12 - kas silumismenüüs on "IE9 standards"), tuleb dokumendi päisesse lisada rida
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

See brauser ei tunne objekti canvas!
Lae endale uuem brauser!

Klõpsa mänguväljal; liikumine - nooleklahvid!
Reload, kui mäng on läbi (peatub)!
Ressursid

Selles loengus esitatakse:

Selles mängus peab mängija võimalikult ruttu korjama mänguväljalt seeni.

Seened kasvavad metsas; kanvaale pruuni tausta andmiseks on html-lehele lisatud CSS-stiil (selle võib lisada näiteks html-lehe päisesse):

<style type="text/css" media="screen">
		canvas { 
			background:url("images/ground.png") 
		}
 </style>

Stiililehega tausta kujundamisel värvitakse taust brauseris vaid üks kord ja see ei kustu kogu kanvaa puhastamisel käsuga clearRect funktsioonis render (allpool). Stiili määramisel võib stiililehes kasutada objekti (canvas), kui kui kanvaasid on mitu, võib kasutada objekti id-d, näiteks kanvaa kirjelduse (nagu sellel lehel, vt. CTRL-U-ga selle dokumendi html-koodi!)

<canvas id="gamecanvas" width="320" height="256">

puhul võib ülalesitatud stiilikirjelduses sõna canvas asendada sõnaga #gamecanvas - sümbol # stiililehel näitab, et kirjeldatakse id-ga määratud html-lehe objekti.

Mängus kasutatakse kolme serverilt laetud pildikest (spriiti) - puu, mängija ja seen. Kuna serverilt resursside (piltide, helide) laadimise kiirus sõltub arvutivõrgust ja operatsioonisüsteemist (asünkroonne tegevus, mille täitmise aega pole võimalik ette teada), võib mänguprogrammi käivitada alles siis, kui kõik ressursid on laetud, muidu võib mäng 'kokku joosta' - programm tahab ekraanile joonistada pilti, mida pole kettalt veel jõutud laadida ja ilmub liivakell ja veateade (eri brauserites ja arvutites võib käitumine olla erinev).

Kettalt laadimine toimub protseduuriga loadImages; see on mängu Javascipt-teksti game.js algus. Enne funktsiooni loadImages kirjeldamist on kirjeldatud objekt sources, milles loetletakse laaditavad failid ja antakse neile nimed; need on objekti sources omadused (properies), failinimed on nende omaduste väärtused. Javascripti objekti tunnuseks on figuursulud { } objekti ümber; objekt koosneb komadega eraldatud paaridest omadus:väärus, seega objektil sources on kolm omadust, mille väärtusteks on vastavad failinimed. Objekti omaduste väärtuse saab punkti kasutamisega, näit sources.treeSprite = "images/tree.png"

Kuna objekt sources on kirjeldatud enne funktsiooni loadImages, on muutuja sources globaalne ja siin määratud failide/ressursside nimesid treeSprite, heroSprite, seenSprite saab kasutada ka funktsiooni loadImages poolt käivitatavas mänguprogrammis initGame::

var sources = {
            treeSprite: "images/tree.png",
            heroSprite: "images/hero.png",
            seenSprite: "images/seen.png"
        };
window.onload = function() {
        loadImages(sources, initGame);  // calls initGame after *all* images have finished loading
    };

    function loadImages(sources, callback) {
        var images = {};
        var loadedImages = 0;
        var numImages = 0;
        for (var src in sources) {
			//console.log(src);
            numImages++;
        }
        for (var src in sources) {
            images[src] = new Image();
            images[src].onload = function(){
                if (++loadedImages >= numImages) {
                    callback(images);
                }
            };
            images[src].src = sources[src]; 
        }
    } 

Nüüd võib kirjeldada funktsiooniga initGame kogu mängu, kuid enne funktsiooni kirjeldamist deklareeritakse globaalmuutujad - paljud tegevused on kirjeldatud eraldi funktsioonidena, kus neid muutujaid vajatakse.

Klaviatuuri klahvide koodid:
http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
var seeni = 10; //how many player has to collect var w,h,d0,h0,r,v,ctx,treeImage,hero,seen,collected,keysdown,then; var startTime = Date.now(); function initGame(images) { // Initialize the canvas var canvas = document.getElementById("gamecanvas"); ctx = canvas.getContext("2d"); w=canvas.width; h=canvas.height; console.log('w,h '+w+h); //Tree treeImage = images.treeSprite; d0 = treeImage.width; //tree width h0 = treeImage.height; v = Math.floor(w/d0); //number of columns r = Math.floor(h/d0); //number of rows //console.log('v,r '+v+r); // Game objects hero = { sprite: images.heroSprite, x: Math.floor(w/2), //to center y: Math.floor(h/2), speed: 2 // movement in pixels }; seen = { sprite: images.seenSprite, x: images.seenSprite.width + (Math.random() * (w - 2*images.seenSprite.width)), y: images.seenSprite.height + (Math.random() * (h - 2*images.seenSprite.height)) } collected = 0; // Handle keyboard controls keysDown = {}; addEventListener("keydown", function (e) { keysDown[e.keyCode] = true; }, false); addEventListener("keyup", function (e) { delete keysDown[e.keyCode]; }, false); // Let's play this game! drawtrees(); startTime = Date.now(); game(); }

Nagu eelnevas näha, on mängus vaid kaks objekti: mängija (hero) ja seen (see on äärmiselt lihtne mäng!); mõlematel on omadused sprite (pildike, mida kanvaal näidatakse) ja objekti koordinaadid x,y. Mängija juhtimine toimub nooleklahvidega, sündmuse keydown kuulaja (anonüümne, nimeta funktsioon, mis kirjeldatakse otse sündmuse kuulaja definitsioonis) salvestab vastava klahvi seisu objekti keysDown vastava omadusena; sündmuse keyup kuulajas reageeriv funktsioon eemaldab selle väärtuse, seega saab iga hetk (mängu igas kaadris) kontrollida klahvide seisu.

Funktsiooni initGame viimane rida käivitab funktsiooni main - see on mängu kaadrivahetusmehhanism (sama kui Flashis sündmuse enterFrame kuulaja) - see funktsioon käivitab end taimeriga ühe millisekundi järel uuesti, kui on veel korjamata seeni; kui kõik seened on korjatud, funktsiooni enam ei käivitata:

 function game () {
	if (collected < seeni) {
		update();
		render();
		setTimeout(game,30);  //repeat
	}
	else {
		var time=Math.round(((Date.now()-startTime)/1000));
		showScore("Korjasid "+time+ " sekundiga " + collected+" seent!");
	}
};

Jääb üle kirjeldada veel mitmesugused abifunktsioonid. Neist tähtsaim on objektide (kangelane ja seen) kokkupõrget arvutav protseduur collision (Flash-is on see süsteemne, s.t. sisseehitatud). Kokkupõrke arvutamisel kasutatakse piirdenelinurki (bounding box): kui need lõikuvad, toimub kokkupõrge. See on veidi ebatäpne, kuid kõige lihtsam ja kiirem meetod ja toimib väiksemate figuuride puhul väga hästi. Protseduur kontrollib, kas ühe nelinurga mingi tipp asub teise objekti sees, näiteks kaks esimest rida kontrollivad, et objekti obj1 piirdenelinurga vasaku ülanurga x-koordinaat obj1.x on objekti obj2 vasaku x-koordinaadi obj2.x ja parema koordinaadi obj2.x + obj2.sprite.width vahel; viimased kaks rida kontrollivad sama y-koordinaatide jaoks:

function collision(obj1,obj2){
return obj1.x <= (obj2.x + obj2.sprite.width) && 
		obj2.x <= (obj1.x + obj1.sprite.width) &&
		obj1.y <= (obj2.y + obj2.sprite.height) &&
		obj2.y <= (obj1.y + obj1.sprite.height);
}

Abiprotseduur drawtrees joonistab mänguväljaku ümber puud:

function drawtrees(){
			
	for (var i=0; i < v; i++){
		ctx.drawImage(treeImage,i*d0,0);
		ctx.drawImage(treeImage,i*d0,h-d0);
		}
	for (var j=1;j < r;j++){
		ctx.drawImage(treeImage,0,j*d0);
		ctx.drawImage(treeImage,w-d0,j*d0);
		}
}

Protseduur update kontrollib, millised nooleklahvid on parajasti alla vajutatud ja muudab vastsvalt sellele mängija koordinaate hero.x, hero.y; kokkupõrke korral suurendatakse kogutud ssente arvu collected, protseduur reset paigutab mängija hero ja seene uuesti ja protseduur render joonistab ekraanile uue seisu (kaadri):

// Update game objects
function update() {
	if (38 in keysDown) { // Player holding up arrow
	if (hero.y > 0)
		hero.y -= hero.speed ;
		if (hero.y < d0) //can not go through trees!
			hero.y = d0;
	}
	if (40 in keysDown) { // Player holding down
	if (hero.y < h - d0-hero.sprite.height)
		hero.y += hero.speed ;
		if (hero.y > h - d0-hero.sprite.height)
			hero.y = h - d0-hero.sprite.height;
	}
	if (37 in keysDown) { // Player holding left
	if (hero.x > 0)
		hero.x -= hero.speed ;
		if (hero.x < d0)
			hero.x = d0;
	}
	if (39 in keysDown) { // Player holding right
	if (hero.x < w - d0-hero.sprite.width)
		hero.x += hero.speed ;
		if (hero.x > w - d0-hero.sprite.width)
			hero.x = w - d0-hero.sprite.width;
	}

	// Are they touching?
	if (collision(hero,seen)){
		++collected;
		reset();
	}
};

// Draw everything
function render() {
	clearRect(0,0,w,h); //kanvaa puhtaks!
	drawtrees();
	ctx.drawImage(hero.sprite, hero.x, hero.y);
	ctx.drawImage(seen.sprite, seen.x, seen.y);
	showScore("Seeni: " + collected);
}
function showScore(txt){
	// Score
	ctx.fillStyle = "rgb(250, 50, 50)";
	ctx.font = "14px Arial";
	ctx.textAlign = "left";
	ctx.textBaseline = "top";
	ctx.fillText(txt, d0, d0); //text, x,y
};
var reset = function () {
	//Put hero to the center
	hero.x = w / 2;
	hero.y = h / 2;
	// Put the mushroom somewhere on the screen randomly
    // check that seen is placed away from hero!
    do{
		seen.x = d0 + (Math.random() * (w - 2*d0));
		seen.y = d0 + (Math.random() * (h - 2*d0));
    } while (collision(seen,hero));
};

Ylesandeid

1. Kui mäng käivitub brauseris, saab kasutaja klahvivajutused operatsioonisüsteemilt ka brauser. Paljud brauserid püüavad neid kohe interpreteerida/täita, näiteks 'nool alla' klahv võib käivitada lehe skrollimise allapoole; loomulikult pole see mängus soovitav. Asenda nooleklahvidega liikumine klahvide 'A' (kood: 65), 'W' (87), 'S' (83), 'D' (63) kasutamisega!

2. Paiguta mänguväljale ka juhuslikkudesse kohtadesse puid, kuid nii, et puud, seen ja kangelane ei puutuks üksteist (vt seene paigutamist funktsioonis reset).

3. Paiguta korraga mitu seent (juhuslik arv ); mänguvälja uuendamine (reset) võib toimuda alles siis kui kõik seened on korjatud

4. Praegune kokkupõrkefunktsioon (kangelase ja seene piirdenelinurgad lõikuvad) on liiga "lofka" - asenda see funktsiooniga, kus kangelase keskpunkt peab lõikuma seene piirdenelinurgaga.