Aprende a crear juegos en HTML5 Canvas

domingo, 21 de julio de 2013

Arrastra al agujero

En esta ocasión, probaremos tu habilidad adquirida como programador de videojuegos. Ya has completado varios juegos con este tutorial, ya sabes como arrastrar y soltar objetos, ya sabes como interactuar con otros círculos, y ya sabes como usar un temporizador. Por tanto, te toca desarrollar el juego que muestro aquí abajo; esta vez no te daré el código. Lo que si te daré, son las imágenes y unos tips.

Para empezar, las imágenes que usé en este juego están contenidas en la siguiente hoja de sprites:


Cada imagen tiene 40px de alto por 40 de ancho; tú ya sabes como hacer uso de ella. El color de fondo que uso en este ejemplo es '#9cc', pero puedes usar el que más te agrade. Lo que si te daré, es el tip de como dibujar una imagen para que siempre se escale al radio del círculo:
ctx.drawImage(img, sx, sy, sw, sh, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2);
Tu ya sabes que drawImage recibe 9 valores: la imagen de la hoja de sprites, el área origen de donde se toma la imagen, y el área destino donde será dibujada. Cuando dibujamos círculos antes, recordarás que lo hacemos de su posición (x,y) menos el radio en cada una de las coordenadas, por lo que solo resta indicar que el ancho y alto de la imagen a dibujar, que es el diámetro del círculo (el doble del radio).

Todo lo demás, ya lo has visto antes. Solo queda armar las piezas de lo antes aprendido, y aplicarlos en este juego. Las indicaciones que debe seguir este juego, son las siguientes:

- Todas las esferas comienzan con un radio de 20. Mientras su radio sea mayor de 10, este se reducirá en 1 (Hace el efecto que las esferas caen de lo alto).
- Al tomar una esfera, su radio obtiene el radio de 12 (Aparenta estar levantada por encima del nivel de las demás).
- Al entrar en contacto con el agujero, la esfera reduce su radio (Aparenta caer por el agujero). Al ser su radio menor a 1, la esfera se volverá a regenerar en un nuevo lugar del escenario.

Tengo confianza en que lograrán el objetivo. Demuéstrenlo dejando el resultado de su prueba en los comentarios. Si aun así, llegan a tener alguna duda durante el desarrollo del mismo, no tengan pena en preguntar.

¡Éxito a todos! ¡Felices códigos! ;)

Edición: Al parecer el juego en el blog no se comporta de forma muy fluida. Puedes ver un ejemplo directo y a pantalla completa en este enlace si lo prefieres: draganddrop.html

Juego final:

[Canvas not supported by your browser]

16 comentarios:

  1. Tu ejemplo no me funciona el "touch".
    lo mas interesante seria,algo como asi ;
    jugables=[];//los circulos
    //se acerca al agujero
    //efecto de irse por el agujero
    if (jugables.distancia(agujero)){
    //post-decremento
    jugables.radio--;
    }
    //Si desaparecio genera una nueva
    if(jugables.radio<1){
    jugables.x=MAth.random()*canvas.w
    jugables.y=Math.random() *canvas.h
    jugables.radio=20;
    }
    //efecto de caida
    if(jugables.radio>10){jugables.radio--}

    ResponderEliminar
    Respuestas
    1. ¿Cual es el problema que tienes, que no te funciona el ejemplo?

      Eliminar
  2. Bueno un amigo me pidio ayuda para resolver esto, ya tenia una parte hecha y queria que quedara como el que tienes,vi tus post y use tus metodos aunque hay cosas que no usaria pero xra empezar,lo hice lo mas practico, agregue comentarios para el necesarios, los eventos no son prciso pero hay esta todo para hacerlo jijij,espero que alguien mas le sirva,muy buena iniciativa,mas documentacion es español es la que necesitamos. Saludos

    var Global = (function(){

    var canvas,ctx,sprite=new Image(),jugador=new Circle(0,0,4),
    agujero=new Circle(100,100,20),jugables=[],puntero,juego={pausado:true,puntaje:0,tiempo:0},tiempoMax=20;
    //RUTA DE LA IMAGEN
    sprite.src="draganddrop.png";

    function inicia () {
    canvas=document.getElementById('canvas');
    ctx=canvas.getContext('2d');
    //mouse=captureTouch(canvas);
    //posicion=coordenadas(canvas,evento);
    puntero=captureMouse(canvas);
    arranca();
    };

    function arranca () {
    setTimeout(arranca,50)
    game();
    draw(ctx);
    }

    function repetir () {
    jugables.length=0;
    for(var i=0;i<3;i++){
    /*TODOs LOS CIRCULOS EMPIEZAN CON RADIO 20, X y Y aleatorio*/;
    jugables.push(new Circle((Math.random()*canvas.width),(Math.random()*canvas.height),20));
    };
    juego.tiempo=300;
    juego.puntaje=0;
    juego.pausado=false;

    }

    function game (){

    jugador.x=puntero.x;
    jugador.y=puntero.y;

    //Primero comprueba el valor y se disminuye
    juego.tiempo--;

    if(puntero.press && juego.tiempo < -tiempoMax){
    repetir();
    };


    if(!juego.pausado){

    for(var i=0;i10){
    local.radio--;
    }
    //Al ser seleccionado aumenta el radio
    // parace levantarse ooooooooo ¬¬
    if(puntero.press){
    if(jugador.distancia(local)<0){
    local.x=jugador.x;
    local.y=jugador.y;
    local.radio=12;
    }
    };
    //Se acabo el tiempo uuuuuuuuuuuuuuu
    if(juego.tiempo<1){
    juego.pausado=true;
    }
    }
    }
    };
    function draw () {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    /*Recorta el sprite
    80 ancho * 40 alto
    El ancho y alto es su radio,la poscion es la su pos actual menos sus radio,y su ancho y alto es el doble del radio (radio + radio) o (radio * 2)
    */

    ctx.drawImage(sprite, 40,0,40,40, agujero.x-agujero.radio,agujero.y-agujero.radio,agujero.radio+agujero.radio,agujero.radio+agujero.radio);

    for(var i=0,l=jugables.length;i0)
    //toFixed es una prop o metodo y representa el objeto como una string y devuelve
    //un valor redondeado
    ctx.fillText('TIEMPO: '+(juego.tiempo/tiempoMax).toFixed(1),150,10);
    else
    ctx.fillText('TIEMPO: 0.0',150,10);
    if(juego.pausado){
    ctx.textAlign='center';
    ctx.fillText('Agarra y suelta',100,135);
    //El texto tarda en aparecer
    if(juego.tiempo<-tiempoMax){
    ctx.fillText('Presiona click para iniciar',100,155);
    }
    ctx.fillText('PUNTAJE: '+juego.puntaje,100,175);
    ctx.textAlign='left';
    }
    };
    //publico
    return {
    init:function(){
    inicia();
    }
    }

    })();
    //Iniciamos eljuego
    Global.init();

    /*
    +++++++++CONSTRUCTOR CIRCULO+++++++++++++++++++++
    */
    function Circle(x,y,radio){
    this.x=x;
    this.y=y;
    this.radio=radio;
    };

    Circle.prototype.distancia=function(circulo){
    var dx=this.x-circulo.x;
    var dy=this.y-circulo.y;
    return (Math.sqrt(dx*dx+dy*dy)-(this.radio+circulo.radio));

    };

    Circle.prototype.dibujar=function(ctx){
    ctx.beginPath();
    ctx.arc(this.x,this.y,this.radio,0,Math.PI*2,true);
    ctx.fill();

    };

    ResponderEliminar
  3. La seguna parte

    /*
    EVENTOS MOUSE Y TOUCH
    RECIBEN COMO PARAMETRO UN ELEMENTO HTML Y DEVUELVEN UN OBJETO CON SUS PROPIEADES**********
    */
    function captureMouse(element) {
    var mouse = {x: 0, y: 0,press:false, event: null},
    body_scrollLeft = document.body.scrollLeft,
    element_scrollLeft = document.documentElement.scrollLeft,
    body_scrollTop = document.body.scrollTop,
    element_scrollTop = document.documentElement.scrollTop,
    offsetLeft = element.offsetLeft,
    offsetTop = element.offsetTop;

    element.addEventListener('mousemove', function (event) {
    var x, y;

    if (event.pageX || event.pageY) {
    x = event.pageX;
    y = event.pageY;
    } else {
    x = event.clientX + body_scrollLeft + element_scrollLeft;
    y = event.clientY + body_scrollTop + element_scrollTop;
    }
    x -= offsetLeft;
    y -= offsetTop;

    mouse.x = x;
    mouse.y = y;
    mouse.event = event;
    }, false);

    element.addEventListener('mouseup',function(evt){
    mouse.press=false;
    mouse.x=null;
    mouse.y=null;
    },false);
    element.addEventListener('mousedown',function(evt){
    mouse.press=true;
    },false);

    return mouse;
    };

    /*
    EVENTOS TOUCH***************************
    */

    function captureTouch (element) {
    var touch = {x: null, y: null, isPressed: false, event: null},
    body_scrollLeft = document.body.scrollLeft,
    element_scrollLeft = document.documentElement.scrollLeft,
    body_scrollTop = document.body.scrollTop,
    element_scrollTop = document.documentElement.scrollTop,
    offsetLeft = element.offsetLeft,
    offsetTop = element.offsetTop;

    element.addEventListener('touchstart', function (event) {
    touch.isPressed = true;
    touch.event = event;
    }, false);

    element.addEventListener('touchend', function (event) {
    touch.isPressed = false;
    touch.x = null;
    touch.y = null;
    touch.event = event;
    }, false);

    element.addEventListener('touchmove', function (event) {
    var x, y,
    touch_event = event.touches[0]; //first touch

    if (touch_event.pageX || touch_event.pageY) {
    x = touch_event.pageX;
    y = touch_event.pageY;
    } else {
    x = touch_event.clientX + body_scrollLeft + element_scrollLeft;
    y = touch_event.clientY + body_scrollTop + element_scrollTop;
    }
    x -= offsetLeft;
    y -= offsetTop;

    touch.x = x;
    touch.y = y;
    touch.event = event;
    }, false);

    return touch;
    };

    ResponderEliminar
  4. Hola Karl que piensas de ejecutar los bucles de los elementos draggables solo cuando se presiona el click esto con el fin de ahorrar recursos... sin embargo siguiendo este enfoque habría mas lineas de código, quedo atento.....

    ResponderEliminar
    Respuestas
    1. ¿Exactamente de que forma propones que se haga dicho proceso? No puedo ver como se puede detectar un posible drag solo al presionar el botón del ratón como preguntas tú.

      Eliminar
    2. Jaja, no disculpame no me hice entender bien, la idea es ejecutar el bucle solo cuando el click este presionado, ¿entiendes?

      Eliminar
    3. De hecho, eso mismo es lo que está ocurriendo, a través de esta línea:

      if(lastPress == 1){

      Eliminar
    4. Ah, si entiendo, estaba mirando el código de tu ejercicio, y como no usabas ese método entonces me asalto la duda...

      Eliminar
    5. ¡Ah sí! Ese cambio fue hecho en esta última versión para poder ajustar el tamaño de las esferas cuando caen, tanto cuando están más arriba, como cuando caen por el agujero. Por ello quedaron fuera del ciclo optimizado, pero es necesario para esta clase de detalles visuales...

      Eliminar
  5. Mira he trabajado una implementación que solo ejecutara los bucles cuando haya ocurrido una interacción con el mouse, de este modo se podría ahorrar algunos recursos en validaciones...

    ResponderEliminar
    Respuestas
    1. Jaja que tonto no he publicado el link, lo dejo aquí.

      http://jsfiddle.net/romualdo97/4ybx0t2x/

      Eliminar
    2. Bueno, en este ejemplo no se ejecuta la animación cuando sueltas una esfera fuera del agujero, ni cuando se regenera, pero sin duda es mucho más óptimo.

      Solo una duda... ¿Qué pasa si sueltas un objeto en el agujero cuando el anterior no ha terminado de caer aún? Creo que ese es otro detalle a considerar. Igual con la velocidad de caída, no creo que ese sea un problema en tu ejemplo.

      Eliminar
    3. Si tienes razon con lo de soltar dos esferas en el agujero, solo no lo he implementado por falta de variables, pero tienes razon con lo de la animacion.... um que lio, la implementare, mil gracias por tus sugerencias eres muy atento con todos los que preguntan...

      Eliminar
    4. ¡Gracias a ti por toda la retroalimentación! Preguntas y sugerencias como las tuyas, son lo que hacen a este blog mejor cada día.

      Eliminar