HTML5 - Aliens Attack!


Tulista: 'x', liigu: nooleklahvid

Universumi Must Jõud (Dark Force) on klooninud Darth Vaderit ja saadab nüüd nende Dart-kloonide armee Universumi kõiki planeete vallutama; kleenuke Luke Skywalker peab elu eest võitlema, et armaadat tagasi lüüa...
( See on mängu http://strd6.com/space_demo/
parandatud-täiendatud variant)

Mängus tuleb tulistada ülalt massiliselt ilmuvaid vaenlasi; mängija (canvas-e allservas) saab liikuda vaid vasakule-paremale, kuid tulistada saab nii otse ples kui ka kaldu paremale-vasakule (nooleklahvid).

Taustapilt (space.png, 800x600 - canvas-e suurus) - Google pildiotsingust, alamkataloogi images; samasse alamkataloogi tulevad ka vaenlase ja kangelase pildid. Nende nimed peavad olema enemy.png, player.png

Heli võib paigutada alamkataloogi sounds. Heli võib laadida nii Javascriptiga (vt allpool skriptis) kui ka HTML5 märgendiga, lisades html-dokumend teksti heliobjekti loomise.

Erinevad brauserid oskavad erinevaid heliformaate, näiteks Chrome ei tunne .wav-heli, kuid tunneb .mp3-formaati, Firefox ja Opera taas tunnevad formaati .ogg, kuid mitte formaati .mp3, sellepärast peab heli olema kolmes formaadis .mp3 .ogg ja .wav; HTML -koodis pakutakse kolme formaati; koodeki näitamine pole kohustuslik, kuid on soovitav:

<audio>
<source src="sound/pauk.mp3" type='audio/mpeg; codecs="mp3"' />
<source src="sound/pauk.ogg" type='audio/ogg; codecs="vorbis"' />
<source src="sound/pauk.wav" type="audio/wav" />
</audio>

Html-lehe struktuur: peas on CSS-stiil (see lisab canvase taustapildi ja paigutab kanvaae lehe keskele (siin selles tutorialis on mänguväli paigutatud CSS-iga paremale äärde)

‹style type="text/css" media="screen">
    canvas { 
		display:block; 
		margin:1em auto; 
		background:url("images/space.png") 
		}
  ‹/style>
Html-dokumendi peas on ka abiscriptide sisselugemine -
<script  type="text/javascript" src="js/loadImages.js"></script>  
	<script  type="text/javascript" src="js/keys.js"></script> 

Need on skriptid, mida tuleb sageli kasutada ja mis sellepärast on otstarbekas salvestada eraldi failidena, siis on lihtne neid korduvalt kasutada. Piltide ja animeeritud spriidilehed laeb skript loadImages.js:


Spriidileht plahvatuse saamiseks
function loadImages(sources, callback) { var images = {}; var loadedImages = 0; var numImages = 0; // get num of sources for(var src in sources) { numImages++; } for(var src in sources) { images[src] = new Image(); images[src].onload = function() { if(++loadedImages >= numImages) { callback(images); } }; images[src].src = sources[src]; } } function spriteSheet(img,coords){ this.coords = coords; //coords = [[x0,y0,w0,h0],...[xn,yn,wn,hn]] this.sheet = img; this.draw = function(nr,x,y){ ctx.drawImage(img,this.coords[nr][0],this.coords[nr][1],this.coords[nr][2],this.coords[nr][3],x,y,this.coords[nr][2],this.coords[nr][3]); } this.draw0 = function(nr,x,y){ ctx0.drawImage(img,this.coords[nr][0],this.coords[nr][1],this.coords[nr][2],this.coords[nr][3],x,y,this.coords[nr][2],this.coords[nr][3]); } } function animated(img,coords,dx,dy,state,sp){ this.image = img; this.coords = coords; //console.log('koll: '+this.image.height); this.dx = dx; //this.width; this.dy = dy; //this.height/3; //32 this.activeFrame = 0; this.speed = sp; //how many *game* frames an animation frame is shown this.step = 0; this.y = 0; this.x = 0; this.state = state; this.setPosition = function(X, Y){ this.x = X; this.y = Y; } this.draw = function(){ if (this.state == "active"){ //console.log(this.step+','+this.activeFrame); ctx.drawImage(this.image,this.coords[this.activeFrame][0],this.coords[this.activeFrame][0],this.dx,this.dy,this.x,this.y,this.dx,this.dy); this.step ++; if (this.step == this.speed) { this.activeFrame++; this.step = 0;} if (this.activeFrame == this.coords.length){ this.step = 0; this.activeFrame = 0; this.state = "hidden"; ctx.clearRect(this.x,this.y,this.dx,this.dy); } } } }

Mäng käivitatakse (funktsioon initGame) alles pärast kõigi piltide edukat laadimist - muidu võivad tekkida olukorrad, kus mäng tahab juba pilti kasutada, aga see pole veel laetud.

Skript keys.js salvestab klaviatuuri olukorra - millised klahvid on hetkel alla vajutatud.

var keysDown = [];
window.addEventListener("keydown", function (e) {
	keysDown[e.keyCode] = true;
	}, false);

window.addEventListener("keyup", function (e) {
	keysDown[e.keyCode] = false;
}, false);

Keha algab mängu pealkirjaga (element H1) "Aliens Attack!" (vms) ja canvas definitsiooniga - laius 800px, kõrgus 550 (või 600), id="canvas".

Pärast lehe struktuuri loomist (peas - stiili, kehas canvas-e definitsioon) võib juba testida - lehe avamisel Firefox-is peaks ilmuma taustapildiga (lehe keskel) canvas.

Html-lehe lõpus (lehe kehas!) laetakse failist game.js mängu skript. Selle esimene osa laeb pildid - vaenlase (Dart Vader!), mängija ja käivitab siis mängu algväärtustamise:

window.onload = function() {
		
	var sources = {
	  enemy: "./images/enemy.png",
	  player: "./images/player.png",
	  explosion: "./images/explosion_sheet.png",
	 };
	loadImages(sources, initGame);
	}
//function initGame(images){ 
var initGame = function(images){
	var canvas = document.getElementById("canvas");
	CANVAS_WIDTH = canvas.width; //800;
	CANVAS_HEIGHT = canvas.height; //550;
	ctx = canvas.getContext("2d");	
	
	var FPS = 30; //Frames Per Second
	var FT = 1000/FPS; //Frame Time (milliseconds)
	var nexplosions = 40;
	bullets = [];
    explosion_snd = document.getElementById("snd");  //FF, Chrome, IE9, Opera, Dragon - OK !
	/*
	var explosion_snd =  new Audio(); //document.createElement('audio');
	explosion_snd.src = "./sound/pauk.ogg";
	explosion_snd.src = "./sound/pauk.wav";
	explosion_snd.src = "./sound/pauk.mp3";
	explosion_snd.preload= true; // make the audio file load silently
	//explosion_snd.volume= 10; // change volume
	//explosion_snd.currentTime= 5; // start playing starting at 5 seconds
	explosion_snd.load();
	*/ 	//IE9-s heliobjekti loomine Javascriptiga ei tööta !
	
Mängija on unikaalne (vaid üks eksemplar), selle jaoks pole klassi tarvis, defineerime ta objektina:
player = {
		x: CANVAS_WIDTH/2,
		y: CANVAS_HEIGHT-images.player.height, //täpselt kanvaa alläärel!
		sprite: images.player,
		width: images.player.width,
		height: images.player.height,
		draw:function(){
			ctx.drawImage(this.sprite,this.x, this.y);
		},
		shoot:function() {
		 // Sound.play("shoot");
			bullets.push(new bullet(this.x + this.width/2,this.y + this.height/2));
		},
		explode: function() {
		  this.active = false;
		  //  plahvatus !!
		  find_free(this.x,this.y);//otsime vaba plahvatuse !
		  explosion_snd.cloneNode(true).play();
		  
		}
	 };
	

Mängu objekte bullet (kuul) ja explosion (plahvatus) tuleb kasutada korduvalt, sageli ka samaaegselt. Plahvatus on animatsioon ja selle mängimine nõuab arvutilt tööd. Kui iga plahvatus (objekt, s.t. andmestruktuur RAM-is) luua alles siis, kui seda vajatakse, peab mänguprogramm selleks küsima operatsioonisüsteemilt mälu; kui plahvatus on lõppenud, peaks selle mälust kustutama (prahikoristus - carbage collect) ja tagastama mäluosa operatsioonisüsteemile. Selline mängu ajal toimuv mänguprogrammi 'vestlus' operatsioonisüsteemiga teeb mängu väga aeglaseks - ühe programmi (mänguprogramm) asemel peavad töötama ja omavahel andmeid vahetama kaks - mänguprogramm ja operatsioonisüsteem. Et mängu ajal poleks tarvis uusi objekte moodustada, genereeritakse mängu algul terve plahvatuste massiiv pikkusega nexplosions (parameeter, mida saab muuta). Kui plahvatust on tarvis, otsib funktsioon find_free(x,y) sellest massiivist esimese 'vaba' (s.t. selline, mis praegu ei mängi) ja käivitatab selle punktis (x,y). Kas plahvatus on vaba (seda saab käivitada) või mitte - seda peab meeles plahvatuse omadus active:

explosions = [];
	for (var i = 0; i < nexplosions; i++){
	var explosion = new animated(images.explosion,[[0,0],[64,0],[128,0],[192,0], [0,64],[64,64],[128,64],[192,64],[0,128],[64,128],[128,128],[192,128],[0,192],[64,192],[128,192],[192,192]],64,64,"hidden",4);
	explosions.push(explosion);
	}
	function find_free(x,y) {
		var i = 0;
		while(i < explosions.length && explosions[i].state == "active"){
			i++;
			}
		if (i < explosions.length) {
			explosions[i].x = x;
			explosions[i].y = y;
			explosions[i].state = "active";
			explosions[i].draw();
			}
		}  
   

Kui plahvatuste massiiv pole piisavaltt pikk, võib juhtuda, et mõnikord ei leitagi 'vaba' plahvatust (kõik plahvatused parajasti mängivad), kuid kuna plahvatuse animatsioon on suur ja kirju, siis mängija ühe puudumist ei märka.

Ka vaenlased salvestatakse massiivina, kuid neid ei genereerita kõiki kohe mängu alguses (see raiskaks asjatult aega), vaid juhuslikult, kuni nende arv saab täis; kui vaenlane on 'mängust väljas' (kas plahvatas või kadus mänguvälja alumise ääre taha), käivitatakse see uuesti:

enemies = [];
	
	function enemy() {
	  this.active = true;
	  this.age = Math.floor(Math.random() * 128);
	  this.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2; //w/4...w-w/4
	  this.y = 10;
	  this.vx = 0
	  this.vy = 2;
	
	  this.width = 32;
	  this.height = 32;
	
	  this.inBounds = function() {
		return this.x >= 0 && this.x <= CANVAS_WIDTH &&
		  this.y >= 0 && this.y <= CANVAS_HEIGHT;
	  };
	
	  this.sprite = images.enemy; //Sprite("enemy"); //faili nimi!
	
	  this.draw = function() {
		ctx.drawImage(this.sprite, this.x, this.y);
	  };
	
	  this.update = function() {
		this.x += this.vx;
		this.y += this.vy;
		this.vx = 3 * Math.sin(this.age * Math.PI / 64);
		this.age++;
		this.active = this.active && this.inBounds();
	  };
	
	  this.explode = function() {
		explosion_snd.cloneNode(true).play();
		find_free(this.x,this.y);
		this.y = -this.sprite.height;  //peidame ylemise ääre taha
	  };
	};

Ka kuule on korduvalt tarvis ja ka kuulid genereeritakse mängu algul massiivi, kuid kuulide puudumine (mängija tulistab, aga kuuli ei tule) oleks kohe märgatav, sellepärast on see massiiv muutuva pikkusega - tulistamisel genereeritakse uusi kuule, mitteaktiivsed eemaldatakse:

function bullet(x,y) {
		this.active = true;
		this.x = x;
		this.y = y;
		this.speed = 5;
		//this.vx = 0;
		this.vy = -this.speed;
		this.width = 3;
		this.height = 3;
		this.color = "#FAA";

		this.inBounds = function() { //kas kuul on mänguväljal
		return this.x >= 0 && this.x <= CANVAS_WIDTH &&
		  this.y >= 0 && this.y <= CANVAS_HEIGHT;
		};

		this.draw = function() {
			ctx.fillStyle = this.color;
			ctx.fillRect(this.x, this.y, this.width, this.height);
		};

		this.update = function() {
			//this.x += this.vx;
			this.y += this.vy;
			this.active = this.active && this.inBounds(); //kui kuul pole enam mänguväljal, pole ta aktiivne
		};

		this.explode = function() {
			this.active = false;  //ka vaenlast tabanud ja plahvatuse tekitanud kuul pole enam aktiivne
		// kuuli plahvatuse asemel plahvatab vaenlane!!!
		};
	}

Mängu põhifunktsioon animate käivitab algul kõigi objektide (muutunud) parameetrite arvutamise funktsiooni update() , siis joonistab ekraanile funktsiooniga draw() uue seisu ka käivitab stopperiga setTimeout enda uuesti:

animate();  //käivitame!
	
	function animate(){
		update();
		draw();
		setTimeout(animate,FT);
	}

Funktsioon update võimaldab mängijat liigutada nii noole- kui ka täheklahvidega 'a', 'd'. Kuna paljud brauserid reageerivad nooleklahvidele (näiteks hakkavad skrollima - Chrome), on nooleklahvid dubleeritud - vasak nool klahviga 'a', parem - klahviga 'd' - need on üldlevinud liigutamisklahvid ja nende kasutamisel saab mängida ühe käega - ka tulistamisklahv 'x' on lähedal:

function update() {
	  if (keysDown[88]) { //'x'
		player.shoot();
	  }
	
	  if (keysDown[37] || keysDown[65]) { // <- or 'a'
		player.x -= 5;
	  }
	
	  if (keysDown[39]|| keysDown[68]) { // -> or 'd') 
		player.x += 5;
	  }
	
	  player.x = clamp(player.x, 0, CANVAS_WIDTH - player.sprite.width); //ei lase mängijat kanvaalt välja
	  
	  bullets.forEach(function(b) {
		b.update();
		 });
	
	  bullets = bullets.filter(function(b) { //mitteaktiivsete kuulide eemaldamine
		return b.active;
	  });
	  
	  enemies.forEach(function(e) {
		e.update();
	  });
	
	  enemies = enemies.filter(function(e) {
		return e.active;
	  });
	
	  handleCollisions();
	
	  if((enemies.length < 20) && (Math.random() < 0.1) ) {
		enemies.push(new enemy());  //lisame juhuslikult vaenlasi kuni < 20
	  }
	 
	}
	
	function draw() {
	  ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
	  player.draw();
	 
	  bullets.forEach(function(b) {
		b.draw();
	  });
	
	  enemies.forEach(function(e) {
		e.draw();
	  });
	  explosions.forEach(function(explosion) {
		explosion.draw();
	  });
	}
	//kokkupõrked piirdekasti abil!
	function collision(a, b) {
	  return a.x < b.x + b.width &&
		a.x + a.width > b.x &&
		a.y < b.y + b.height &&
		a.y + a.height > b.y;
	}
	
	function handleCollisions() {
	  bullets.forEach(function(bullet) {
		enemies.forEach(function(enemy) {
		  if(collision(bullet, enemy)) {
			enemy.explode();
			bullet.active = false;
		  }
		});
	  });
	
	  enemies.forEach(function(enemy) {
		if(collision(enemy, player)) {
		  enemy.explode();
		  player.explode();
		}
	  });
	}
	
	
	function clamp(x,min,max){  //piirame muutuja x väärtuse rajadesse min..max
	if (x < min) x = min;
	if (x > max) x = max;
		return x;}
	}

Ja ongi kõik...


Ülesandeid.
1. Kangelasel näib olevat mõlemas käes relv - pane ta kahe käega ( vaheldumisi) tulistama !
2. Lisa punktide arvutamine ja ekraanil (mängu kõrval või all - div 'inf') näitamine.


© Jaak Henno

Küsimused, probleemid : e-mail