Talibanid on teada saanud, et järgmine oht neile tuleb kõrge tehnoloogilise tasemega Tallinna Tehnikaülikoolist ja siin ei saa kasutada lennukitega ründamise taktikat - Estonian Air kukub enne kokku kui nad oma rünnakuplaanid valmis saavad. Sellepärast kutsusid nad appi Universumi Mustad Jõud (Dark Force), kes kloonisid Darth Vaderi ja saadab nüüd nende Darth-kloonide armee TTU-d hävitama/vallutama; kuid Juuljus astub neile vapralt vastu...
Mängus tuleb tulistada ülalt massiliselt ilmuvaid vaenlasi; mängija (kanvaa allservas) saab liikuda vaid vasakule-paremaleja tulistada üles (nooleklahvid, 'X').
Taustapilt salvestatakse alamkataloogis images; samasse alamkataloogi tulevad ka vaenlase ja kangelase pildid. Nende nimed peavad olema enemy.png, player.png
Helid
Heli võib paigutada alamkataloogi sounds. Heli võib laadida nii Javascriptiga (vt allpool skriptis, välja kommenteeritud) kui ka HTML5 märgendiga, lisades html-dokumend teksti heliobjekti loomise.
Erinevad brauserid oskavad mängida 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>
Heli laadimine html-koodis näib olevat parem kui laadimine Javascriptiga (analoogiline piltide laadimisega) - brauser teab, millist heliformaati ta vajab, Javascriptis peaks olema lisatud brauseri kontroll.
HTML5-lehe struktuur
Html-lehe struktuur: peas on CSS-stiil (see lisab kanvaa taustapildi ja paigutab kanvaa 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/space0.png") } ‹/style>
Skriptiutiliidid
Korduvalt kasutatavad skriptid (näiteks piltide laadimine enne mängu algamist) on kasulik vormistada eraldi skriptifailidena; 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:
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); } } } }
Spriidileht plahvatuse saamiseks
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, 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.
Mängu initsialiseerimine
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/Juuljus.png", explosion: "./images/explosion_sheet.png", }; loadImages(sources, initGame); } //function initGame(images){ var initGame = function(images){ var canvas = document.getElementById("canvas"); w0 = canvas.width; //800; h0 = 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: w0/2, y: h0-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"); - lisa käe valik - parem/vasak 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älu ökonoomne kasutamine
Mängu objekte 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. Väga kergesti tekib ka mäluhäving (memory leak) - programm ei tagasta alati kasutud mälus loodud objekti mälu ja tulemusena hakkab programmi poolt kasutatav mälu 8piiramatult) kasvama, kui brauser 'kukub kokku'; programmi mälukasutust on (Windows-is) lihtne jälgida: CTRL+ALT+Del - ' Task Manager'.
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 = w0 / 4 + Math.random() * w0 / 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 <= w0 && this.y >= 0 && this.y <= h0; }; 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); //paneb laineliselt liikuma 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 <= w0 && this.y >= 0 && this.y <= h0; }; 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, w0 - 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, w0, h0); 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... Mängija on võitnud, kui ta tapab 1000 vaenlast (rohkem pole isegi Talibanidel kusagilt võtta)
Ülesandeid.
1. Kangelasel on mõlemas käes relv - pane ta kahe käega ( vaheldumisi) tulistama; kuulid peaks välja lendama enam-vähem) käes oleva toru otsast selle suunas !
2.Lisa mängija edukuse (kui palju vaenlasi on tapetud) ja tervise arvestus - kui vaenlane põrkab mängijaga kokku, peab selle tervis vähenema 10% ja kui tervis on 0, on mängija kaotanud; tervis näidatakse punase kriipsuga mängu vasakus nurgas
3. Kui vaenlasi tapetakse, muutuvad nad tigedamaks ja hakkavad kiiremini allapoole liikuma - täienda ! Allalangemise kiirusega saab määrata ka mängu raskusastme.
© Jaak Henno