Un método para detectar objetos alineados.

En este tutorial se expone un método para detectar si cierto número de objetos iguales están juntos y alineados en el juego. La siguiente imagen ilustra la idea mejor:

Captura de pantalla

La idea básica es la siguiente: En la mayoría de este tipo de juegos, los objetos caen y se van apilando en el fondo de la ventana del juego, es decir, los objetos más abajo llevan más tiempo en el juego que los que van apareciendo por arriba. ¿Todo bien? Pues por eso decidí que el mecanismo comience por detectar el objeto más cercano a la esquina inferior izquierda.

Esquina inferior izquierda

Como se puede ver, los objetos alineados son unos círculos, que a partir de ahora llamaré obj_bola. La bola más cercana a la esquina indicada será el primer objeto que va a probar si hay otra bola junto a ella. Ocupándonos de la bola más a la izquierda, se puede ver que hay cinco probables posiciones en las cuales pudiera haber una bola contigua. Resulta obvio que para cada una de estás cinco posiciones corresponde un ángulo de dirección (respecto a la bola primera), como lo muestra la siguiente imagen:

Direcciones

De esta manera, la bola más a la izquierda, o primera, o actual (como queiras llamarle) tiene que probar si en cada una de esas posiciones hay o un objeto bloque obj_bloque u otra bola. Si estamos en la primera bola, y se detecta un objeto bloque, esa dirección no es buena, pues el objeto bloque impide formar una posible línea con otra bola, por lo que esa dirección se descarta. En la imagen, esto pasaría con la dirección 0°, y entonces se requiere aumentar la dirección en 45° para hacer la siguiente comprobación.

La siguiente comprobación sería algo como: "¿En la dirección a 45 grados hay una bola contigua?". En este caso, vemos que la dirección es "buena", es decir, hay una colisión con una bola adyacente, por lo que ahora tenemos detectadas dos bolas juntas. Esto en la mayoría de los juegos no es suficiente, siendo lo normal que se requiera detectar 3 o 4 bolas en línea para tener un patrón válido (En este ejemplo, se necesitan cuatro bolas en línea). Bien, hemos detectado que hay una bola a 45 grados de la primer bola, como el juego se trata de buscar bolas alineadas, sería incorrecto cambiar la dirección a 90° en este punto, pues si ya encontramos una dirección "buena" a 45°, debemos seguir en la misma línea/dirección, pero debemos mover nuestra referencia, es decir, la siguiente comprobación ya no la haremos en la primera bola (la acabamos de hacer), sino que debemos "movernos" al origen de la segunda bola y a partir de ese punto hacer la comprobación en la misma dirección de 45°

Una vez en la segunda bola, y con la dirección todavía a 45°, se observa que otra vez detectaremos una colisión, ahora con la tercer bola, por lo que el proceso anterior se repetiría: se han detectado tres bolas en línea y la referencia se moverá de nuevo a la tercer bola, la cual detectará una colisión con la cuarta y última bola, que es la finalidad del juego. Probablemente ya te hayas preguntado cómo se detectaría en GM:S la colisión contra la bola adyacente. En mi caso he decidido usar la función collision_line() la cual devuelve el id de la bola contra la que se colisiona.

Toda la funcionalidad descrita se realiza en un objeto controlador, que es el que se va moviendo al origende cada bola y va probando colisiones en las distintas direcciones. El ejemplo funciona correctamente y lo puedes comprobar descargándolo y ejecutándolo, pero quedan pendientes algunos detalles, como por ejemplo, borrar la ds_list y usar una bandera para indicarle al controlador si verifica colisiones o no (por ejemplo, no tiene caso verificar paso a paso colisiones si en el juego hay menos de 4 bolas).

A continuación voy a describir únicamente la parte medular del proyecto, es decir, los eventos [CREATE] y [STEP] del objeto controlador. Para [CREATE] tenemos:

col = 0
primera = 0
scan = 1
lista_i = ds_list_create();

x = 10
y = 10

angulo[0] = 0
angulo[1] = 45
angulo[2] = 90
angulo[3] = 135
angulo[4] = 180

dir = 0
radio = 65
eslabon = 0
bueno = 0

xx = 0
yy = 0
choice = 0          // Al estar pausado el juego, 0 = volver; 1 = ir a menu de opciones

col almacena el redultado de la colisión. primera almacena el id de la bola actual. Un nombre más adecuado habría sido "actual", por ejemplo, pero al ver que el ejemplo funcionaba, me ha dado flojera cambiarlo xD. scan es una variable que puede ser muy útil aunque no la he implementado aun. Sirve para indicar cuando se deben buscar objetos alineados y cuándo no. Su uso haría el código más eficiente ya que sólo bajo ciertas condiciones se buscarían bolas en línea. lista_i es una lista para guardar las cuatro bolas en línea (si es que el patrón se cumple). El arreglo angulo almacena las cinco posibles direcciones en las cuales la bola actual(primera) puede encontrar otra adyacente. dir guarda la dirección actual (una de las cinco de angulo[ ]). radio indica la longitud de la linea de colisión para la función coliision_line, este valor se debe ajustar dependiendo del tamaño de tus objetos y la distancia entre ellos. eslabon es una variable que va contando cuántas colisiones distintas ocurren, y es la variable que se prueba para saber si hay cuantro bolas en línea. La variable bueno se pone en 1 cuando se han encontrado 4 bolas en linea. xx e yy indican el punto final de la línea de colisión (consulta la función collision_line en el manual) y dependen del valor de radio y la direccion actual

En el evento [STEP] está el siguiente código:

//Detectar la bola más abajo y más a la izquierda
primera = instance_nearest(0, room_height, obj_bola)

x = primera.x
y = primera.y

for (var i = 0; i < 5; i += 1 )
{
    dir = angulo[i]
    while ( dir == angulo[i] ) {
        if (ds_list_find_index(lista_i, primera) < 0)
            ds_list_add(lista_i, primera)
        instance_deactivate_object(primera)
        xx = lengthdir_x(radio, angulo[i])
        yy = lengthdir_y(radio, angulo[i])
        col = collision_line(x, y, x + xx, y + yy, obj_bola, true, false)
        if (col > 1)
        {             ds_list_add(lista_i, col)
            eslabon += 1
            primera = col
            x = primera.x
            y = primera.y
        }
        else {
            eslabon = 0
            dir = -1      //salir del while
        }

        if (eslabon == 3)
        {
            bueno = 1;
            break;      //salir del while
        }
    } ////////////////////////// FIN WHILE ////////////////////////

    if (eslabon >= 3)
        break;        //Salir del ciclo for si se encontró una línea
    }

Lo primero que hace el código es detectar cuál es la bola más a la izquierda y más cercana al fondo de la ventana, esa será la instancia primera. Luego, el objeto controlador se mueve a esa posición. Comienza un ciclo for que se repetirá cinco veces, una vez por cada dirección de probable colisión (0°, 45°, 90°, 135° y 180°). Dentro del ciclo FOR, lo primero que se hace es ajustar la dirección al primer valor del arreglo angulo (0 en la primer iteración) y se comienza el ciclo, WHILE, cuya condición para que se ejecute es que dir sea igual al angulo[i]. Dentro del segundo ciclo, se guarda el id de la primer bola en la lista, y ésta misma instancia se desactiva. Una de las razones para desactivarla es porque la función collision_line() detecta colisiones con bolas, y si el objeto control está ubicado en el origen de la bola actual, la función detectaría colisión con esta bola actual, pero esto no es lo que se pretende, sino detectar colisión con OTRA bola adyacente. A su vez, al deshabilitar la bola actual, la instancia desaparece, lo que ayuda a identificar de manera visual qué bolas están involucradas en la línea sin tener que destruírlas.

Bien después de la explicación anterior, continuamos: se calcula el punto final de la línea de colisión, basándonos en la magnitud de radio y la dirección actual. Usamos el resultado para trazar una línea de colision como lo indica la función collision_line() y su resultado se guarda en la variable col. En seguida probamos si ha habido una colisión en la dirección indicada, y de ser así, el id de la bola adyacente se guarda en la lista; actualizamos la variable primera para que apunte a ésta bola, que pasará a ser la bola actual, y movemos el obj_controlador a esa nueva posición. En caso de no haber colisión en la dirección actual, limpiamos la variable eslabon y abandonamos el ciclo while pues hemos usado una "dirección mala".

Dentro del mismo ciclo WHILE, cuando se detecta una colisión en la dirección actual, se prueba la variable eslabon para ver si ha alcanzado el valor de 3, lo cual indicaría que se han encontrado 4 pelotas en línea. Este IF se usa una instrucción break y se usa tanto en el ciclo WHILE como en el FOR para abandonar las iteraciones una vez que se han encontrado 4 objetos en línea..

Aquí finalizamos el tutorial. Insisto en que necesita más trabajo para poder usarlo en un juego real, pero me siento satisfecho pues creo que el objetivo principal del tutorial se cumple: explicar la idea básica tras la detección de objetos en línea. Además, después de echarle un vistazo, seguro que se te ocurren otras maneras de implementar la detección.

Descarga el proyecto: 4 en línea

Penumbra.