Videojuegos con processing

Artículo que explica los conceptos básicos de la programación de videojuegos implementados con el lenguaje Processing. Se plantea la construcción de dos sencillos juegos, el Buscaminas y Destruir el tanque, con los que el autor explicará los conceptos más relevantes de los algoritmos utilizados.

Por Pedro Puertas el 20/09/2017

Processing es un lenguaje basado en Java que permite de una forma rápida y fácil realizar proyectos visuales. Seguramente no es tan óptimo como para crear juegos del nivel de los creados con Unity u otro tipo de motores, pero es una muy buena opción para aprender los conceptos básicos que hay detrás de todo videojuego.

¿Que es un videojuego?

Podemos definir un videojuego como un juego que se desarrolla en una pantalla y en el que una o más personas interactúan mediante algún tipo de dispositivo electrónico. Los videojuegos se pueden clasificar en las siguientes categorías, aunque dentro de ellas podemos encontrar numerosas subcategorías.

  • De tablero. Son juegos que simulan un juego de tablero convencional, serían por ejemplo el solitario, el parchís, o la guerra de barcos. En esta categoría también se pueden englobar juegos más actuales como el Apalabrados, el Triviados o el Candy Crush.
  • Plataformas. Son juegos en el que hemos de controlar un personaje en tercera persona y se han de superar ciertos obstáculos y luchar contra diferentes enemigos hasta conseguir llegar a un punto que nos permite cambiar de nivel. Este tipo de juegos son por ejemplo el Mario Bross, Sonic, etc.
  • Estrategia, Son juegos en los que es necesario realizar una serie de acciones concretas para superar cada nivel, suelen ser simuladores de civilizaciones del tipo Minecraft, Age of Empires o Sim City, antiguamente eran en 2D, pero con la potencia actual de las gráficas actualmente todos son en 3D.
  • Colaborativos, Son juegos para jugar en grupo, en general son juegos que mezclan la esencia de los juegos de plataformas con los de estrategia teniendo que jugar con algún compañero para poder superar con éxito cada nivel. Por ejemplo Gears of war, Super Mario 3D World o Little Big Planet.
  • Primera persona. En el que controlamos un personaje y visualizamos el escenario a través de sus ojos, aunque en algunos juegos es posible cambiar la vista a tercera persona. Este tipo de juego se hizo muy famoso con la llegada de Wolfenstein 3D y Doom.

Hay un aspecto en todo juego muy importante que no hay que olvidar, y es que la misión principal de cualquier juego es la de divertir, por lo que sin duda el factor más determinante de que un juego guste y tenga éxito es que tenga un buen argumento. De nada sirven espectaculares gráficos si posteriormente el argumento del juego o su jugabilidad es deficiente. Juegos como Angry birds o Candy Crush no tienen grandes gráficos, sin embargo, son juegos que han atrapado a millones de personas. Por ello es importante cuando se diseña un videojuego el plantear un buen argumento en el que haya suficientes retos para que el espectador se sienta atraído por ellos. Otro aspecto imprescindible es que el juego siempre se debe poder resolver, aunque tenga un nivel muy alto de dificultad el juego debe poder terminarse, en caso contrario el jugador se sentirá engañado y perderá el interés por jugar.

Programando un videojuego

Los primeros videojuegos requerían que se desarrollaran en lenguaje ensamblador, ya que la potencia de los procesadores era muy escasa y el único modo de conseguir procesos optimizados era el de programar directamente en lenguaje máquina. Con la llegada de procesadores más potentes el lenguaje preferido fue C, un lenguaje de alto nivel pero con un gran control sobre la memoria y los procesos del procesador, permitiendo un alto nivel de optimización en el proceso de compilación.

Con la llegada de los primeros móviles, se estandarizó mucho el desarrollo de juegos en Java, ya que permitía una gran compatibilidad con muchos tipos de dispositivos. Aunque Java es un lenguaje que no ejecuta código nativo, las últimas máquinas virtuales permiten un alto grado de optimización, hay juegos como Minecraft que están desarrollados en Java que aprovechan la compatibilidad con OpenGL para poder representar gráficos avanzados.

En los últimos años, con la llegada de múltiples dispositivos y sistemas operativos, han aparecido plataformas de desarrollo especializadas, la más conocida es Unity, que permite desarrollar código en C# y Unityscript (un derivado de Javascript adaptado al entorno de Unity) y que tiene ciertas similitudes estructurales con Processing, ya que incluye una función Start que seria la análoga a setup y Update que equivaldría a draw.

En este artículo vamos a plantear el desarrollo de dos tipos de videojuegos muy básicos pero que nos permitirán aprender diferentes aspectos comunes a la mayoría de videojuegos, como son las máquinas de estados, gestión de animaciones, recursividad, etc.

Buscaminas

El juego del buscaminas se hizo muy popular con la llegada de Window95, es un juego simple pero adictivo. Se trata de localizar las bombas situadas en un tablero. Cada vez que se pulsa una celda que no contenga una bomba, indica el número de bombas adyacentes a esa casilla, de forma que podamos predecir donde pueden estar cada una de las bombas.

Uno de los conceptos más habituales en cualquier juego es el disponer de una máquina de estados. Básicamente al definir el juego, definimos los diferentes estados en los que está el juego, por ejemplo, en este caso tenemos INSTRUCCIONES, JUGANDO, GANADOR, PERDEDOR.

La máquina de estado se inicia en el modo instrucciones, eso significa que mientras esté en ese estado se mostrará una pantalla de bienvenida con las instrucciones del juego. Al pulsar una tecla o el botón de la pantalla, cambiaremos de estado y pasaremos a estado JUGANDO. En este estado se presentará una pantalla formada por una cuadrícula de 10 x 10, cada una de las celdas puede contener una bomba.


El juego se ha estructurado en tres clases:

Buscaminas, es la clase que contiene el método de inicialización Setup y el de dibujado draw. Contiene además la máquina de estados.

Tablero, que representa el tablero del juego, contiene diversas rutinas para controlar el estado del juego.

Celda, el tablero está formado por una rejilla de celdas. Cada celda puede esconder una bomba o estar vacía.

Una máquina de estados por norma se implementa como una o más variables de tipo entero que identifican que momento del argumento o historia se está ejecutando. En nuestro caso la variable se ha denominado estado y permite los estados INSTRUCCIONES, JUGANDO, GANADOR y PERDEDOR.

         
        // Maquina de estados
        static final int INSTRUCCIONES = 0;
        static final int JUGANDO = 1;
        static final int GANADOR = 2;
        static final int GAMEOVER = 3;
         
        int estado = INSTRUCCIONES;
      


El punto donde se controla en que estado estamos es normalmente en la rutina de pintado, al tratarse de una variable entera, la forma más optima de controlar su valor es mediante la sentencia switch que es más optima que enlazar diferentes sentencias if – else

     
    /**
     Pintado del lienzo dependiendo del estado
     */
    void draw() {
      background(0);
      // Pintamos el texto con los créditos de autoría
      fill(255, 255, 0);
      textFont(font12, 12);
      text("CC-BY Pedro Puertas Estivill", width/2, height-20);  
    
      // Dependiendo de la máquina de estados pintamos las instrucciones, el tablero o 
    
      // el indicador de que ha ganado o perdido
    
      switch (estado) {
      case INSTRUCCIONES:
        fill(255);
        textFont(font16, 16);
        text("Buscaminas", width/2, 20);
        textFont(font12, 12);
        text("Debes encontrar las "+tablero.num_bombas+" bombas que ", width/2, 40);
        text("hay escondidas en el tablero", width/2, 52);
        text("Pulsa una tecla para empezar", width/2, 72);
        return;
      case GANADOR: 
        fill(0, 155, 0);
        textFont(font16, 16);
        text("BIEN!!! HAS GANADO", width/2, height-70);
        fill(255);
        textFont(font12, 12);
        text("Pulsa una tecla para otra partida", width/2, height-50);
        break;
      case GAMEOVER: 
        fill(255, 0, 0);
        textFont(font16, 16);
        text("GAMEOVER", width/2, height-70);
        fill(255);
        textFont(font12, 12);
        text("Pulsa una tecla para otra partida", width/2, height-50);
        break;
      }
      // Siempre pintamos el tablero excepto en las instrucciones que vuelve directamente
    
      tablero.draw();
    }
     
  


Otro de los puntos donde se controla la máquina de estados es en las rutinas de teclado, ratón o joystick. Porque dependiendo de en que estado estemos los eventos se omiten.

     
    /**
     Control del teclado
     */
    void keyPressed() {
      //println(key);
      //println(keyCode);
      if (key == CODED) return; 
      // Dependiendo del estado actual hacemos una acción u otra 
      switch (estado) {
      case INSTRUCCIONES:
        estado = JUGANDO; // Pasamos a jugando si está mostrando las instrucciones
        return;
      case GANADOR:  
      case GAMEOVER:
        // Reiniciamos el tablero para iniciar una nueva partida
        int nb = 2 + (int)random(8);
        num_celdas = 5 + nb;
        if (num_celdas < 10) num_celdas = 10;
        tablero = new Tablero(num_celdas, nb*3);
        // Cambiamos el tamaño de la pantalla que es aleatorio.
        surface.setSize(ancho*num_celdas, ancho*num_celdas+100);
        estado = JUGANDO;
        return;
      }
    }
     
  


La clase Tablero es la más importante del juego, es la que controla la distribución de las bombas, el control de las celdas, etc. En el artículo voy a comentar la función principal que utiliza recursividad para gestionar el click del ratón sobre una celda, el resto del código está comentado en detalle en la propia clase que puede descargarse al final del artículo.

La recursividad es la capacidad que tiene una función de llamarse a si misma para ejecutar una operación repetitiva, por ejemplo para calcular el factorial de un número. Se utiliza principalmente para simplificar el código y ahorrar controles sobre el estado de la operación. Otro ejemplo en que se utiliza mucho es en la generación de fractales ya que son figuras que se representan al producirse múltiples iteraciones.

     
    int destapa(int x, int y) {
      // Calculamos a partir de las coordenadas de la pantalla que celda hay que destapar
    
      int i = x/ancho;
      int j = y/ancho;
        
      // Nos aseguramos que la celda calculada esté dentro del rango del tablero
    
      if (i >= 0 && i < columnas && j >= 0 && j < filas) {
        // Si la celda está marcada volvemos
    
        if (tablero[i][j].marcada) return -2;
        // Si hay una bomba o ya está destapada retornamos el estado de la celda
    
        if (tablero[i][j].estado == BOMBA || tablero[i][j].visible) return tablero[i][j].estado;
        // Si la celda no está vacía y no es una bomba, destapamos la celda, descontamos las celdas
    
        // pendientes para controlar si el usuario ha ganado y retornamos su valor
    
        if (tablero[i][j].estado > VACIO) {
          tablero[i][j].visible = true;
          no_visibles--;
          return tablero[i][j].estado;
        }  
        // Si la celda está vacía, destaparemos la celda clicada y todas las celdas adyacentes
    
        if (tablero[i][j].estado == VACIO) {
          tablero[i][j].visible = true;
          no_visibles--;
          destapa(x-ancho, y-ancho); // Destapar esquina superior izquierda
    
          destapa(x, y-ancho);       // Destapar celda superior
    
          destapa(x+ancho, y-ancho); // Destapar esquina superior derecha
    
          destapa(x-ancho, y);       // Destapar celda izquierda
    
          destapa(x+ancho, y);       // Destapar celda derecha
    
          destapa(x-ancho, y+ancho); // Destapar celda inferior izquierda
    
          destapa(x, y+ancho);       // Destapar celda inferior
    
          destapa(x+ancho, y+ancho); // Destapar celda inferior derecha
    
        }
      }
      return VACIO;
    }
     
  

¿Por qué es interesante el uso de la recursividad en este control?

Porque nos permite optimizar el código ya que el control de si está marcada, si tiene una bomba, etc. de cada una de las celdas sólo se hace sólo una vez. El resultado reproduce la acción de que el usuario hubiera clicado las celdas del tablero. Es muy importante que una función recursiva no sea infinita, ya que si no el sistema se bloqueará. En este caso al realizar los diferentes retornos antes de ejecutar las llamadas a la misma función controlan que en algún momento termine. Si una rutina recursiva está mal programada, se agotará la pila de variables (stack) y el sistema acabará lanzando una excepción de error.

La clase Celda es la encargada de pintar cada una de las celdas, a partir de las variables estado, visible y marcada se controla el tipo de celda y si está o no destapada.

En este primer juego hemos visto varios aspectos importantes a destacar:

  • Organización de los elementos por clases, en las que cada clase representa un objeto y tiene sus propias variables y funciones.
  • Máquina de estados, que controla el estado general del juego mediante una variable de tipo numérico.
  • Recursividad, recurso de programación que permite escribir código optimizado para la gestión de tareas repetitivas.

Tiro al tanque

Este juego sería una versión conceptualmente muy simplificada de un juego tipo Angry birds, pero en lugar de utilizar pájaros se utilizan proyectiles, y el objetivo es destruir un tanque que se acerca a nuestro cañón. El juego es muy simple, pero nos va a permitir estudiar los siguientes conceptos:

  • Animación de objetos
  • Física en los videojuegos
  • Detección de colisiones


Animación de objetos

Podemos definir la animación de un objeto como todo cambio que se produce en la pantalla que simula el movimiento real del objeto que representa. Para que el cerebro interprete un cambio de imagen como un movimiento real (persistencia retinaria), es necesario que estos cambios se produzcan a una velocidad constante a un mínimo de 12 o 14 frames, aunque las velocidades ideales se sitúan por encima de los 24 frames en el cine o los 30 frames de la televisión. Actualmente la mayoría de juegos gracias a las tarjetas gráficas de última generación permiten velocidades cercanas a los 120 frames por segundo.

Las animaciones se producen a partir de los siguientes cambios:

  • Traslación. Una parte de la pantalla (objeto) se desplaza por la pantalla.
  • Rotación. Un objeto gira sobre un punto de la pantalla.
  • Forma. Un objeto cambia su forma o su morfología.

Los diferentes movimientos pueden combinarse, así por ejemplo un personaje caminando es producido por un cambio en la forma y un desplazamiento por la pantalla.

Sprites

Los objetos que se mueven por la pantalla se denominan SPRITES, se hicieron muy populares en la época de los 80 especialmente con el Commodore 64 y con los MSX, estos miniordenadores incorporaban un chip gráfico con soporte por hardware de Sprites de hasta 16 colores, actualmente los sprites se generan por software a partir de un conjunto de imágenes. En este juego la rana, el tanque y la nube son sprites, la rana simula una animación completa realizando transformaciones de forma, translación y rotación.

Para implementar un Sprite, se ha creado una clase que se encarga de realizar el movimiento en cada frame, por lo que una vez creada la clase, se pueden incorporar múltiples objetos en movimiento para crear un juego. Para aplicar animación de forma, es necesario generar una imagen con pequeños cambios por frame.

El archivo rana.png es un archivo PNG con transparencia que está formado por 8 imágenes de 64 x 128, en este caso tiene más altura para realizar el salto (frames 2 y 3).

El procedimiento de pintado es muy simple, se carga en memoria toda la imagen en un objeto Pimage, cada vez que el sistema requiere pintar el objeto, se copia la porción del frame que toca en una imagen intermedia y en la posición indicada. Una vez pintado, se incrementa el número de frame a pintar en la siguiente llamada.

Veamos el código detallado paso a paso

     
     
      /**
       Función de pintado, se le puede indicar el frame exacto o -1 si queremos que haga la
       animación
       
       @param nf - Número de frame a pintar. Si -1 pinta animación
       */
      void draw(int nf) {
        
        if (!visible) return;
        
        // Si el número de frame es negativo, cambiamos de frame a la velocidad fijada
        if (nf < 0) {
          nFrame += speed;
          // Si llegamos al últio frame volvemos al primero
          if (nFrame >= nFramesX*nFramesY) nFrame = 0;
        } else {
          // Fijamos el frame 
          nFrame = nf;
        }
        // Calculamos el ancho i alto del sprite a partir del número de frames en X e Y
        int w = img.width/nFramesX;
        int h = img.height/nFramesY;
     
        // Crea una imagen temporal para pintar del ancho y alto declarado
        PImage frame = createImage(this.wi, this.he, ARGB);
     
        // Calculamos el trozo de imagen que hemos de tomar desde la imagen completa y se escala 
        // al ancho y alto declarado. 
        frame.copy(img, (int)(nFrame%nFramesX)*w, (int)(nFrame/nFramesX)*h,w,h,0,0,this.wi,this.he);
     
        // Como la función scale afecta al sistema de coordenades de todo el escenario, primero 
        // hemos de guardar su estado con la instrucción pushMatrix despues lo escalamos, pintamos 
        // y volvemos a recuperar el estado guardado (popMatrix)
        pushMatrix();            // Guardamos el estado actual
        // Escalamos a flip (si es -1 hace un reflejo horizontal, el vertical no se modifica
        scale(flip, 1.0);
        // Si lo hemos volteado, les coordenades van justo al revés, es decir el punto 0 està a la 
        // derecha, lo controlamos multiplicando X por la variable flip
        image(frame, x*flip, y);
        // Una vez pintado en la pantalla, recuperamos el estado del sistema para que no afecte al 
        // resto de objetos
        popMatrix();               
    }
     
     
  


La parte más importante es el cálculo de la porción a copiar, los frames se numeran de izquierda a derecha y de arriba abajo. Por tanto el frame 5 (rana con los ojos cerrados) es la fila 1 y columna 1 para calcular la fila y columna se realizan las siguientes operaciones:

Columna = nframe%nFramesX -> 5 % 4 frames = 1
Fila = nFrame/nFramesX -> 5 / 4 frames = 1

Para calcular la coordenada X e Y del trozo de la imagen a copiar se multiplica el número de columna calculada por el ancho y la fila por el alto. Por tanto la esquina superior izquierda del frame 5 está ubicada en la posición x= 164 y = 1 128, por lo que para pintar el frame copiaremos desde la posición (64, 128) una porción de 64 x 128 píxeles. El frame 2 estaría en la posición (192, 0), etc.

Otro aspecto importante es como se realizan las transformaciones en Processing. La forma más fácil de entenderlo es pensando que el lienzo de Processing es como una hoja de papel. La hoja está situada paralela en su posición inicial (pushMatrix), pero para dibujar objetos rotados o volteados es más cómodo rotar o voltear la hoja (scale) en lugar de forzar el brazo para dibujar un trazo que queremos rotar. Una vez dibujado en la posición correcta, volvemos a situar el papel en la posición horizontal para dibujar otros objetos (popMatrix). Las transformaciones son acumulativas por tanto si se realizan traslaciones, escalados o rotaciones cada operación se hace sobre la transformación existente, por lo que se debe tener en cuenta de recuperar el estado entre transformaciones para no obtener resultados inesperados.

Física en los videojuegos

La física es un elemento muy importante dentro del mundo de los videojuegos, ya que es la vía para reproducir fenómenos reales en un medio virtual. Cuanto mejor se apliquen las fórmulas físicas en los algoritmos de los programas, más real serán los movimientos en un juego.

Algunas de las aplicaciones de la física en los videojuegos son:

  • Caída de objetos (Ley de la gravedad)
  • Lanzamiento de proyectiles (Tiro parabólico)
  • Sistemas de partículas, permiten la simulación de humo, precipitaciones, etc.
  • Fricción, los cuerpos que se deslizan tienden a frenarse dependiendo del material de la superficie y el objeto.
  • Restitución. Calcula la capacidad de rebote de un objeto contra otra superficie.


En nuestro juego, concretamente usaremos el tiro parabólico, que está compuesto por dos tipos de movimientos, el movimiento rectilíneo uniforme horizontal y el movimiento rectilíneo uniformemente acelerado vertical, donde la aceleración viene dada por la fuerza de la gravedad, las variables que intervienen son:

  • Fuerza de la gravedad 9,8 m/s
  • Velocidad inicial (velocidad de lanzamiento m/s)
  • Ángulo de lanzamiento
  • Altura de lanzamiento (en este caso 0 m)

En el juego, el usuario puede determinar los valores de las variables V0 y el ángulo (), la variable t serán los frames que transcurren con la animación desde el momento del lanzamiento y la variable g es una constante que representa la fuerza de la gravedad con valor 9.8. La rutina que dibuja la bala en cada instante de tiempo es la siguiente:

     
    void pinta_bala() {
      // La bala sólo se pinta si han disparado
      if (estado == DISPARANDO) {
        // Uso de la fórmula de tiro parabólico
        // x = x0 + v0*cos(angulo)*t
        // y = y0 - v0*sin(angulo)*t-g*t^2/2
        
        // Se realiza una adaptación de la posición inicial dependiendo del ángulo que tiene el 
        // cañón en la coordenada x con 130+(-angulo/2) y en la coordenada Y con (height-60-angulo)
        x= 130+(-angulo/2)+v_ini*cos(radians(angulo))*t;
        y= (height-60-angulo)-(v_ini*sin(radians(angulo))*t-gr*pow(t, 2)/2);
        
        // Se realiza una pequeña animación de la bala mientras está en el aire, para conseguir un 
        // efecto oscilante aprovechamos la función coseno que retorna valores -1 y 1 que 
        // multiplicados por un factor nos permite aumentar y disminuir la bala cíclicamente por 
        // tanto tendremos un tamaño de la imagen entre 100% i 70% aprox.
        float zoom = 0.8-(cos(t*3)/6);
        pushMatrix(); // Guardamos el estado actual
        imageMode(CENTER); // Cambiamos el modo de posición de la imagen al centro
        translate(x, y); // Cambiamos el punto origen a la posición de la bala
        // Ajustamos la rotación de la bala según la velocidad y el momento del vuelo
        rotate(radians(-angulo+t*(100/v_ini)*7));
        scale(zoom); // Escalamos la imagen
        // Pintamos la bala en la posició 0,0 ya que hemos trasladado el origen de coordenadas
        image(bala, 0, 0); 
        imageMode(CORNER); // Retornamos el modo de imagen a la esquina superior izquierda
        popMatrix(); // Recuperamos el estado del lienzo
        // Incrementamos la posición en el tiempo (t)
        t+=0.25;
        // Si llegamos a tierra hemos de esperar a que lancen una nueva bala, volvemos a REPOSO
        if (y > height) {
          estado = REPOSO;
          x = 0;
          intentos--; // Restamos el número de tiradas
          if (intentos <= 0) {
            // Si ya no quedan intentos se ha terminado la partida
            contador = TIEMPO_ESPERA;
            estado = ERROR;
            // Reposicionamos el tanque
            tanque.pos((int)(width-178-random(300)), tanque.getY());
            tanque.reset();
          }
        }
      }
    }
     
  

Detección de colisiones

Una gran mayoría de los juegos, deben manejar que efecto se produce cuando dos objetos se encuentran en un mismo punto, en algunos casos, se debe producir un efecto rebote, en otros casos una destrucción de uno de los objetos o ambos. La máxima dificultad es la detección de las colisiones cuando los objetos son irregulares.

Básicamente hay dos formas de realizar el cálculo:

  • Por área, los objetos ocupan un área rectangular o circular, cuando las áreas se superponen es que hay colisión. Una mejora de este sistema es dividir el objeto en un conjunto de áreas y analizarlas individualmente. Por ejemplo una persona puede tener un área circular para la cabeza, una rectangular para el tronco, etc. Es decir se divide la forma compleja en un conjunto de áreas básicas.
  • Píxel a píxel, se realiza un detección por área rectangular, si hay una posible colisión, se analiza a partir de la máscara que indica que píxeles son visibles, si dos píxeles superpuestos son visibles indica que hay colisión. Este sistema es muy preciso pero tiene un alto coste computacional.


En nuestro caso usaremos el método de áreas en su forma más simple, detectando la colisión con un rectángulo.

Está función está implementada en la clase Sprite con las funciones dentro y colisionan.

Para la función dentro se ha utilizado un rectángulo para realizar la comparación, en este caso se recibe un punto y comprueba que esté dentro de los límites del rectángulo.

     
      /**
       Función que mira si un punto está dentro del sprite
      */
      boolean dentro(float xx, float yy) {
        // Calculamos el centro del sprite
        int mx = (int)x+wi/2*flip; // Hay que tener en cuenta si está volteado
        int my = (int)y+he/2;
        
        // Si el punto está dentro del rectángulo devolverá true
        return (xx > mx-wi/2 && xx < mx+wi/2 && yy > my-he/2 && yy < my+he/2);
      }
     
  


En cambio en el caso de colisionan, la comprobación se realiza a partir del centro de los dos sprites, si las áreas se superponen es que hay colisión.

     
      /**
       Función que mira si ha colisionado con otro sprite
      */
      boolean colisionan(Sprite b) {
        // Calculamos el centre del sprite a comparar
        int bx = (int)b.x+b.wi*b.flip/2;
        
        // Calculamos el centro del sprite
        int mx = (int)x+wi/2*flip;
        int my = (int)y+he/2;
        
        // Si el centro està dentro del recuadro del sprite comparado es que han colisionado
        return (b.visible && mx >= bx-b.wi/2 && mx <= bx+b.wi/2 && my >= b.y && my <= b.y+b.he); 
      }
     
  


Para realizar una comprobación circular, se debería calcular la distancia entre los centros de los dos objetos con la fórmula de cálculo de Álgebra Euclidiana:

Posteriormente basta con comparar ese valor con la suma de los radios de los dos sprites, si esta es mayor no hay colisión, en caso contrario los objetos colisionan.

Como hemos podido ver en esta segunda parte del artículo, para crear un videojuego, hay que crear un argumento (reglas del juego) y los diferentes personajes y elementos que deben aparecer en la trama. Si los personajes son animados, deben crearse los gráficos para cada posición diferente de la animación. Debe existir un módulo que realiza las comprobaciones oportunas de que se cumplen las reglas del juego y de si la partida ha finalizado. Agregar efectos sonoros mejorará las sensaciones del usuario, obviamente se debería de añadir un nivel mayor de dificultad para destruir el tanque, contador de tiempo restante, obstáculos para complicar el tiro, etc.

Descarga en este link el código fuente completo del artículo. Fuentes.zip

Documentación