Вы находитесь на странице: 1из 13

Aprende a crear juegos en HTML5 Canvas

lunes, 14 de mayo de 2012

Hojas de sprites y animaciones


Ahora que tenemos la base para un juego completo, pasaremos a agregar las imgenes. Anteriormente ya vimos como agregar imgenes a un juego, as que aprovechar este momento para ensearles una tcnica ms avanzada: Las hojas de sprites. Una hoja de sprites (Spritesheet) es una imagen grande que contiene varios sprites en la misma, tal como la imagen siguiente que es la que usaremos en el presente ejemplo:

La ventaja de las hojas de sprites sobre las imgenes individuales, es que la hoja de sprites pesa menos que la suma de todas las imgenes individuales, lo que es una ventaja especial en un juego web, por que significa menos ancho de banda consumido, menos llamadas al servidor y una descarga ms rpida. HTML5 Canvas ya viene con un mtodo avanzado para dibujar secciones de una imagen sobre nuestro canvas, lo que nos permite usar hojas de sprites con mucha facilidad. Para usar esta forma, hay que pasar nueve parmetros a la funcin drawImage: ctx.drawImage(image, sX,sY,sWidth,sHeight, dX,dY,dWidth,dHeight); El primer parmetro, es la imagen que tiene nuestra hoja de sprites. Los siguientes cuatro, indican el rea de nuestra imagen que ser dibujada (source), y los ltimos cuatro, indican el rea en nuestro canvas donde ser dibujada esta imagen (destiny). Prosigamos al ejemplo prctico. Comencemos declarando una variable imagen y asignando su fuente a nuestra imagen: var spritesheet=new Image(); spritesheet.src='spritesheet.png'; Despus, vayamos a donde dibujamos nuestro jugador, y sustituyamos el rectngulo verde, dibujando la primer imagen de nuestra hoja de sprites. La nave est en en la posicin 0,0 de nuestra hoja de sprites, y tiene una altura y anchura de 10x10 pixeles, por tanto, la funcin quedara as:

ctx.drawImage(spritesheet,0,0,10,10,player.x,player.y,player.width,player.height ); Prosigamos con las naves enemigas. La cuarta imagen de nuestra hoja de sprites es la nave enemiga, y la quinta, la nave totalmente en blanco, que representa la nave enemiga cuando est siendo daada. Ambas tienen 10x10 pixeles de alto y ancho, y se encuentran en las posiciones

30,0 y 40,0 respectivamente. Por tanto, las funciones para dibujarlas quedaran de esta forma: for(i=0,l=enemies.length;i<l;i++){ if(enemies[i].timer%2==0) // ctx.fillStyle='#00f';

ctx.drawImage(spritesheet,30,0,10,10,enemies[i].x,enemies[i].y,enemies[i].width, enemies[i].height); else // ctx.fillStyle='#fff';

ctx.drawImage(spritesheet,40,0,10,10,enemies[i].x,enemies[i].y,enemies[i].width, enemies[i].height); //ctx.fillRect(enemies[i].x,enemies[i].y,enemies[i].width,enemies[i].height); } Ahora que ya comprendemos la forma en que funciona esta funcin, hagamos lo mismo para las mejoras: for(i=0,l=powerups.length;i<l;i++){ if(powerups[i].type==1) // ctx.fillStyle='#f90';

ctx.drawImage(spritesheet,50,0,10,10,powerups[i].x,powerups[i].y,powerups[i].wid th,powerups[i].height); else // ctx.fillStyle='#cc6';

ctx.drawImage(spritesheet,60,0,10,10,powerups[i].x,powerups[i].y,powerups[i].wid th,powerups[i].height); //ctx.fillRect(powerups[i].x,powerups[i].y,powerups[i].width,powerups[i].height) ; } Y para los disparos: for(i=0,l=shots.length;i<l;i++) //ctx.fillRect(shots[i].x,shots[i].y,shots[i].width,shots[i].height); ctx.drawImage(spritesheet,70,0,5,5,shots[i].x,shots[i].y,5,5); Nota que los disparos tienen una altura y anchura de 5x5 pixeles, a diferencia de los dems elementos que hemos dibujado antes. Para hacer el juego ms jugable, he bajado la velocidad de las naves enemigas y de las mejoras

de 10 a 5 pixeles por turno, as como agregado 4 naves al comienzo del juego. Probemos el juego, y veremos como cada imagen de nuestra hoja de estilos es dibujada correctamente en su posicin del juego. Otra ventaja que presentan las hojas de sprites, es que permiten crear animaciones de forma sencilla. Como podrs ver en nuestra hoja de sprites, esta ya est preparada para agregar animaciones a nuestro jugador y a los disparos. Para crear animaciones, primero se necesita un contador que nos indique la animacin por dibujar. Declaramos una variable para ello al comienzo de nuestro juego: var aTimer=0; Posteriormente, se agrega el contador al ciclo, sumndose en uno cada turno mientras el juego no est en pausa. Para mantener mayor control sobre este contador, cuando supere el valor de 360, restaremos ese valor sobre el mismo: aTimer++; if(aTimer>360) aTimer-=360; Se elige 360, por que es el mnimo nmero divisible entre los nmeros del 1 al 10. Si tienes una animacin de ms de 10 imgenes, ten cuidado con este ciclo, pues puede crear saltos inesperados en dicha animacin. Este valor puedes cambiarlo por el que creas conveniente, pero alerta a estos detalles sobre los objetos animados. Ahora, aplicaremos la animacin a nuestros objetos por medio de este contador. Para obtener la imagen de la animacin a dibujar, sacamos el mdulo de nuestro contador entre la cantidad de imgenes que tiene nuestra animacin, y este valor lo multiplicamos por el alto o el ancho de cada imagen, ya sea que estn dispuestos de forma vertical u horizontal dentro de nuestra hoja de estilos. Por ejemplo si tuvieramos una animacin de 6 imgenes, la formula quedara de esta forma: (aTimer%6)*10 Y este valor se suma al origen en X o Y, segn corresponda si estn dispuestos de forma horizontal o vertical. En el caso de nuestra nave, son tres imgenes dispuestas de forma horizontal con 10 pixeles de ancho, por tanto el dibujado quedara de esta forma:

ctx.drawImage(spritesheet,0+(aTimer%3)*10,0,10,10,player.x,player.y,player.width ,player.height); En el caso de los disparos, son dos imgenes dispuestas de forma vertical con 5 pixeles de alto, por tanto, se dibujaran de esta forma:

ctx.drawImage(spritesheet,70,0+(aTimer%2)*5,5,5,shots[i].x,shots[i].y,5,5); En los dos casos previos, dio la casualidad que el origen al que se sume el valor es 0, y como sumar un valor a 0 es redundante, podemos eliminar esta parte. Pero nota que la mayora de las veces, la animacin no comenzar en 0, as que debers sumar su origen correspondiente. Si deseas hacer ms lenta una animacin, la formula es ligeramente ms complicada. Primero, hay que dividir el valor de nuestro contador entre la cantidad de pausas que se desea hacer, despus

este valor se convierte a un entero con la funcin "parseInt". Posteriormente ya se saca el mdulo de la cantidad de imgenes que tiene nuestra animacin, y este valor lo multiplicamos por el alto o ancho de cada imagen, segn su disposicin. Por ejemplo, si quisieramos hacer que nuestra animacin de 6 imgenes fuera 4 veces ms lenta, la formula quedara de esta forma: (parseInt(aTimer/4)%6)*10 No aplicar esta formula a ninguna animacin del presente juego, pero considero que es una buena herramienta saber como hacer dicho efecto para cuando lo necesiten en sus futuros juegos. Con esto, hemos aplicado una hoja de estilos a nuestro juego, y la base ha quedado completa. As concluimos con el conocimiento para hacer un segundo juego, y con este, puedes seguir aplicando lo que has aprendido para mejorar y personalizar tu propio juego de naves. Gracias por acompaarme una vez ms en este sitio, y como siempre, cualquier duda o comentario, estoy en mi mejor disposicin para ayudarles. Felices cdigos!

Codigo Final

'use strict'; window.addEventListener('load',init,false); var canvas=null,ctx=null; var lastPress=null; var PRESSING=[]; var PAUSE=true; var GAMEOVER=true; var score=0; var multishot=1; var aTimer=0; var player=new Rectangle(70,280,10,10,0,3); var shots=[]; var enemies=[]; var powerups=[]; var messages=[]; var spritesheet=new Image(); spritesheet.src='spritesheet.png';

function random(max){ return ~~(Math.random()*max); }

function init(){ canvas=document.getElementById('canvas'); canvas.style.background='#000'; ctx=canvas.getContext('2d'); run(); }

function run(){ setTimeout(run,50); game(); paint(ctx); }

function reset(){ score=0; multishot=1; player.x=70; player.y=280; player.health=3; player.timer=0; shots.length=0; enemies.length=0; powerups.length=0; messages.length=0; enemies.push(new Rectangle(10,0,10,10,0,2)); enemies.push(new Rectangle(50,0,10,10,0,2)); enemies.push(new Rectangle(90,0,10,10,0,2)); enemies.push(new Rectangle(130,0,10,10,0,2)); GAMEOVER=false; }

function game(){ if(!PAUSE){ // GameOver Reset if(GAMEOVER) reset();

// Move Player //if(PRESSING[38]) //UP // player.y-=10;

if(PRESSING[39]) //RIGHT player.x+=10; //if(PRESSING[40]) //DOWN // player.y+=10;

if(PRESSING[37]) //LEFT player.x-=10;

// Out Screen if(player.x>canvas.width-player.width) player.x=canvas.width-player.width; if(player.x<0) player.x=0;

// New Shot if(lastPress==32){ if(multishot==3){ shots.push(new Rectangle(player.x-2,player.y+2,4,4)); shots.push(new Rectangle(player.x+4,player.y,4,4)); shots.push(new Rectangle(player.x+10,player.y+2,4,4)); } else if(multishot==2){ shots.push(new Rectangle(player.x,player.y,4,4)); shots.push(new Rectangle(player.x+6,player.y,4,4)); } else shots.push(new Rectangle(player.x+3,player.y,4,4)); lastPress=null; }

// Move Shots for(var i=0,l=shots.length;i<l;i++){ shots[i].y-=10; if(shots[i].y<0){ shots.splice(i--,1);

l--; } }

// Move Messages for(var i=0,l=messages.length;i<l;i++){ messages[i].y+=2; if(messages[i].y<260){ messages.splice(i--,1); l--; } }

// Move PowerUps for(var i=0,l=powerups.length;i<l;i++){ powerups[i].y+=5; // Powerup Outside Screen if(powerups[i].y>canvas.height){ powerups.splice(i--,1); l--; continue; }

// Player intersects if(player.intersects(powerups[i])){ if(powerups[i].type==1){ // MultiShot if(multishot<3){ multishot++; messages.push(new message('MULTI',player.x,player.y)); } else{ score+=5; messages.push(new message('+5',player.x,player.y)); } } else{ // ExtraPoints score+=5;

messages.push(new message('+5',player.x,player.y)); } powerups.splice(i--,1); l--; } }

// Move Enemies for(var i=0,l=enemies.length;i<l;i++){ if(enemies[i].timer>0) enemies[i].timer--;

// Shot Intersects Enemy for(var j=0,ll=shots.length;j<ll;j++){ if(shots[j].intersects(enemies[i])){ score++; enemies[i].health--; if(enemies[i].health<1){ enemies[i].x=random(canvas.width/10)*10; enemies[i].y=0; enemies[i].health=2; enemies.push(new Rectangle(random(canvas.width/10)*10,0,10,10,0,2)); } else{ enemies[i].timer=1; } shots.splice(j--,1); ll--; } }

enemies[i].y+=5; // Enemy Outside Screen if(enemies[i].y>canvas.height){ enemies[i].x=random(canvas.width/10)*10; enemies[i].y=0;

enemies[i].health=2; }

// Player Intersects Enemy if(player.intersects(enemies[i])&&player.timer<1){ player.health--; player.timer=20; }

// Shot Intersects Enemy for(var j=0,ll=shots.length;j<ll;j++){ if(shots[j].intersects(enemies[i])){ score++; enemies[i].health--; if(enemies[i].health<1){ // Add PowerUp var r=random(20); if(r<5){ if(r==0) // New MultiShot

powerups.push(new Rectangle(enemies[i].x,enemies[i].y,10,10,1)); else // New ExtraPoints

powerups.push(new Rectangle(enemies[i].x,enemies[i].y,10,10,0)); } enemies[i].x=random(canvas.width/10)*10; enemies[i].y=0; enemies[i].health=2; enemies.push(new Rectangle(random(canvas.width/10)*10,0,10,10,0,2)); } else{ enemies[i].timer=1; } shots.splice(j--,1); ll--; } }

// Damaged if(player.timer>0) player.timer--;

// GameOver if(player.health<1){ GAMEOVER=true; PAUSE=true; } } // Pause/Unpause if(lastPress==13){ PAUSE=!PAUSE; lastPress=null; } }

function paint(ctx){ ctx.clearRect(0,0,canvas.width,canvas.height); //ctx.fillStyle='#0f0'; if(player.timer%2==0) //ctx.fillRect(player.x,player.y,player.width,player.height); ctx.drawImage(spritesheet,(aTimer%3)*10,0,10,10,player.x,player.y,player.width,p layer.height); for(var i=0,l=powerups.length;i<l;i++){ if(powerups[i].type==1) // ctx.fillStyle='#f90';

ctx.drawImage(spritesheet,50,0,10,10,powerups[i].x,powerups[i].y,powerups[i].wid th,powerups[i].height); else // ctx.fillStyle='#cc6';

ctx.drawImage(spritesheet,60,0,10,10,powerups[i].x,powerups[i].y,powerups[i].wid th,powerups[i].height);

//ctx.fillRect(powerups[i].x,powerups[i].y,powerups[i].width,powerups[i].height) ; } for(var i=0,l=enemies.length;i<l;i++){ if(enemies[i].timer%2==0) // ctx.fillStyle='#00f';

ctx.drawImage(spritesheet,30,0,10,10,enemies[i].x,enemies[i].y,enemies[i].width, enemies[i].height); else // ctx.fillStyle='#fff';

ctx.drawImage(spritesheet,40,0,10,10,enemies[i].x,enemies[i].y,enemies[i].width, enemies[i].height); //ctx.fillRect(enemies[i].x,enemies[i].y,enemies[i].width,enemies[i].height); } //ctx.fillStyle='#f00'; for(var i=0,l=shots.length;i<l;i++) //ctx.fillRect(shots[i].x,shots[i].y,shots[i].width,shots[i].height); ctx.drawImage(spritesheet,70,(aTimer%2)*5,5,5,shots[i].x,shots[i].y,5,5);

ctx.fillStyle='#fff'; for(var i=0,l=messages.length;i<l;i++) ctx.fillText(messages[i].string,messages[i].x,messages[i].y); ctx.fillText('Score: '+score,0,20); ctx.fillText('Health: '+player.health,100,20); //ctx.fillText('Last Press: '+lastPress,0,20); //ctx.fillText('Shots: '+shots.length,0,30); if(PAUSE){ ctx.textAlign='center'; if(GAMEOVER) ctx.fillText('GAME OVER',75,150); else ctx.fillText('PAUSE',75,150); ctx.textAlign='left'; }

else{ aTimer++; if(aTimer>360) aTimer-=360; } }

document.addEventListener('keydown',function(evt){ lastPress=evt.keyCode; PRESSING[evt.keyCode]=true; },false);

document.addEventListener('keyup',function(evt){ PRESSING[evt.keyCode]=false; },false);

function Rectangle(x,y,width,height,type,health){ this.x=(x==null)?0:x; this.y=(y==null)?0:y; this.width=(width==null)?0:width; this.height=(height==null)?this.width:height; this.type=(type==null)?1:type; this.health=(health==null)?1:health; this.timer=0;

this.intersects=function(rect){ if(rect!=null){ return(this.x<rect.x+rect.width&& this.x+this.width>rect.x&& this.y<rect.y+rect.height&& this.y+this.height>rect.y); } } }

function message(string,x,y){ this.string=(string==null)?'?':string;

this.x=(x==null)?0:x; this.y=(y==null)?0:y; }
Publicado por Karl Tayfer en 03:00