Aprende a crear juegos en HTML5 Canvas

lunes, 20 de agosto de 2012

Presionando el boton del raton

Anteriormente vimos como calcular la distancia entre dos objetos, y de esta forma, como poder interactuar con ellos. Ahora demos algo de funcionalidad a esto, creando un pequeño "Dispara al objetivo".

Si tomaste el curso original donde aprendimos a hacer el juego de la serpiente, notarás que mucho de esto es reciclado de la parte 4 "Interactuando otros elementos". Comencemos rescatando algunas de las variables usadas en esta sección, sus funciones ya las has de conocer:
    var lastPress=null;
    var score=0;
Como ya habrás supuesto, usaremos score para almacenar el puntuaje acumulado en el juego, y lastPress para almacenar, no esta vez la última tecla presionada en el teclado, si no el último botón presionado del ratón.

Nota: lastPress puede almacenar al mismo tiempo, tanto el último botón presionado del ratón, como la última tecla presionada por el teclado. Solo errará en almacenar ambas acciones, si son presionados al mismo tiempo con diferencia menor a media décima de segundo. Que esto suceda, es muy poco probable, sin embargo, si desarrollas un videojuego donde esperas que algo así pueda ocurrir, y ambas acciones son imprescendibles de ser leídas en el código al mismo tiempo, debes tomarlo en cuenta para evitar problemas, y en dado caso, almacenar ambas acciones en variables diferentes.

Pasemos a ver como almacenar en lastPress cuando el botón del ratón es presionado:
        document.addEventListener('mousedown',function(evt){
            lastPress=evt.which;
        },false);
Con este escucha, cada vez que el ratón sea presionado, asignaremos a lastPress el valor de evt.which, el cual nos indica el último botón del ratón presionado, siendo 1 para clic izquierdo, 2 para clic central, y 3 para clic derecho.

Ahora, modificaremos la parte en donde se medía la distancia entre player y target, para que al ser lastPress uno (clic izquierdo presionado), se ponga el fondo claro (efecto de disparo), y si la distancia entre player y target es menor de cero (colisionan), el score se aumente en uno, y el objetivo cambie de posición a otro lugar al azar en la pantalla, justo como la manzana del juego de la serpiente:
        if(lastPress==1){
            bgColor='#333';
            if(player.distance(target)<0){
                score++;
                target.x=random(canvas.width/10-1)*10+target.radius;
                target.y=random(canvas.height/10-1)*10+target.radius;
            }
        }
        else
            bgColor='#000';
Nota que a la nueva posición, le sumamos el radio de target, esto dado que se dibuja desde el centro y no desde la esquina superior derecha, como el rectángulo.

No olvides la función random que hemos usado en juegos anteriores:
    function random(max){
        return ~~(Math.random()*max);
    }
Y dibujar el score al final de la función paint, así como regresar lastPress a su valor nulo:
        ctx.fillStyle='#fff';
        ctx.fillText('Distance: '+player.distance(target).toFixed(1),0,10);
        ctx.fillText('Score: '+score,0,20);
        lastPress=null;
Al probar el juego, veremos nuestro resultado. Cada que disparemos a uno de los objetivos, nuestro score se sumará en uno, y el objetivo cambiará de posición en la pantalla.

Ahora, es posible que notes, que al hacer clic en cualquier lugar de tu página, el fondo se hará más claro, lo que significa el registro de un disparo. Lo mas lógico es que deseemos registrar solo cuando se presiona el ratón dentro de nuestro canvas, y no en cualquier lugar de nuestra página.

Para esto, debemos asignar el escucha del ratón precisamente al canvas, y no al documento. Pero si intentamos asignar este escucha antes de dar el valor inicial a nuestro canvas, esto producirá un error, ya que el valor predefinido de la variable canvas es nulo. Por eso, debemos asignar este escucha dentro del init.

Para poder manejar de forma más sencilla los escuchas, los juntaremos todos dentro de una función enableInputs. Modifica el init a forma que quede de la siguiente forma:
    function init(){
        canvas=document.getElementById('canvas');
        ctx=canvas.getContext('2d');
        canvas.width=300;
        canvas.height=200;

        enableInputs();
        run();
    }
Posteriormente, agrega este código en lugar de los actuales escuchas del ratón.
    function enableInputs(){
        document.addEventListener('mousemove',function(evt){
            mousex=evt.pageX-canvas.offsetLeft;
            mousey=evt.pageY-canvas.offsetTop;
        },false);
        canvas.addEventListener('mousedown',function(evt){
            lastPress=evt.which;
        },false);
    }
Como veremos, ahora agregamos el escucha del mousedown a nuestro canvas justo después de darle su valor inicial, dentro de la función init.

Al final, el código se verá como mostraremos más adelante, pero antes, les dejaré una tarea a través de nuestra nueva dinámica...


El reto de la semana.

Ya tenemos las bases para un juego nuevo en este momento, pero le hacen falta gráficos y sonidos. Tú ya aprendiste a hacer esto cuando hicimos juntos el curso del juego de la serpiente, por lo que tienes todas las habilidades necesarias para lograrlo. ¡Adelante!

Solo te daré un consejo, para prevenir un posible error. Recuerda que los círculos se dibujan desde el centro y no desde el punto superior izquierdo, como los rectángulos o las imágenes. Es por eso, que al dibujar la imagen a un círculo, debes restar su radio a la posición inicial, de esta forma:
        ctx.drawImage(img,this.x-this.radius,this.y-this.radius);
¡Mucha suerte!

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var lastPress=null;
    var mousex=0,mousey=0;
    var score=0;
    var bgColor='#000';
    var player=new Circle(0,0,5);
    var target=new Circle(100,100,10);

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

        enableInputs();
        run();
    }

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

    function run(){
        requestAnimationFrame(run);
        act();
        paint(ctx);
    }

    function act(){
        player.x=mousex;
        player.y=mousey;

        if(player.x<0)
            player.x=0;
        if(player.x>canvas.width)
            player.x=canvas.width;
        if(player.y<0)
            player.y=0;
        if(player.y>canvas.height)
            player.y=canvas.height;

        if(lastPress==1){
            bgColor='#333';
            if(player.distance(target)<0){
                score++;
                target.x=random(canvas.width/10-1)*10+target.radius;
                target.y=random(canvas.height/10-1)*10+target.radius;
            }
        }
        else
            bgColor='#000';
    }

    function paint(ctx){
        ctx.fillStyle=bgColor;
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#f00';
        target.stroke(ctx);
        ctx.strokeStyle='#0f0';
        player.stroke(ctx);

        ctx.fillStyle='#fff';
        ctx.fillText('Distance: '+player.distance(target).toFixed(1),0,10);
        ctx.fillText('Score: '+score,0,20);
        lastPress=null;
    }

    function enableInputs(){
        document.addEventListener('mousemove',function(evt){
            mousex=evt.pageX-canvas.offsetLeft;
            mousey=evt.pageY-canvas.offsetTop;
        },false);
        canvas.addEventListener('mousedown',function(evt){
            lastPress=evt.which;
        },false);
    }

    function Circle(x,y,radius){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?0:radius;
    }

    Circle.prototype.distance=function(circle){
        if(circle!=null){
            var dx=this.x-circle.x;
            var dy=this.y-circle.y;
            return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
        }
    }
    
    Circle.prototype.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }

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

30 comentarios:

  1. Buenas, quería hacerte una consulta con esta parte, ya que haciendo algunas pruebas, no estoy seguro en que me estoy equivocando o si se puede hacer. el problema que yo tengo es que quiero crear circulos con un array con el mouse.
    var enemies=[];//array

    player.x=mousex;
    player.y=mousey;

    if(lastKey == 1){

    enemies.push(new Circle(player.x,player.y,10));
    //lastKey=null;
    }
    //para moverlos
    for(i=0;i<0;i++){
    enemies[i].y+=10;
    if(enemies[i].y>canvas.height)
    enemies.splice(i--,1);
    }

    y los dibujo

    for(i=0;i<0;i++){

    ctx.strokeStyle='#f00';
    ctx.beginPath();
    ctx.arc(enemies[i].x,enemies[i].y,enemies[i].radius,0,Math.PI*2,true);
    ctx.stroke();
    }

    espero tu repuesta. Gracias

    ResponderEliminar
    Respuestas
    1. Creo que el problema es que en el for comparas con i<0 en lugar de i<enemies.length. Revisa si eso resuelve el problema.

      Eliminar
  2. Gracias karl tayfer, ese era el problema... lo dejo aca por si alguien le sirve

    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var lastKey=null;
    var mousex=0,mousey=0;
    var score=0;
    var player=new Circle(0,0,5);
    //var enemies=new Circle(100,100,10);
    var enemies=[];

    //var target=new Circle(100,100,10);

    function init(){
    canvas=document.getElementById('canvas');
    canvas.style.background='#000';
    ctx=canvas.getContext('2d');
    canvas.addEventListener('mousedown',function(evt){
    lastKey=evt.which;
    },false);

    run();
    }

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

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

    function game(){
    player.x=mousex;
    player.y=mousey;

    if(player.x<0)
    player.x=0;
    if(player.x>canvas.width)
    player.x=canvas.width;
    if(player.y<0)
    player.y=0;
    if(player.y>canvas.height)
    player.y=canvas.height;




    // New Shot
    if(lastKey == 1){
    //enemies.push(new Rectangle(mousex,mousey,10,10));
    enemies.push(new Circle(player.x,player.y,10));
    //lastKey=null;
    }

    // Move Shots
    for(i=0;icanvas.height+20)
    enemies.splice(i--,1);
    }

    }

    function paint(ctx){
    ctx.clearRect(0,0,canvas.width,canvas.height);

    for(i=0;irect.x&&
    this.yrect.y);
    }
    }
    }

    function Circle(x,y,radius){
    this.x=(x==null)?0:x;
    this.y=(y==null)?0:y;
    this.radius=(radius==null)?0:radius;

    this.distance=function(circle){
    if(circle!=null){
    var dx=this.x-circle.x;
    var dy=this.y-circle.y;
    return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
    }
    }
    }

    ResponderEliminar
  3. disculpa me podrias ayudar a crear una sopa de letras

    ResponderEliminar
    Respuestas
    1. Con gusto resolveré las dudas que tengas. ¿Que llevas hecho?

      Eliminar
  4. Hola que tal, tengo un problemilla. Tengo la funcion cam, y no se como nivelar el punto del raton ya que me aparece mas arriba que el cursor.
    Saludos

    ResponderEliminar
    Respuestas
    1. En la función enableInputs donde calculas la posición del ratón, debes sumar la posición de la cámara para obtener su valor relativo al movimiento de la cámara.

      Eliminar
  5. mousex=evt.pageX-canvas.offsetLeft+cam.x;
    mousey=evt.pageY-canvas.offsetTop+cam.y;

    sigue apareciendo mal, como 100px mas abajo y 40 a la derecha. :(

    Gracias por tu tiempo man

    ResponderEliminar
    Respuestas
    1. ¿Es el único caso que podría estar afectando tu posición? ¿Cuando la cámara esta en la esquina superior y derecha, funciona bien?

      Eliminar
  6. Buenas, tengo una duda sobre una trayectoria parabolica de un objeto. Por mucho que lo pienso no doy con ello. Necesito increementar player.velX y player.velY de forma que haga la trayectoria de la parabola y acabe en el punto que yo le doy. La parabola es dada 3 puntos en inicial, medio y el final..

    No se si me podrás ayudar.

    ResponderEliminar
    Respuestas
    1. ¿Cuál es la fórmula que debes seguir, o las constantes que debes tomar en cuenta? Comúnmente esta clase de movimientos se da por algún factor como la gravedad... ¿Necesitas calcularle en tiempo real, o todo como un punto previo?

      Eliminar
    2. Primero lo fijo con el click y guardo la posicion, al soltar el click vuelvo a calcular la parabola dados esos puntos y los valores xt y yt se los paso al player.x y player.y. Al calcular la parabola le aplico aceleración. El problema creo que es de la camara. El player tiene gravedad de 0.04.

      Asi calculo la parabola previa que se muestra con puntos hasta ahi bien mas o menos hay que retocar el eje. Pero bien


      //coordenadas parabola
      p0 = {x:player.x-cam.x, y:player.y-cam.y}; //inicio
      if(mousex>player.x) p1 = {x:player.x+102-cam.x, y:player.y-150-cam.y};
      if(mousex 1) { //ha terminado
      player.t = 1;



      }






      points.push({x:xt,y:yt});

      // dibujamos trayectoria
      for (var i = 0; i< points.length; i++) {
      ctx.drawImage(playerd, points[i].x, points[i].y,3,3);
      }



      Al soltar el click, vuelvo a calcular lo mismo pero se lo paso al player y ahi peta del todo y se descuadra todo. Creo que es de la camara ya que la linea de puntos sale perfecta. No cojo bien los datos con el ratón y puede que se lie todo con eso + la camara. Soy el mismo del comentario de arriba

      El raton lo tengo como te comenté arriba restando la camara, pero conforme voy hacia la derecha el objeto que le he asignado las varibles del raton empieza a irse mas a la derecha.

      Vaya tocho te he escrito!!! LLevo desde las 9 de la mñana con esto ^-^

      Muchas gracias por leerme

      Eliminar
    3. Perdona he copiado mal el codigo, es asi

      p0 = {x:player.x-cam.x, y:player.y-cam.y}; //inicio
      if(mousex>player.x) p1 = {x:player.x+102-cam.x, y:player.y-150-cam.y};
      if(mousex 1) {
      player.t = 1;

      }

      //dibujamos la trayectoria
      ctx.drawImage(punto,xt, yt);

      Eliminar
    4. ¿Puedes poner tu ejemplo completo en un JSFiddle si no lo tienes ya en línea? Creo que ver el código completo en funcionamiento me ayudará a encontrar de forma mas rápida y certera el origen del problema.

      Eliminar
  7. No se que pasa!!! no sale mi codigo aqui

    p0 = {x:player.x-cam.x, y:player.y-cam.y}; //inicio
    if(mousex>player.x) p1 = {x:player.x+102-cam.x, y:player.y-150-cam.y}; //infelxion
    if(mousex 1) {
    player.t = 1;



    }




    points.push({x:xt,y:yt});

    // puntos
    for (var i = 0; i< points.length; i++) {
    ctx.drawImage(playerd, points[i].x, points[i].y,3,3); // 10 x 10 es el tamaño
    }

    ctx.closePath();
    ctx.drawImage(playerd,xt, yt);

    ResponderEliminar
    Respuestas
    1. Este código se ve ya demasiado complejo tomando en cuenta todas las variantes. ¿Tienes tu código en línea en algún lugar para poder revisarlo?

      Eliminar
    2. Si perdona, es que no se que pasa que no se copiaba. Te dejo el código para tener la trayectoria de la parabola, funciona. EL problema viene que no quiero la trayectoria en x,y sino el incremento que le debo dar al player, es decir player.velX y player.velY.

      Ya que solo con x,y seria player.x = xt y player.y=yt
      lo que me deja fuera la friccion y gravedad. Te dejo el codigo de la parabola. No se como sacar de ahí el incremento necesario para trazarla.

      http://www.copiatelo.com/index.php?show=m350c5c3b

      Eliminar
    3. La palabra "trazarla" me hace confundir tus intenciones específicas.

      ¿Quieres dibujar la curva antes de que el personaje ejecute la acción? ¿O tu intensión que es que el personaje simplemente ejecute la parábola conforme avance?

      Intentando deducir que es lo segundo, creo que lo que buscas es esto:

      player.vx *= friction;
      player.vy += gravity;
      player.x += player.vx;
      player.y += player.vy;

      Eliminar
    4. La dibujo antes de que ejecute la accion. Hago click y se ve la parabola, suelto el click y quiero que la haga el player. Pero necesito calcular que incremento necesita. Vaya es como angry birds, primero se ve la parabola y luego se hace.

      Eliminar
    5. Ok, me queda mucho más claro ahora. Estos son dos formas diferentes de aplicar los mismos parámetros, ya que uno requiere que los cálculos sean en tiempo real, para lo cual debes de aplicar las fuerzas, y el otro requiere calcularlos previamente, para lo cual necesitas aplicar una fórmula.

      Ahora, la formula puede cambiar ligeramente dependiendo como apliques las fuerzas constantes, así que para ayudarte mejor, necesito saber los valores de las constantes que estés aplicando (Fuerza, fricción, resistencia, viento, etc), y la forma en que las estás aplicando a su movimiento.

      Creo haber entendido que ya tenías uno de los dos y solo te faltaba el otro ¿Es esto cierto? Y en dado caso, ¿Cuál es el que te falta?

      Eliminar
    6. en http://www.copiatelo.com/index.php?show=m350c5c3b se hace una parabola dada 3 puntos y se imprime en pantalla. Hasta ahi bien. Ahora quiero que la haga el player pero no pasandole coordenadas x e y sino fuerza y angulo supongo. Pero ahi ya me pierdo. Es como angry birds primero dibuja una parabola y luego la hace. Eso pero en html5.
      Es muuucho mas dificil de lo que pensaba.

      Eliminar
    7. ¿Pero cuales son las fuerzas que le aplicas? Intenté replicar tu proyecto con el código que me pasaste, pero me hacen falta demasiados datos para saber como llegaste a ese punto, por lo que no pude hacerlo.

      ¿Puedes decirme al menos las constantes que estás aplicando y como deberían afectar al personaje?

      Eliminar
  8. Las fuerzas dan igual, si supieras hacer un ejemplo con las fuerzas que tu quieras sería genial yo no doy para mas. Es "sencillo" con el raton haces una parabola dependiendo de como lo muevas y al solar el click el player la hace.

    ResponderEliminar
    Respuestas
    1. Bueno, revisa este ejemplo y checa si ilustra un poco tu camino. Solo tiene una sencilla fuerza de gravedad, ya sería cuestión que tu lo amplíes el código como necesites:

      http://jsfiddle.net/daPhyre/on4ay7mk/

      Eliminar
    2. Yeahh tio, es justo lo que buscaba!! PERFECTO QUE MAQUINA! Te voy a donar algo para que te eches unas cañas a mi salud. Muchas gracias por tu tiempo y dedicacion

      Eliminar
    3. ¡Muchas gracias! ¡Me alegra saber que te ha servido! ¡Mucha suerte!

      Eliminar
  9. Saludos, no se mucho de canvas, como hago para que un cuadro se mueva hazta chocar con otro pero que el cuadro que seleccione para moverse se resalte y que aparezcan flechas a las direcciones que lo puedo mover y que al precionarlas se mueva hacia dicha direccion?

    Perdon por mi falta de conocimiento en esta area, si tienes algo que me sirva de referencia para lo grar lo que quiero te lo agradeceria mucho!

    ResponderEliminar
    Respuestas
    1. No he comprendido muy claro lo que intentas, pero creo que podría ser una fusión de estas dos lecciones:

      http://juegos.canvas.ninja/2013/03/objetos-solidos.html
      http://juegos.canvas.ninja/2013/09/botones-en-pantalla.html

      Espero te ayude. ¡Felices códigos!

      Eliminar
  10. e men si me puedes ayudar busco un ejemplo de un botón en el canvas osea die click a un rectángulo y me lanze una alerta

    ResponderEliminar
    Respuestas
    1. Revisa este tema:

      http://juegos.canvas.ninja/2015/03/rectangulos-y-el-raton.html

      La única diferencia, sería quitar el efecto drag, y que al momento de liberar el mouse sobre el rectángulo, realice la acción que ocupas. ¡Éxito!

      Eliminar