Aprende a crear juegos en HTML5 Canvas

domingo, 17 de febrero de 2013

Manejo de escenas

Ahora que ya sabemos hacer un juego, es tiempo de pensar en agregarle detalles para hacerlo más atractivo y funcional, y para ello necesitaremos múltiples escenas que aparecerán antes y después de la función principal de nuestro juego.

Existen varias formas de manejar escenas. Quizá la forma mas sencilla y directa de hacerlo, sea creando estados condicionales, como se muestra en el siguiente ejemplo:
    var SCENE_MAIN = 0,
        SCENE_GAME = 1,
    
        currentScene = 0;
    
    function act() {
        if (currentScene === SCENE_MAIN) {
            //...
        } else if (currentScene === SCENE_GAME) {
            //...
        }
    }
    
    function paint(ctx) {
        if (currentScene === SCENE_MAIN) {
            //...
        } else if (currentScene === SCENE_GAME) {
            //...
        }
    }
Dentro de cada área condicional se pondría el código a ejecutar para el caso de cada escena deseada, cambiando el valor de la variable "currentScene" para simular el cambio de escena. Esta es una técnica bastante práctica, sin embargo, cuando los juegos son más complejos y con muchas escenas posibles, mantener un código así puede ser bastante difícil.

Es por ello que les enseñaré en esta ocasión una técnica diferente, que si bien es un poco más compleja de comprender al comienzo, al final resultará mucho mas práctica, automatizada y fácil de mantener.

Para empezar, necesitaremos un arreglo que almacene nuestras escenas, y la variable currentScene del ejemplo pasado para manejar las distintas escenas:
    var currentScene = 0,
        scenes = [];
Las escenas las manejaremos a través de una función "Scene", la cual al momento de ser creada, le asignaremos automáticamente una ID única basada en el tamaño del arreglo de escenas, y lo pondremos dentro del mismo:
    function Scene() {
        this.id = scenes.length;
        scenes.push(this);
    }
Al prototipo de esta función le asignaremos las funciones que todas escenas manejaran: una función "act" y una función "paint" como la que ya hemos aprendido a manejar hasta ahora, además de una función "load" que se ejecutará automáticamente al cargar dicha escena:
    Scene.prototype = {
        constructor: Scene,
        load: function () {},
        paint: function (ctx) {},
        act: function () {}
    };
Si vas a manejar delta de tiempo, no olvides mandarla también a la función "act". Finalmente, agregaremos una función "loadScene" para que cargue nuestra nueva escena de forma automática, de esta forma:
    function loadScene(scene) {
        currentScene = scene.id;
        scenes[currentScene].load();
    }
Por último, modificaremos nuestras funciones "run" y "repaint" para que ejecute automáticamente la escena en la que nos encontramos:
    function repaint() {
        window.requestAnimationFrame(repaint);
        if (scenes.length) {
            scenes[currentScene].paint(ctx);
        }
    }

    function run() {
        setTimeout(run, 50);
        if (scenes.length) {
            scenes[currentScene].act();
        }
    }
Podrás notar que antes de ejecutar su respectiva función, comprobamos antes que haya al menos una escena dentro del arreglo de escenas. De esta forma, si olvidamos crear al menos una escena, no retornará un error. Ahora que ya tenemos todo listo, procedamos a declarar nuestras escenas. Para este ejemplo práctico, crearemos dos: la escena del menú principal, y la escena de nuestro juego:
    var mainScene = null,
        gameScene = null;
Para que nuestro juego funcione en este nuevo formato, tenemos que cambiar nuestras funciones "act" y "paint" por "gameScene.act" y "gameScene.paint". Dado que estas funciones son variables ya existentes, tenemos que "re-asignarles" las funciones, de la siguiente forma:
    // Game Scene
    gameScene = new Scene();

    //function reset(){
    gameScene.load = function () {
        //...
    }
    
    //function paint(ctx){
    gameScene.paint = function (ctx) {
        //...
    }
    
    //function act(){
    gameScene.act = function () {
        //...
    }
Notarás que también he cambiado la función "reset" por "gameScene.load" para que cargue automáticamente cuando se cambie a esta escena.

Ahora, agregaremos nuestro menú principal:
    // Main Scene
    mainScene = new Scene();

    mainScene.paint = function (ctx) {
        // Clean canvas
        ctx.fillStyle = '#030';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw title
        ctx.fillStyle = '#fff';
        ctx.textAlign = 'center';
        ctx.fillText('SNAKE', 150, 60);
        ctx.fillText('Press Enter', 150, 90);
    };

    mainScene.act = function () {
        // Load next scene
        if (lastPress === KEY_ENTER) {
            loadScene(gameScene);
            lastPress = null;
        }
    };
En esta escena, solo he agregado un escucha a la tecla "Enter" para que empiece el juego, y en la función "paint" se imprime tan solo el título del juego. Nota que no he asignado la función "load" a "mainScene", esto por que no es necesario cargar nada en esta escena. Sin embargo, la escena que cargue predeterminadamente, no cargará su función "load" por defecto, y si se manda a llamar a ella antes de ser creada, no tendrá efecto alguno, por lo que si deseamos que la escena predeterminada cargue su función "load" al comienzo del juego, se tendrá que forzar su carga tras su creación de esta forma:
    mainScene.load = function () {
        //...
    }
    mainScene.load();
Para finalizar, ahora que ya tenemos todo nuestro juego con escenas preparado, cambiaremos el comportamiento en nuestro juego al hacer Game Over, para que nos regrese a la pantalla principal:
            // GameOver Reset
            if (gameover) {
                loadScene(mainScene);
            }
Con esto nuestro juego está listo para soportar múltiples escenas. Ya solo es necesario que tú crees y personalices las necesarias para tu propio juego. ¡Felices códigos!

Código final:

[Canvas not supported by your browser]
/*jslint bitwise:true, es5: true */
(function (window, undefined) {
    'use strict';
    var KEY_ENTER = 13,
        KEY_LEFT = 37,
        KEY_UP = 38,
        KEY_RIGHT = 39,
        KEY_DOWN = 40,
        
        canvas = null,
        ctx = null,
        lastPress = null,
        pause = false,
        gameover = false,
        currentScene = 0,
        scenes = [],
        mainScene = null,
        gameScene = null,
        body = [],
        food = null,
        //var wall = [],
        dir = 0,
        score = 0,
        iBody = new Image(),
        iFood = new Image(),
        aEat = new Audio(),
        aDie = new Audio();

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

    document.addEventListener('keydown', function (evt) {
        if (evt.which >= 37 && evt.which <= 40) {
            evt.preventDefault();
        }

        lastPress = evt.which;
    }, false);
    
    function Rectangle(x, y, width, height) {
        this.x = (x === undefined) ? 0 : x;
        this.y = (y === undefined) ? 0 : y;
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
    }

    Rectangle.prototype = {
        constructor: Rectangle,
        
        intersects: function (rect) {
            if (rect === undefined) {
                window.console.warn('Missing parameters on function intersects');
            } else {
                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);
            }
        },
        
        fill: function (ctx) {
            if (ctx === undefined) {
                window.console.warn('Missing parameters on function fill');
            } else {
                ctx.fillRect(this.x, this.y, this.width, this.height);
            }
        },
        
        drawImage: function (ctx, img) {
            if (img === undefined) {
                window.console.warn('Missing parameters on function drawImage');
            } else {
                if (img.width) {
                    ctx.drawImage(img, this.x, this.y);
                } else {
                    ctx.strokeRect(this.x, this.y, this.width, this.height);
                }
            }
        }
    };

    function Scene() {
        this.id = scenes.length;
        scenes.push(this);
    }

    Scene.prototype = {
        constructor: Scene,
        load: function () {},
        paint: function (ctx) {},
        act: function () {}
    };

    function loadScene(scene) {
        currentScene = scene.id;
        scenes[currentScene].load();
    }

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

    function repaint() {
        window.requestAnimationFrame(repaint);
        if (scenes.length) {
            scenes[currentScene].paint(ctx);
        }
    }

    function run() {
        setTimeout(run, 50);
        if (scenes.length) {
            scenes[currentScene].act();
        }
    }

    function init() {
        // Get canvas and context
        canvas = document.getElementById('canvas');
        ctx = canvas.getContext('2d');

        // Load assets
        iBody.src = 'assets/body.png';
        iFood.src = 'assets/fruit.png';
        aEat.src = 'assets/chomp.m4a';
        aDie.src = 'assets/dies.m4a';

        // Create food
        food = new Rectangle(80, 80, 10, 10);

        // Create walls
        //wall.push(new Rectangle(50, 50, 10, 10));
        //wall.push(new Rectangle(50, 100, 10, 10));
        //wall.push(new Rectangle(100, 50, 10, 10));
        //wall.push(new Rectangle(100, 100, 10, 10));

        // Start game
        run();
        repaint();
    }

    // Main Scene
    mainScene = new Scene();

    mainScene.paint = function (ctx) {
        // Clean canvas
        ctx.fillStyle = '#030';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw title
        ctx.fillStyle = '#fff';
        ctx.textAlign = 'center';
        ctx.fillText('SNAKE', 150, 60);
        ctx.fillText('Press Enter', 150, 90);
    };

    mainScene.act = function () {
        // Load next scene
        if (lastPress === KEY_ENTER) {
            loadScene(gameScene);
            lastPress = null;
        }
    };

    // Game Scene
    gameScene = new Scene();

    gameScene.load = function () {
        score = 0;
        dir = 1;
        body.length = 0;
        body.push(new Rectangle(40, 40, 10, 10));
        body.push(new Rectangle(0, 0, 10, 10));
        body.push(new Rectangle(0, 0, 10, 10));
        food.x = random(canvas.width / 10 - 1) * 10;
        food.y = random(canvas.height / 10 - 1) * 10;
        gameover = false;
    };

    gameScene.paint = function (ctx) {
        var i = 0,
            l = 0;
        
        // Clean canvas
        ctx.fillStyle = '#030';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw player
        ctx.strokeStyle = '#0f0';
        for (i = 0, l = body.length; i < l; i += 1) {
            body[i].drawImage(ctx, iBody);
        }
        
        // Draw walls
        //ctx.fillStyle = '#999';
        //for (i = 0, l = wall.length; i < l; i += 1) {
        //    wall[i].fill(ctx);
        //}
        
        // Draw food
        ctx.strokeStyle = '#f00';
        food.drawImage(ctx, iFood);

        // Draw score
        ctx.fillStyle = '#fff';
        ctx.textAlign = 'left';
        ctx.fillText('Score: ' + score, 0, 10);
        
        // Debug last key pressed
        //ctx.fillText('Last Press: '+lastPress,0,20);
        
        // Draw pause
        if (pause) {
            ctx.textAlign = 'center';
            if (gameover) {
                ctx.fillText('GAME OVER', 150, 75);
            } else {
                ctx.fillText('PAUSE', 150, 75);
            }
        }
    };

    gameScene.act = function () {
        var i = 0,
            l = 0;
        
        if (!pause) {
            // GameOver Reset
            if (gameover) {
                loadScene(mainScene);
            }

            // Move Body
            for (i = body.length - 1; i > 0; i -= 1) {
                body[i].x = body[i - 1].x;
                body[i].y = body[i - 1].y;
            }

            // Change Direction
            if (lastPress === KEY_UP && dir !== 2) {
                dir = 0;
            }
            if (lastPress === KEY_RIGHT && dir !== 3) {
                dir = 1;
            }
            if (lastPress === KEY_DOWN && dir !== 0) {
                dir = 2;
            }
            if (lastPress === KEY_LEFT && dir !== 1) {
                dir = 3;
            }

            // Move Head
            if (dir === 0) {
                body[0].y -= 10;
            }
            if (dir === 1) {
                body[0].x += 10;
            }
            if (dir === 2) {
                body[0].y += 10;
            }
            if (dir === 3) {
                body[0].x -= 10;
            }

            // Out Screen
            if (body[0].x > canvas.width - body[0].width) {
                body[0].x = 0;
            }
            if (body[0].y > canvas.height - body[0].height) {
                body[0].y = 0;
            }
            if (body[0].x < 0) {
                body[0].x = canvas.width - body[0].width;
            }
            if (body[0].y < 0) {
                body[0].y = canvas.height - body[0].height;
            }

            // Food Intersects
            if (body[0].intersects(food)) {
                body.push(new Rectangle(0, 0, 10, 10));
                score += 1;
                food.x = random(canvas.width / 10 - 1) * 10;
                food.y = random(canvas.height / 10 - 1) * 10;
                aEat.play();
            }

            // Wall Intersects
            //for (i = 0, l = wall.length; i < l; i += 1) {
            //    if (food.intersects(wall[i])) {
            //        food.x = random(canvas.width / 10 - 1) * 10;
            //        food.y = random(canvas.height / 10 - 1) * 10;
            //    }
            //
            //    if (body[0].intersects(wall[i])) {
            //        gameover = true;
            //        pause = true;
            //    }
            //}

            // Body Intersects
            for (i = 2, l = body.length; i < l; i += 1) {
                if (body[0].intersects(body[i])) {
                    gameover = true;
                    pause = true;
                    aDie.play();
                }
            }
        }
        // Pause/Unpause
        if (lastPress === KEY_ENTER) {
            pause = !pause;
            lastPress = null;
        }
    };
    
    window.addEventListener('load', init, false);
}(window));

37 comentarios:

  1. oye aun me parece muy complicado, que tal si se cargan scripts js correspondientes a cada escena del juego, por ejemplo habria un menuAct.js y tendria :
    act(){
    //todo el code
    }

    y menuPaint.js que tendria :

    paint(){
    //la fase de pintado
    }

    que opinas, seria posible ? que piensas tanbien de cargar un html por cada escena ? (esto ultimo es lo que hago actualmente pero no se si halla algo mas optimo)

    Por ultimo me gustaria si profundizas mas en la parte de audio mediante POO y los eventos y metodos de la api de audio.

    gracias por tu ayuda.

    ResponderEliminar
    Respuestas
    1. La primer idea que dices es posible y válida, aunque no permite encapsulamiento del código, además que requiere la descarga de multiples scripts lo que no es óptimo para velocidad de carga y consumo de ancho de banda.

      Existen programas que te permiten distribuir tu código JS en varios archivos de desarrollo y compilarlos en uno solo al final para liberacion, lo que podría resolver el problema mencionado, por si deseas considerarlo como alternativa.

      Sobre distribuir las escenas en múltiples HTML, no solo es costoso al servidor y velocidad, si no que además la comunicación entre ellos es difícil, por lo que mantener un juego así es muy poco óptimo. Precisamente el presente tema permite manejar multiples escenas dentro del mismo juego.

      Finalmente, ¿Qué dudas tienes respecto a Audio? Para poder ayudarte mejor a las necesidades que existan al respecto.

      Eliminar
    2. Hola, muchas gracias, sucede que mi proyecto lo estoy portando a moviles (webview, android, IOs, etc) con cocoonjs por eso hago lo de cargar una html por cada escena (nivel), jeje, aunque si lo coloco en un host probablemente tenga que seguir tus consejos, aunque no se si con webworkers se pueda encapsular ?

      En cuanto a lo de audio seria los metodos de cada audio, especificamente no he podido hacer lo siguiente :

      function creaSonido(){
      //como se haria aqui ?
      //se que tendria que ir document.createElement("audio");
      // o new Audio();

      //asi no funciona
      this=new Audio();


      }

      var golpe = new creaSonido ("sound/golpe.ogg");

      lo he hecho directo pero me gustaria por POO.

      asi lo he hecho :

      var golpe = document.createElement("audio");

      golpe.preload="auto";

      golpe.src="tal/tal.tal";

      //y no he podido cambiar la velocidad de reproduccion

      golpe.playbackRate=2.0;//esto no funciona

      //aunque esto ultimo del playrate ya le encontre otra solucion

      Eliminar
    3. Bueno, Cocoon encapsula el código JS a nativo, así que si haces multiples html, el problema que te comentaba se hereda a esta versión también.

      Los webworkers son códigos de Javascript ejecutados asíncronos... No se como eso pueda resolver tu problema de múltiples html.

      Sobre el audio que intentas crear, viendo el código que has dejado, creo que intentas hacer esta función:

      function creaSonido(path){
      var au=document.createElement("audio");
      au.preload="auto";
      au.src=path;
      return au;
      }

      Avísame si era esto lo que estabas buscando.

      Eliminar
    4. Este comentario ha sido eliminado por el autor.

      Eliminar
    5. de verdad? pensaba que como todos los recursos estan en el ejecutable (zip, apk, ipa) y no va a haber llamadas a servidor por recursos no seria problema cargar un html por escena!

      Eliminar
    6. Yo decía lo que te mencionaba de los multiples HTML que no tienen consistencia entre ellos para manejar datos en común, y lo poco optimizado que resulta recargar la página. Aunque de la descarga de archivos, ahí si no hay conflicto en tenerlo todo de forma local.

      Eliminar
  2. Hola, siempre es un placer recibir un tema nuevo, muchas gracias.
    Muy buen tema, yo lo estaba haciendo de la primera manera,estamanera se me hace aun mas ordenada, como estoy haciendo un rpg necesito muchas escenas.

    una pequeña duda, ¿cual es un buen programa para hacer lo de distribuir el .js?, me serviria para no tener todo mas ordenado.

    De antemano Gracias!

    ResponderEliminar
    Respuestas
    1. Existen multiples herramientas para compilar Javascript: Closure Compiler, YUI Compressor, Minify, UglifyJS, MergeJS, CompressJS...

      El problema es que prácticamente todas estas tienes que ejecutarlas en la consola, así que a menos que tengas un script que automatice esta tarea, es apenas poco mejor que copiar todos los archivos en uno solo de forma manual.

      Actualmente estoy trabajando en una solución para automatizar esta tarea con el clic un botón para un popular IDE, pero de aquí a que salga, puede aún tomar algo de tiempo, así que estas son las soluciones por ahora.

      Eliminar
    2. Muchas gracias, supongo que de esa manera seria mas facil hacer lo que mecionabas (dividir el js y compilarlo al final)???, y gracias

      Eliminar
  3. Sin ti el mundo seria un lugar mas caótico, gracias por tus tutos

    ResponderEliminar
  4. quiero hacer el primero de las condicionales me das una mano que no lo puedo agregar a mi codigo

    ResponderEliminar
    Respuestas
    1. Seguro, ¿En qué parte te has atorado?

      Eliminar
    2. no entiendo que codigo pongo en scene main y game se lo quiero agregar al de la serpiente

      Eliminar
    3. Main es tu menú principal, y Game es el juego en sí, por poner ejemplos. Tú decidirás cuantas y cuales escenas deseas implementar.

      Eliminar
    4. voy entendiendo mira esta bien lo que hago
      http://codepen.io/anon/pen/QbqVoy

      gracias por responder

      Eliminar
    5. Vas por buen camino. Ahora sólo te resta mover todo el código dentro de act y paint respectivamente adentro de las llaves de la condicional "else if(currentScene==SCENE_GAME){".

      Eliminar
    6. hola, va quedando en currentScene==SCENE_MAIN de act tengo que llamar la funcion reset???

      http://codepen.io/anon/pen/yNzGXY

      otra pregunta la condicion que muestra el fin de juego y pausa en la funcion paint va a dentro de currentScene==SCENE_GAME ??

      Eliminar
    7. ¡Ah sí! Falta la línea de "currentScene = SCENE_GAME;" al presionar "enter" cuando estás en SCENE_MAIN.

      Y en SCENE_GAME va todo lo de pausa, tanto dibujarle en "paint", como la condición "if (pause)", y cambiarla entre verdadera y falsa.

      Eliminar
  5. asi mira http://codepen.io/anon/pen/eNeYGo

    ResponderEliminar
    Respuestas
    1. ¡Así precisamente! Buen trabajo.

      Eliminar
    2. al presionar enter quiero que la serpiente empiece a correr como hago eso

      Eliminar
    3. Sí pausa es falso al comienzo, el juego iniciaría directamente.

      Eliminar
    4. quedo muy bien muchas gracias ahora voy por el juego de naves cualquier cosa te consulto.

      saludos

      Eliminar
  6. ¿Qué tal, Karl? Antes que nada gracias por los tutoriales porque me han sido muy útiles.

    Tengo una duda y es la siguiente. En un tutorial anterior vimos que era recomendable agregar funciones a nuestras "clases" vía prototypes para que solo hubiera una copia de dicha función que fuera accesible por todos los objetos que se creen de esa clase.

    Sin embargo, en esta parte veo que usas prototypes para agregar las funciones act, paint y load a todas las Scenes y que después redefinís el act, paint y load de cada tipo de escena haciendo, por ejemplo, gameScene.act = function(){codigo}
    ¿Esto no hace que cada Scene tenga su propia copia de act() en donde act() va a hacer algo distinto dependiendo de la Scene? Si es así, ¿tiene algún sentido agregar métodos mediente prototypes en este caso?

    ResponderEliminar
    Respuestas
    1. Cuando creas un objeto basado en una función pseudo-clase, puedes sobreescribir sus funciones prototipo; esto es común en otros lenguajes como parte de la herencia en programación orientada a objetos.

      Efectivamente, la intensión es que cada Scene creada tenga su propio "act" y "paint" y "load", pues cada una de estas escenas se comportará de forma distinta. Pero sí en algún caso, alguna de estas escenas no requiere de una de estas funciones personalizadas, no es necesario crear la función vacía, ya que las nuevas escenas vacías tienen sus funciones prototipo por defecto, evitando de esta forma errores potenciales en caso de omitir la creación de alguna de estas funciones esenciales.

      Eliminar
    2. Me parecía que podía ir por ahí la mano. Gracias por responder.

      Eliminar
  7. Hola de nuevo Karl, estoy implementando la escenas, y mi duda es con el constructor, me dice que escena no es un constructor.

    ResponderEliminar
    Respuestas
    1. Que extraño, parece que no detecta la función Scene que declaras justo antes. ¿Este error te lo está reportando la consola de Google? ¿Hiciste alguna modificación al código que dejé aquí?

      Eliminar
  8. https://www.dropbox.com/sh/8ci0bph2qzfoxco/AADO764tDnaW0w_FCqfcGVzKa?dl=0
    Gracias por contestar, te dejo en este enlace el código para si puedes lo mires¡

    ResponderEliminar
    Respuestas
    1. Ya vi tu problema. Estás declarando un arreglo llamado "escenas", y posteriormente una función llamada exactamente de la misma forma, lo que confunde al código y utiliza una sobre la otra. Corrigiendo eso, debería resolverse el problema.

      Como consejo y práctica común, las clases (y pseudo-clases en este caso) suelen empezar en mayúscula, y los modelos (clases para objetos) suelen ser nombradas siempre en singular. Por ello te recomendaré que esta función la llames "Escena" en lugar de "escenas". Te ayudará a diferenciarles de forma más rápida y sencilla.

      Eliminar
  9. Gracias por tu tiempo¡ Me funciona, pero ahora cuando le doy a intro se queda en pausa y le vuelvo a dar y empieza todo a ver como duplicado hacia abajo, y se queda pillado

    ResponderEliminar
    Respuestas
    1. Temía por un momento estarme volviendo loco, pues no encontraba el error, y casi estaba seguro que algo estaba duplicando los elementos en el juego, pero en realidad el error era mucho más simple: Olvidaste re-dibujar el fondo al comienzo de la función paint de la escena del juego. Corrigiendo esto, deberá funcionar tu juego sin conflictos.

      PD: ¿Arreglaste también el bug de la variable de nombre duplicado en tu función cargaescena?

      Eliminar
  10. Muchísimas gracias por tan excelentes tutoriales, realmente desconocía demasiadas cosas, he estado trabajando en un proyecto de la escuela viendo y consultando este blog y tengo una duda, al momento de realizar el game over,quisiera que saliera una imagen ya sea un gif o un sprite, ¿cómo podría hacer eso?, de antemano muchas gracias en serio que me ha servido bastante

    ResponderEliminar
    Respuestas
    1. Puedes dibujarle con ctx.drawImage dentro de la condicional de gameOver. ¿Que tipo de imagen ocuparías dibujar en este estado?

      Eliminar
    2. No esperaba una respuesta tan rápida, gracias!!, pues la idea del juego es crear una cara riéndose (simulando un profesor) e insertar el audio de la risa (algo así como andross de star fox pero mucho más sencillo jaja), muchas gracias en seriooo

      Eliminar
    3. Ya veo. Entonces, sin duda puedes hacerlo con drawImage. Si quieres saber cómo hacer la animación, puedes revisar el tema de hoja de sprites y animaciones, y el audio creo que ya tienes las herramientas para llevarlo a cabo. ¡Éxito!

      Eliminar