Registrarse

[Pokeemerald] Pathfinding para reemplazar applymovements

Juanjo

Hacker del pasado... Compilador del presente
Miembro insignia
¡Hola!

Vengo a presentar un comando en el que estoy trabajando llamado MoveObjectToPos()

¡La idea es no liarse con tanto applymovement y simplemente definir la coordenada xy del destino para el mini que queramos y listo. Sin usar applymovement el mini se movera a donde queramos evitando así obstaculos.

¿Y por qué hacer todo esto?


Bien, supongamos que tenemos esto: Un mapa lleno de objetos y minisprites y queremos coreografiar una escena con applymovement. Es un dolor de cabeza estar contando pasos, haciendo que el mini en cuestión se mueva los pasos necesarios, no se sobreponga con otros objetos y que respete las reglas del terreno. Y bueno, ahora imaginemos que queremos que el prota pueda activar el script de desde varias posiciones… ¿Cómo es que hacíamos todo de forma tradicional? Pues a punta de condicionales, contando pasos y creando secuencias personalizadas para cada posibilidad… O también tenemos al loquillo de turno que ponía un pasillo de un solo espacio para no poner gatillos….

¿Y si hacemos algo más robusto?

Ahora bien, digamos que ya tienes tu coreografía lista y todo como debe ser…. Pero se te ocurre cambiar el mapeado… Bien, pues se te van a ir los applymovements al carajo.

¿Y si te dijera que se puede tunear el applymovement de tal manera que solo tengas que poner las coordenadas finales del objeto en cuestión?

Técnicamente podríamos calcular las coordenadas del mini, restarlas por las coordenadas de destino que queramos y moverse «x» casillas a la derecha o a la izquierda dependiendo del signo y «y» verticales hasta llegar al destino.

Sería un algoritmo fácil de ejecutar y se comportaría bien en mapas abiertos y donde no importen mucho los obstáculos.



En el caso del chico de Petalia quedaría bastante bien: en vez de tantas líneas de código y condicionales dependiendo de cada gatillo, se podría hacer que cada gatillo ejecutara el mismo script de leer las coordenadas del prota y mover al chico a esas coordenadas (bueno restando una casilla, que hay que respetar el espacio personal ¿Qué no les enseñan urbanidad en la casa? ¡Caramba!)

Un comando así podría ser tan sencillo como:

C:
MoveObjectToPos(IdObject, X, Y, Facing)
Donde el IdObject representa el ID del mini a mover, X y Y la coordenada final y el facing un comando opcional para que luego del movimiento el mini quede apuntando hacia alguna dirección.


Vale, pero ¿Cómo hacemos cuando hay obstáculos en el camino?

Para casos más generales esta solución no es suficiente y es necesario tener en cuenta el mapa en sí mismo, al igual que los obstáculos, otros minis y las elevaciones del terreno.


Y para esto entra a ayudarnos nuestra querida amiga la Inteligencia Artificial.

Algoritmos de búsqueda basados en grafos:

Los algoritmos de búsqueda son un campo de la inteligencia artificial tradicional y son indispensables para resolver problemas en áreas tan diversas como la robótica, las telecomunicaciones, la clasificación de datos, los problemas lógico-matemáticos y por supuesto, los videojuegos.

Esto es básicamente un grafo de búsqueda



A partir de un nodo inicial vamos explorando los nodos hijo (o children) hasta que encontremos la solución que queremos.

Adaptar este grafo a nuestro problema es tan sencillo como definir los hijos de cada nodo como los posibles pasos que podría dar el mini (adelante, atrás, arriba, abajo) y nos encargaríamos de evaluar cuales de estos nodos hijos no son posibles de alcanzar (obstáculo, fuera del mapa, mini obstruyéndolo o diferencia de altura). Con la información del mapa y el estado de los overworld es más que suficiente para esta evaluación. Por lo que podemos concentrarnos en que además el camino que encuentre sea el óptimo siempre, es decir, el que lo lleve al destino con el menor número de pasos posibles.

¿Qué algoritmos de búsqueda podrían servirnos

Existen muchos tipos de algoritmos de búsqueda aplicada a árboles y grafos de búsqueda, los más simples son simplemente explorar cada una de las ramificaciones hasta llegar al fondo y seguir con la siguiente, de forma que por fuerza bruta nos topemos con la solución (Deep First Search). Sin embargo, además que gastamos un inmenso tiempo de computación no garantizamos que cuando demos con el goal el camino será el óptimo.

También existe la opción de explorar todos hijos primero e ir bajando en profundidad de todas las ramas al mismo tiempo. Este algoritmo se llama Breadth First Search (BFS), además, si usamos este método y a cada transición le damos un costo (como número de pasos) y a medida que vamos bajando en el árbol hacemos que el costo de los nodos sea más alto, podríamos entonces quedarnos con el goal que tenga el costo menor… Es más, podríamos diseñar una frontera donde almacenemos todos los nodos hijos que vamos descubriendo y los vamos eligiendo del menor al mayor costo total. De esta manera, la frontera va separando las casillas exploradas de las que aún no hemos explorado.

A este algoritmo se le conoce como algoritmo de Dijkstra o de los caminos mínimos, y es una genialidad, porque siempre nos asegura que la meta que encontremos será óptima, es decir que el camino que seguirá el mini al final será el del menor número de pasos…

Aun así, que el algoritmo sea óptimo, no significa que sea el mejor. Puesto que tenemos que barrer con casi todos los nodos y realizar muchas operaciones para encontrar el nodo final y el camino óptimo.



En las imágenes, podemos ver los nodos explorados en azul y la frontera en verde. Se puede notar que casi todo el mapa se ha explorado y que un ligero cambio de un tile puede hacer que todo el mapa sea explorado. Aún así, el algoritmo siempre escoge la ruta más corta entre el inicio y el destino. Izquierda: Número de pasos: 18, Número de operaciones: 120. Derecha: Número de pasos: 26, Número de operaciones: 130.

¿Qué importa si es óptimo? ¡Yo solo quiero que encuentre un camino y rápido!

Los mapas de los juegos de Pokemon guardan un detalle muy bueno ¡Los conocemos de antemano!, a menos que juegues Mundo Misterioso, sabes con antelación como será un mapa porque está compilado desde antes, eso nos puede traer una pequeña ventaja. ¿Y si en vez de calcular el número de pasos dados para cada nodo más bien calculamos la distancia que le falta para llegar a la meta?

A este tipo de búsqueda se le llama búsqueda informada, y si hacemos eso, estaríamos usando una heurística para calcular el costo, esta vez nos interesa reducir esa distancia (que la calculamos como en ejemplo de Petalia: distancia en horizontal más distancia en vertical) sin importar los obstáculos que haya (de eso ya se encarga el grafo al escoger los hijos posibles). Este es un algoritmo Best First. Como podrás darte cuenta, el uso de la heurística nos ha brindado información de la meta y como llegar a ella rápidamente, sin embargo, al no contar los pasos para el costo no estamos seguros si la solución a la que llegamos es óptima o no, solamente que hemos llegado rápido y sin hacer tantos cálculos.



Como vemos en las imágenes, los nodos explorados son muchísimo menos, es decir, se encuentra un resultado mucho más rápido y sin tanto coste computacional. Sin embargo, como vemos en la imagen de la derecha, el camino que ha encontrado no es el óptimo. Izquierda: Número de pasos: 18, Número de operaciones: 65. Derecha: Número de pasos: 28, Número de operaciones: 101.




¡ESPERA, soy perfeccionista! ¿No puedes darme un algoritmo que sea óptimo y que no haga tantos cálculos innecesarios?

Para los que lo quieren todo tenemos el maravilloso A* (se pronuncia a-star), que toma lo bueno del Dijkstra al llevar la cuenta de los costos de los nodos, pero también teniendo en cuenta la heurística del Best First.

Para hacer esto, a la hora de decidir a que nodo darle prioridad realiza la siguiente operación:


f(n ) = g(n ) + h(n )

Donde n es el nodo, f(n ) es el valor sobre el que se discriminará el nodo n con respecto a los otros nodos. g(n ) es el costo en pasos acumulado del nodo y h(n ) es la heurística de dicho nodo con respecto al destino. Si da la casualidad de que dicha heurística es una subestimación del costo final (es decir que siempre será menor o igual al costo final) garantizaremos que el algoritmo será siempre óptimo.

De esta manera podemos jugar con el algoritmo para hacerlo más rápido para encontrar una solución o hacerlo más dado encontrar la solución óptima.

En mi código de la implementación dejo un peso que le pueden agregar a la heurística:

https://github.com/J2M2/pokeemerald/blob/8747f910aa52085469ded4dfcf0fe7e0c014e0d3/include/movement_path_planning.h#L30


C:
#define HEUR_WEIGHT 1
Si es 0 tendrán Dijkstra, si es 1 será A* y si eligen un valor muy alto tendrán un Best First.

¿Qué resultado obtenemos con A*?



Izquierda: Número de pasos: 18, Número de operaciones: 78. Derecha: Número de pasos: 26, Número de operaciones: 113.

Si bien el número de operaciones es ligeramente mayor que el obtenido con el Best First, usando A* garantizamos que el camino siempre será óptimo y la heurística nos intentará reducir el número de operaciones hasta donde sea posible.



Podemos ver ambos ejemplos in-game en los gifs usando el aloritmo A* para encontrar el camino más corto.

¿Vale y como uso esto en mi proyecto?

Puedes echarle un vistazo a mi fork en https://github.com/J2M2/pokeemerald/tree/path_planner. ¡Eso sí, aún no está listo y por favor no lo vayas a incorporar a un proyecto serio! Sigo haciendo pruebas con el algoritmo y no lo he vuelto aún un comando. De momento puedo llamarlo con un callnative y añade los parámetros manualmente en las líneas https://github.com/J2M2/pokeemerald/blob/8747f910aa52085469ded4dfcf0fe7e0c014e0d3/src/movement_path_planning.c#L34

¿Cuándo estará lista una versión que pueda meter en mi proyecto?

Pronto, digamos que, en una o dos semanas, por supuesto, haré un anuncio en el foro y un tutorial de cómo utilizarlo.

¿Qué le hace falta?



  • Que funcione como un comando normal de cualquier script y no como un callnative (para eso me va a ayudar el bueno de @Samu que tiene experiencia con esto).
  • Como han visto, la cantidad de nodos explorados y operaciones aumenta exponencialmente con el número de tiles. Aún no lo he probado en mapas muy grandes y creo que podría hacer overflow de memoria.
  • Quisiera evitar lo anterior haciendo algunas listas dinámicas (que no les tenga asignar un máximo número de nodos desde antes) y que evite hacer movimientos en las zonas fuera de la pantalla de carga. Esto reduciría el mapa sobre el que explora el camino muchísimo más y evitaría los problemas de overflow.
  • Adicionalmente, quiero añadir que pueda atravesar zonas de diferente altura, calculando su propia altura durante el movimiento (y no desde el principio como lo tengo ahora) y así pueda subir a puentes y saltar barrancos o usar tiles por los que solo se pueden pasar en un sentido.
Además de applymovements fáciles, ¿Para qué más podría usarlo?

Este comando abrirá un abanico de posibilidades para los proyectos. Desde personificación de movimientos y dinámicas, minijuegos, dinamismo, ahorro de tiempo con scripts y hasta el famoso follow me.

Me alegra mucho volver a WAH a aportar mi granito de arena a este mundo que jamás ha podido salir de mi corazón. No se extrañen de verme más seguido en el foro, de que empiece a subir más aportes y tutoriales…. ¡Y de una sorpresa apocalíptica de un proyecto que tengo ya en desarrollo en decomp!

Saludos,


Juanjo
 
Última edición:

Xiros

¡Pokémon Omega con actualización del 30/8!
Miembro de honor
Avances como este me ponen muy orgulloso de nuestra comunidad, y me hacen pensar que con decomp podemos lograr fantásticas cosas.

Con gente como tú o Samu podemos lograr que hayan excelentes proyectos en decomp con mayor facilidad de ejecución.

Estoy atento a la publicación de avances!
 

Edo

You've met with a terrible fate, haven't you?
Miembro de honor
Sin ningún tipo de duda, este es un grandísimo aporte. Ya desde su concepción, la idea es increíble, revolucionadora y por cierto que muy útil. Su pragmatismo logrará evitar incontables horas de aburrimiento y cálculos de movimientos, ahora innecesarios —y ni hablar de que le sienta genial a aquellos creadores cuya estrecha visión del esfuerzo obligaba a limitar el espacio del protagonista a un bloque para forzar una sola posibilidad de movimiento.
Es un grandísimo orgullo que este tipo de cosas salgan de acá, de nuestro viejo y querido foro. Y ojalá no quede solo en esto, sino que sea apenas el comienzo de algo mucho más grande y fructífero. Y aunque no puedo prometer nada, mi deseo es aportar tanto como pueda —incluso desde mi humilde posición, tan vaga y desairada. Trabajaré en ello.

Saludos.
 

pikachu240

Junior C# Developer
Creo que si se hiciera el cálculo a fuera antes de compilarse con un programa externo, porque esa ruta no es dinámica, sino fija, ergo puede tenerse applymovement puestos antes de ejecutar el compile :)
 

Juanjo

Hacker del pasado... Compilador del presente
Miembro insignia
¿Qué le hace falta?

  • Que funcione como un comando normal de cualquier script y no como un callnative.
Arreglado, ver commits adca7736965b771a9033b5816652b909dac40f3b y f4d50c31ae6ae82073f0cdc3e21fd32adf8b4607

  • Como han visto, la cantidad de nodos explorados y operaciones aumenta exponencialmente con el número de tiles. Aún no lo he probado en mapas muy grandes y creo que podría hacer overflow de memoria.
Se ha incrementado un poco el margen aunque aún no es dinámico, por lo que esa parta podrá estar a disposición de ser actualizado.

  • Adicionalmente, quiero añadir que pueda atravesar zonas de diferente altura, calculando su propia altura durante el movimiento (y no desde el principio como lo tengo ahora) y así pueda subir a puentes y saltar barrancos o usar tiles por los que solo se pueden pasar en un sentido.
Arreglado, ver commit 5bc904533adfb09b9d91d3f83c0c23cba3d3b8eb


Es código es ya 100% funcional y puede ser usado en cualquier proyecto usando el comando:

Código:
moveobjecttopos OBJ_EVENT_ID, POS_X, POS_Y, FACE_DIR
waitmovement 0
Donde facedir puede ser 0 (abajo), 1 (arriba), 2 (izquierda), 3 (derecha).

Ejemplo:

Código:
moveobjecttopos 1, 5, 4, 0
waitmovement 0

ESCENARIO DE PRUEBA:
He creado un escenario de puentes y alturas para probarlo:

1612917556829.png


Se va a mover el minisprite de una zona de menor altura (3) a una de mayor (4). Puede irse por debajo y rodear el mapa o cruzar el puente (más corto). Se puede ver el resultado usando el prota (OBJ_EVENT_ID_PLAYER ) o un NPC arbitrario.

thingy.gif



Si quieren puedo hacer un tutorial sobre como usarlo, aunque no tiene mucha ciencia. Solo denle pull a mi fork.

Esperen pronto un post explicando como funciona el código. Les prometo que sera muy interesante y podran aprender un poco de inteligencia artificial y de algoritmos de búsqueda. También mi experiencia personal de como usé este algoritmo para encontrar la forma de ensamblar una estrutucta de tiles hexagonales en el espacio usando un brazo robótico.

Saludos,
Juanjo
 
Última edición:

Xiros

¡Pokémon Omega con actualización del 30/8!
Miembro de honor
Me alegro mucho de ver esos resultados!!

No puedo esperar a estarlo usando :)

Resolviste el tema de que el prota podía pasar por encima a otros?
 

Juanjo

Hacker del pasado... Compilador del presente
Miembro insignia
Me alegro mucho de ver esos resultados!!

No puedo esperar a estarlo usando :)

Resolviste el tema de que el prota podía pasar por encima a otros?
Sí y no. En parte el problema del prota era de alturas. Ahora ya no pasa por encima de ningún objeto. Sin embargo, si ponemos al prota en la otra esquina del mapa, como no se han dibujado los objetos lejanos (puesto que la cámara está lejos). Al moverse, pues no tendrá en cuenta esos minis y podría pasar por encima.
 

Xiros

¡Pokémon Omega con actualización del 30/8!
Miembro de honor
Sí y no. En parte el problema del prota era de alturas. Ahora ya no pasa por encima de ningún objeto. Sin embargo, si ponemos al prota en la otra esquina del mapa, como no se han dibujado los objetos lejanos (puesto que la cámara está lejos). Al moverse, pues no tendrá en cuenta esos minis y podría pasar por encima.
En ese caso se podría usar el clásico applymovement y seguir disfrutando de esta maravilla en el 99% de los casos restantes!

Estoy atento a más novedades!
 

Juanjo

Hacker del pasado... Compilador del presente
Miembro insignia
En ese caso se podría usar el clásico applymovement y seguir disfrutando de esta maravilla en el 99% de los casos restantes!

Estoy atento a más novedades!
Correcto, o dividir el movimiento en dos comandos

LLEGÓ LA HORA DE HABLAR DEL CÓDIGO
La intuición detrás de la solución de planificación de camino es el algoritmo de búsqueda basado en Graph Search para encontrar el camino entre un nodo inicial y uno final.

El mejor equilibrio entre una solución óptima (que garantice siempre la ruta más corta) y una solución «codiciosa» (que priorice encontrar la mejor solución disponible y no pierda tiempo en encontrar una mejor) es el algoritmo A*.

C-like:
function searchPath(object, x, y, facing)# returns a solution or failure

    node ← a node with State derived from object and path-cost=0
    frontier ← a priority queue by Path-Cost with node as the only element
    explored ← an empty set

    loop do
    if Empty(frontier) then return failure
        node ← Pop(frontier) /* chooses the lowest-cost node in frontier*/

    if problem.Goal-Test(node.State) then return Solution(node)
        add node.State to explored

    for each action in possible_movements(DOWN, UP, LEFT, RIGHT) do
        if not isMoveNotPossible(node, action)
            child ← Child-Node( node, action)

            if child.State is neither in explored nor frontier then
                f(child) ← cost function for the child (VER MAS ABAJO LA DEFINICIÓN DE f([I]node[/I]))
                frontier ← Insert(child, frontier, f(child))

            else if child.State is in frontier with higher Path-Cost then
                        replace that frontier node with child
El costo es el alma de la optimización porque es usado como prioridad para elegir un nuevo nodo u otro en la cola de prioridad priority queue .

El algoritmo A* evalúa los nodos combinando el coste de la ruta g(node) y el coste estimado a la meta h(node) (heurística):
f (node) = g(node) + h(node),
donde h(node) tiene que ser admisible. Una heurística admisible es una subestimación, es decir, tiene que ser menor que el coste real.

Para el vainilla Dijkstra (UCS) este valor de la heurística h(node) es 0 y garantiza una solución óptima, pero podría hacer muchos más cálculos y una mayor evaluación de nodos que lo estrictamente encesario.

El Greedy Best First Seach, no utiliza un coste acumulado del camino del nodo g(node) y sólo utiliza la heurística para seguir la solución.

La heurística suele ser la distancia lineal entre la meta y el nodo actual, este valor va disminuyendo a medida que nos acercamos más y más al nodo meta.
En Pokemon Esmeralda el movimiento diagonal no es realizado por los NPCs por lo que la distancia euclidiana no es la mejor opción.

Sin embargo, la distancia Manhattan (L1) podría ser una mejor alternativa:


Ver http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html para más información sobre la heurística.

La constante HEUR_WEIGHT puede ser modificada para cambiar el peso que tiene la heurística, permitiéndole usar UCS cuando su valor es 0, y Greedy Best-First cuando el valor es mucho mayor que 1. Para el A* normalito simplemente usar 1.

Los nodos están definidos en https://github.com/J2M2/pokeemerald/blob/path_planner/include/movement_path_planning.h#L37 y contienen:

state: int para representar la posición del nodo en la matriz (plana) que se utiliza para hacer comparaciones.
coords: coordenadas x,y.
costo: int, costo hasta ahora del nodo: g(node) = número de pasos dados hasta el nodo actual
currentElevation
: int, elevacion actual (coordenada z)
path: array de movimientos, el camino seguido hasta el nodo.

En https://github.com/J2M2/pokeemerald/blob/path_planner/include/movement_path_planning.h también hay algunas funciones auxiliares para utilizar la cola de prioridad, el set y la heurística.
 
Última edición:

Kaktus

Miembro insignia
Miembro insignia
Excelente explicación en referencia a lo matemático. Grandísimo aporte. Ahora sólo falta lidiar con mapas cuyo contenido sea dinámico p:
 
Arriba