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 imágenes. Anteriormente ya vimos como agregar imágenes a un juego, así que aprovecharé este momento para enseñarles una técnica más 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:

[SpriteSheet]

La ventaja de las hojas de sprites sobre las imágenes individuales, es que la hoja de sprites pesa menos que la suma de todas las imágenes 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 más rápida.

HTML5 Canvas ya viene con un método 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 parámetros a la función drawImage:
ctx.drawImage(image, sX,sY,sWidth,sHeight, dX,dY,dWidth,dHeight);
El primer parámetro, 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 práctico. Comencemos creando nuestra función personalizada en el prototipo de la pseudo-clase "Rectangle", que nos permitirá dibujar el área de una imagen en el área correspondiente a nuestro rectángulo:
    Rectangle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
        if(img.width)
            ctx.drawImage(img,sx,sy,sw,sh,this.x,this.y,this.width,this.height);
        else
            ctx.strokeRect(this.x,this.y,this.width,this.height);
    }
Declaremos ahora una variable de tipo imagen y asignemos su fuente a la ruta de nuestra imagen:
    var spritesheet=new Image();
    spritesheet.src='assets/spritesheet.png';
Después, vayamos a donde dibujamos nuestro jugador, y sustituyamos el rectángulo verde, dibujando la primer imagen de nuestra hoja de sprites. La nave está en en la posición 0,0 de nuestra hoja de sprites, y tiene una altura y anchura de 10x10 pixeles, por tanto, la función quedaría así:
            player.drawImageArea(ctx,spritesheet,0,0,10,10);
            //player.fill(ctx);
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 dañada. 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 quedarían de esta forma:
        for(var i=0,l=enemies.length;i<l;i++){
            if(enemies[i].timer%2==0){
                ctx.strokeStyle='#00f';
                enemies[i].drawImageArea(ctx,spritesheet,30,0,10,10);
            }
            else{
                ctx.strokeStyle='#fff';
                enemies[i].drawImageArea(ctx,spritesheet,40,0,10,10);
            }
            //enemies[i].fill(ctx);
        }
Ahora que ya comprendemos la forma en que funciona esta función, hagamos lo mismo para las mejoras:
        for(var i=0,l=powerups.length;i<l;i++){
            if(powerups[i].type==1){
                ctx.strokeStyle='#f90';
                powerups[i].drawImageArea(ctx,spritesheet,50,0,10,10);
            }
            else{
                ctx.strokeStyle='#cc6';
                powerups[i].drawImageArea(ctx,spritesheet,60,0,10,10);
            }
            //powerups[i].fill(ctx);
        }
Y para los disparos:
        ctx.strokeStyle='#f00';
        for(var i=0,l=shots.length;i<l;i++)
            //shots[i].fill(ctx);
            shots[i].drawImageArea(ctx,spritesheet,70,0,5,5);
Nota que los disparos tienen una altura y anchura de 5x5 pixeles, a diferencia de los demás elementos que hemos dibujado antes.

Para hacer el juego más 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 posición del juego.

Animaciones


Otra ventaja que presentan las hojas de sprites, es que permiten crear animaciones de forma sencilla. Como podrás 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 el tiempo que ha pasado para calcular la animación correspondiente. Declaramos una variable para ello al comienzo de nuestro juego:
    var elapsedTime=0;
Posteriormente, se agrega el contador al ciclo, el tiempo delta pasado cada turno mientras el juego no esté en pausa. Para mantener mayor control sobre este contador, cuando supere el valor de 3600 (una hora), restaremos ese valor sobre el mismo:
            // Elapsed time
            elapsedTime+=deltaTime;
            if(elapsedTime>3600)
                elapsedTime-=3600;
Anteriormente aprendimos como calcular el tiempo delta, pero dado que estamos corriendo nuestro juego en forma constante a 50 milisegundos por ciclo, podemos hacer un truco para simplificar este cálculo. Tan solo debemos enviar este tiempo ocurrido convertido a segundos en la variable act, que al dividirlo entre 1000, resulta 0.05:
    function run(){
        setTimeout(run,50);
        act(0.05);
    }
No olvides recibir este valor en la variable act con la variable de nombre deltaTime. Ahora, aplicaremos la animación a nuestros objetos por medio de este contador.

Para obtener la imagen de la animación a dibujar, primero multiplicamos la variable de temporizador por la cantidad de cuadros por segundos que queremos en nuestra animación y lo convertimos en un entero. Posteriormente sacamos el módulo de nuestro contador entre la cantidad de imágenes que tiene nuestra animación, y este valor lo multiplicamos por el alto o el ancho de cada imagen, ya sea que estén dispuestos de forma vertical u horizontal dentro de nuestra hoja de estilos. Esto se muestra de forma más clara en la siguiente fórmula:
(~~(elapsedTime*framesPerSecond)%totalFrames)*frameWidth
Por ejemplo si tuviéramos una animación de 6 imágenes que quisiéramos a 20 cuadros por segundo, con 10 pixeles de ancho cada imagen dentro de la hoja de sprites, la formula quedaría de esta forma:
(~~(elapsedTime*20)%6)*10
Y este valor se suma al origen en X o Y, según corresponda si están dispuestos de forma horizontal o vertical. En el caso de nuestra nave, son tres imágenes dispuestas de forma horizontal con 10 pixeles de ancho, las animaremos a 10 cuadros por segundo para que no pasen demasiado veloz, por tanto el dibujado quedaría de esta forma:
            player.drawImageArea(ctx,spritesheet,(~~(elapsedTime*10)%3)*10,0,10,10);
En el caso de los disparos, son dos imágenes dispuestas de forma vertical con 5 pixeles de alto, por tanto, se dibujarían de esta forma:
            shots[i].drawImageArea(ctx,spritesheet,70,(~~(elapsedTime*10)%2)*5,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 mayoría de las veces, la animación no comenzará en 0, así que deberás sumar su origen correspondiente.

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 acompañarme una vez más en este sitio, y como siempre, cualquier duda o comentario, estoy en mi mejor disposición para ayudarles. ¡Felices códigos!

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var KEY_ENTER=13;
    var KEY_SPACE=32;
    var KEY_LEFT=37;
    var KEY_UP=38;
    var KEY_RIGHT=39;
    var KEY_DOWN=40;

    var canvas=null,ctx=null;
    var lastPress=null;
    var pressing=[];
    var pause=true;
    var gameover=true;
    var score=0;
    var multishot=1;
    var elapsedTime=0;
    var player=new Rectangle(90,280,10,10,0,3);
    var shots=[];
    var enemies=[];
    var powerups=[];
    var messages=[];
    var spritesheet=new Image();
    spritesheet.src='assets/spritesheet.png';

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

    function init(){
        canvas=document.getElementById('canvas');
        ctx=canvas.getContext('2d');
        canvas.width=200;
        canvas.height=300;
        
        run();
        repaint();
    }

    function run(){
        setTimeout(run,50);
        act(0.05);
    }

    function repaint(){
        requestAnimationFrame(repaint);
        paint(ctx);
    }

    function reset(){
        score=0;
        multishot=1;
        player.x=90;
        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(30,0,10,10,0,2));
        enemies.push(new Rectangle(70,0,10,10,0,2));
        enemies.push(new Rectangle(110,0,10,10,0,2));
        enemies.push(new Rectangle(150,0,10,10,0,2));
        gameover=false;
    }

    function act(deltaTime){
        if(!pause){
            // GameOver Reset
            if(gameover)
                reset();
            
            // Move Player
            //if(pressing[KEY_UP])
            //    player.y-=10;
            if(pressing[KEY_RIGHT])
                player.x+=10;
            //if(pressing[KEY_DOWN])
            //    player.y+=10;
            if(pressing[KEY_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==KEY_SPACE){
                if(multishot==3){
                    shots.push(new Rectangle(player.x-3,player.y+2,5,5));
                    shots.push(new Rectangle(player.x+3,player.y,5,5));
                    shots.push(new Rectangle(player.x+9,player.y+2,5,5));
                }
                else if(multishot==2){
                    shots.push(new Rectangle(player.x,player.y,5,5));
                    shots.push(new Rectangle(player.x+5,player.y,5,5));
                }
                else
                    shots.push(new Rectangle(player.x+3,player.y,5,5));
                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--;
                    }
                }
            }
            
            // Elapsed time
            elapsedTime+=deltaTime;
            if(elapsedTime>3600)
                elapsedTime-=3600;
            
            // Damaged
            if(player.timer>0)
                player.timer--;
            
            // GameOver
            if(player.health<1){
                gameover=true;
                pause=true;
            }
        }
        // Pause/Unpause
        if(lastPress==KEY_ENTER){
            pause=!pause;
            lastPress=null;
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#0f0';
        if(player.timer%2==0)
            //player.fill(ctx);
            player.drawImageArea(ctx,spritesheet,(~~(elapsedTime*10)%3)*10,0,10,10);
        for(var i=0,l=powerups.length;i<l;i++){
            if(powerups[i].type==1){
                ctx.strokeStyle='#f90';
                powerups[i].drawImageArea(ctx,spritesheet,50,0,10,10);
            }
            else{
                ctx.strokeStyle='#cc6';
                powerups[i].drawImageArea(ctx,spritesheet,60,0,10,10);
            }
            //powerups[i].fill(ctx);
        }
        for(var i=0,l=enemies.length;i<l;i++){
            if(enemies[i].timer%2==0){
                ctx.strokeStyle='#00f';
                enemies[i].drawImageArea(ctx,spritesheet,30,0,10,10);
            }
            else{
                ctx.strokeStyle='#fff';
                enemies[i].drawImageArea(ctx,spritesheet,40,0,10,10);
            }
            //enemies[i].fill(ctx);
        }
        ctx.strokeStyle='#f00';
        for(var i=0,l=shots.length;i<l;i++)
            //shots[i].fill(ctx);
            shots[i].drawImageArea(ctx,spritesheet,70,(~~(elapsedTime*10)%2)*5,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,150,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',100,150);
            else
                ctx.fillText('PAUSE',100,150);
            ctx.textAlign='left';
        }
    }

    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;
    }

    Rectangle.prototype.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);
        }
    }
    
    Rectangle.prototype.fill=function(ctx){
        ctx.fillRect(this.x,this.y,this.width,this.height);
    }

    Rectangle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
        if(img.width)
            ctx.drawImage(img,sx,sy,sw,sh,this.x,this.y,this.width,this.height);
        else
            ctx.strokeRect(this.x,this.y,this.width,this.height);
    }

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

    window.requestAnimationFrame=(function(){
        return window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame || 
            function(callback){window.setTimeout(callback,17);};
    })();
})();

23 comentarios:

  1. Buenas no me quedo muy claro lo de la hoja de sprites, como se en que posicion esta.

    ResponderBorrar
    Respuestas
    1. Eso debes de verlo al juntar las imágenes que hagas en tu editor favorito. Las posiciones se miden en píxeles.

      Borrar
  2. De que tamaño deberían ser los sprites? Gran trabajo :D

    Saludos

    ResponderBorrar
    Respuestas
    1. ¡Del tamaño que quieras que sean tus imágenes! No existe ninguna limitante para esto.

      Borrar
  3. Hola, como se haria para leer un sprite en el que las imagenes estan en filas, es decir, la misma animación por ejemplo correr y que no se encuentre solo en una fila sino es 3 dentro del archivo de imagen.

    Seria asi

    MMMM
    MMMM
    MMMM

    Gracias man!

    ResponderBorrar
    Respuestas
    1. Querido amigo Juan... Complicas las cosas que ya son más o menos difíciles :P... Pero supondré que tienes tu razón para hacerlo de esta forma, y en verdad no es tan difícil, así que permíteme ayudarte con tu problema.

      Para empezar, necesitas saber en el origen en x de columna corresponde; esto se hace como hasta ahora, con el módulo de la cantidad de imágenes en horizontal, multiplicado por el ancho de las imágenes: (aTimer%4)*10,

      Posteriormente para el origen en y de la fila correspondiente. Para ello, hay que dividir el temporizador entre la cantidad de objetos por columna, obtener su entero, y eso nos dará la fila correspondiente, al que de forma similar, hay que multiplicar por la altura de la imagen: ~~(aTimer/4)*10,

      El resto es como siempre, dando su ancho y alto, y la función personalizada se ha de encargar de lo demás. ¡Suerte en tu proyecto! Felices códigos :)

      Borrar
    2. Dificil, porque debería de poner cada sprite seguido en vez de en filas?

      ctx.drawImage(

      pajarossprite,
      // Source x
      (~~(step/20)%5)*41,
      // Source y
      ~~(step/20)*41,
      // Source width
      ......

      Lo he puesto asi pero no funciona. No aparecen. El x se que funciona ya lo probé. Pero no baja en y.

      Gracias por tu tiempo

      Borrar
    3. Perdon en y tengo puesto ~~(step/5)*41

      Borrar
    4. ¿Por que divides step entre 20? Si lo haces para hacer mas lenta la animación, debes hacer lo mismo con Y (Dividir entre 20 y luego entre 5)

      Borrar
  4. Esto va a ser complicado. Entonces un sprite con 30 imagenes como lo leemos. En x solo? Va a ser lo mejor.
    Podrias escribir un libro, HTML5 para dummys yo te lo compraria fijo. Version digital o papel.

    ResponderBorrar
    Respuestas
    1. Tan solo agrega así en Y:

      ~~(~~(step/20)/5)*41,

      Eso debería resolver el problema.

      Borrar
  5. Muchas gracias Kart! Oye lo del libro no es broma. UN librito para principiantes con todo lo que tienes ya sería mejor que muchos bodrios que he visto por 30€ en amazon. Se lee bien y lo mejor es que se te entiende. Un saludo y a seguir asi.

    ResponderBorrar
    Respuestas
    1. Muchas gracias. Me alegra saber que estos humildes tutoría les sean claros y de gran ayuda para ti. Consideraré la opción que me presentas.

      Borrar
  6. Hola Karl que pasa como estás? Queria saber como hacer una animación pero solo una vez ahora mismo lo hago con una varible en el condicional antes del dibujado. Hay otra forma mas sutil? Cómo se cuando acaba? ya que step siempre aumenta.

    Un saludo y cuenta como va el libro.

    ResponderBorrar
    Respuestas
    1. Definitivamente requieres de una condicional para esta clase de animación. La forma en que yo lo hago es usar una variable exclusiva para esa animación, y dejar de modificar su valor cuando llegue a su punto máximo. Hasta ahora, no se si haya forma mas óptima de hacerlo.

      ¡Saludos! Y de momento, aun no hay novedades importantes para contar del libro.

      Borrar
  7. Hola Karl me gustaria saber que opina sobre crear animacion en flash y luego exportarlo a aseljs,json.... He leido que para crear juegos en javascipt y funcione de manera optima en dispositivos mobiles es la mejor opcion. Ya que cuando se juntan muchas spritesheet va lento. te dejo un video que se muestra lo que te digo.

    http://tv.adobe.com/watch/cs6-creative-cloud-feature-tour-for-web/generating-sprite-sheets-using-flash-professional-cs6/

    Este video exporta un png con la animación y un código en javascript que no se si se puede compaginar lo aprendido en estos tutoriales.
    Un saludo.

    ResponderBorrar
    Respuestas
    1. Easel.js es una librería para usar JavaScript con un código muy similar a ActionScript. Lo que hace Flash, es exportar una animación previamente creada a un SpriteSheet, idéntico al que usamos en este ejemplo.

      La única diferencia es que usas un editor visual que automatiza todo el proceso, pero si quieres hacerlo manualmente o usar un programa de $700 dolares para ello, es tu decisión.

      Borrar
  8. Este comentario ha sido eliminado por el autor.

    ResponderBorrar
    Respuestas
    1. He revisado tu código, y he de admitir que no está nada mal implementado. Lo único que si te recomiendo, es que no calcules la delta de tiempo en cada spritesheet, pues de ser varias en el lienzo, gastarán innecesariamente energía del procesador. Fuera de ello, tu código va por buen camino.

      Ahora, ya que me has pedido ayuda sobre mi forma de hacerlo comparado al tuyo, he jugado un poco con tu código para hacerlo más al estilo en que lo hago yo. Te dejo el fiddle para que compares las diferencias y veas como ha sido implementado. Si aun tienes dudas al respecto, puedes hacermelas con confianza:

      http://jsfiddle.net/daPhyre/ytkya3np/1/

      Borrar
    2. Jaja te admiro infinitas veces, has reducido las lineas de mi código casi al 70%, ahora estoy con lápiz y papel graficando el comportamiento de las variables, hay una cosa que aun no logro entender con claridad y es el uso del operador modulo, entiendo que valores devuelve el modulo pero no la funcionalidad en el código, bueno en todo caso estaré graficando las variables creo que de ese ejercicio obtendré algunas respuestas, pero ahora quisiera preguntarte una cosa, en tus juegos ¿tu creas una hoja de sprites para todo el juego o una por objetos?, de ser afirmativa la primera opción como solucionas el problema de tener que modificar las coordenadas que ubican el sprite en la hoja de sprites, en el ejemplo que das en este blog usas una única hoja de sprites para todo tu mundo, pero bueno quisiera saber que enfoque te gusta mas y además en una escala del uno al diez cual crees que seria la dificultad de implementar esta característica al código
      ( el de jsfiddle ).

      posdata: Elimine mi pregunta por que creí que no me contestarías y no quería dañarte el blog, discúlpame..

      Borrar
    3. Ya descubrí como implementar la característica que decía, tu lo has dicho en esta entrada, simplemente sumar, aun así me podrías decir que enfoque te gusta mas a la hora de trabajar con spritesheets [ '¿una spritesheet para todo el juego?', '¿una spritesheet por objeto?' ]

      Borrar
    4. ¿No te da la opción de restaurarlo? Creo que era una buena pregunta para referencia a otros lectores.

      ¡En fin! Sobre tu duda, creo que eso depende del estilo y magnitud del proyecto. Lo más óptimo es usar la menor cantidad de hojas de estilos, sobre todo el proyectos web, aunque cuidar también que estas no sean exageradamente gigantes. Muchos programas incluso tienen limites como 1024, 2048 o 4096 los más poderosos hoy día...

      Pero es más común que esta clase de optimizaciones se hagan ya cuando el proyecto ha quedado bien definido. Durante el desarrollo, suelo optar por hojas de estilo por objeto, para poder desarrollar y hacer pruebas de forma mas sencilla y rápida. Así podemos experimentar con distintos estilos de imágenes y animaciones con facilidad.

      Borrar
  9. Hola, tal vez nadie me responda pero el codigo que dejó Karl al final me da errores en la ultima linea, pone Unexpected token ')' y si lo borro pone lo mismo y no se que hacer, por favor alguien que me ayude.

    ResponderBorrar