17 años en Internet

15 noviembre 2022

[Dev-Blog] [Proyecto Isekai] [#9] Filosofeando sobre autómatas


    Cuando pensamos en un juego lo primero que suele venirnos a la cabeza son las acciones que puede hacer nuestro personaje: Correr, saltar, disparar o hablar suelen ser los ejemplos básicos. También nos damos cuenta de que estas acciones suelen ser auto-exclusivas o incluso pueden combinarse: Si hablamos no podemos correr, pero si corremos podemos saltar y/o disparar. Como podéis apreciar, podemos listar todas estas acciones y combinaciones como si de un mapa de estados se tratara.

    La mayoría de motores traen su propio sistema de gestión de estados, permitiendo vincular las animaciones de una forma bastante simple. Un ejemplo magistral de esto es Unity con su "Animation Controller". Ahora bien, los programadores solemos pensar en estos estados de una forma condicional, lo cual suele producir bastantes bugs que a simple vista no pensamos. Por poner unos ejemplos reales: En Skyrim un dragón puede atacar al NPC con el que estás hablando y en Genshim Impact los enemigos te pueden atacar mientras tu personaje se pasea en una cinemática.

    Lo que voy a proponer no es la panacéa, pero me gusta seguirlo como enfoque: No sé si habéis estudiado los fundamentos de la inteligencia artificial, pero una de las cosas que se te dicen al empezar a hacer prácticas es que evites poner condicionales en un autómata. Y un autómata no es ni más ni menos que un conjunto de nodos con un alfabeto por los que vamos naveando de un estado al otro. El enfoque preferible suele ser el de ponderar las distintas reglas disponibles y que en función del nodo donde se encuentre nuetro autómata tome una deción A o B basándose en esas poderaciones. No sé si entendéis a qué me refiero, pero cuando definimos el motor de un juego, podemos entender los diferentes estados de nuestros objetos como el de un autómata.

    Por ejemplo, si pensamos en un side scroller 2D donde disparamos flechas, nuestro personaje debería de contar con los siguientes estados:

  • A: Reposo (estado inicial)
  • B: Correr -> B1 arrancar, B2 correr, B3 frenar
  • C: Saltar -> C1 despegue, C2 aterrizaje
  • D: Disparar -> D1 sacar una flecha del carcaj, D2 Disparar, D3 guardar el arco
  • E: Hablar
  • F: Acceder al menú de ítems
  • G: Agacharse -> G1 agacharse, G2 mantenerse agachado, G3 levantarse
  • H: Muerte
  • I: Recibir daño

    La idea no es especificaros todos los estados y sus detalles, pero más bien que os quedéis con la idea de cómo funciona el concepto. Al comenzar un nivel, nuestro personaje se encuentra quieto (o con una animación de estar quieto), la cual suele conocerse como estado de Reposo. Desde A (reposo), nuestro personaje puede arrancar a correr (B1), empezar a saltar (C1), sacar una flecha para disparar (D1), intentar agacharse (G1), hablar con un NPC (E), abrir el inventario (F) o recibir daño (I).

    Además, no podemos pasar del estado A al B3 (frenar, al soltar el botón de correr) sin haber pasado antes por los estados B1 (cuando pulsamos el botón de correr) y B2 (mientras mantenemos el botón de correr). De la misma forma, tampoco podemos pasar de A a D3 (guardar el arco) sin haber pasado antes por D1 (sacar una flecha) y D2 (disparar).

    La principal diferencia del ejemplo de A a B3 y de A a D2 radica en la pulsación del botón correspondiente: Ambas arrancan en A, pero al comenzar a correr pasamos a B1, seguimos siempre en B2 mientras mantenemos el botón pulsado y al soltar el botón pasamos a B3 y luego volvemos a A; Por el contrario, al pulsar el botón de disparo pasamos automáticamente al estado D1, después al D2 y ahí, si el botón sigue pulsado, volvemos a sacar una flecha en D1 y a disparla en D2... y seguimos así hasta que soltamos el botón, guardamos el arco en D3 y volvemos a reposo en A.

    Ahora bien, ¿qué pasa si recibimos daño mientras disparamos? Pues todo dependerá del nodo donde nos encontramos: Con D1 y con D3 no hay problema, puesto que la flecha no ha salido aún del carcaj (como en D1) o se encuentra ya viajando por el escenario (como es el caso de D3): En estos dos casos símplemente pasamos al estado I (recibir daño) y del estado I volveremos a reposo (A) o muerte (H), en función de la vida que nos quede. El estado D2 es más especial, porque la lógica nos indica que la flecha tiene que salir a la par de que recibimos daños... así creamos un nuevo estado: D2I, el cual pasará a I y después a reposo (A) o muerte (H). Gracias al nuevo estado, D2I, podemos incluso permitirnos la licencia de que la flecha salga disparada con cierto grado de desvío.

    Y ahora os hago otra reflexión: ¿Es lo mismo saltar estando parado que saltar mientras corremos? Pues la lógica nos dice que no: Las animaciones del salto, la altura del salto y la distancia recorrida serán distintas. Es decir, las animaciones y acciones que produce son distintas, por lo que tienen que diferenciarse. En ese sentido pasamos a tener dos estados diferenciados: "Salto parado" (CP) y Salto en movimiento" (CM), cada uno con su respectivo subestado de arranque (CP1 y CM1) y aterrizaje (CP2 y CM2).

    Además, toca reflexionar otra cosa. En un juego en 2D, ¿consideramos el mismo estado correr hacia la derecha que correr hacia la izquierda? ¿Y saltar corriendo hacia la derecha es el mismo estado que saltar mientras se corre hacia la izquierda? Bueno, muchos programadores lo que hacen es definir una variable de dirección y trabajan con un sólo estado, pero en la práctica esto se considera duplicar los estados también, puesto que los gráficos y las direcciones de movimiento son idénticos pero espejados y el compartamiento lo alteran de forma condicional.

    Lo consideres un nuevo estado o no, lo importante es tener claro cómo tenemos que reaccionar en un cambio de sentido. Me explico: No podemos correr primero hacía la derecha y luego hacia la izquierda sin pasar antes por los estados de frenada (B3), vuelta a reposo (A) y vuelta a la aceleración (B1), puesto que si permitimos el libre albedrío (continuar siempre en B2 con indiferencia del sentido) podemos acabar con animaciones rotas o nada naturales. De misma forma, no podemos permitir que en medio de un salto el usuario cambie de sentido en mitad del vuelo. Lo que sí que podemos hacer es que en mitad de un salto realicemos un efecto de frenada en el aire si el usuario fuerza la dirección inversa, de forma que el jugador tenga cierta forma de controlar dónde aterrizar. Pero una cosa es frenar ligéramente (es decir, menos distancia recorrida) y otra realizar un cambio de sentido (que no se debería de poder hacer). Esto significa que podemos crear un nuevo estado, llámenlo CM2B (aterrizaje del salto en movimiento con frenada) y se activaría cuando nos encontremos en CM2 y el jugador esté presionando la dirección inversa.

    Y así, entre pitos y flautas, estaréis apreciendo que lo que es un simple juego de tipo "side scroller" acaba teniendo un autómata gigantésco únicamente para el protagonista. Si ademas queremos meterle otros tipos de efecto, como que el jugador sufre envenenamiento (con gráficos alterados y movimientos más lentos y torpes) o que se está quemando (animaciones más rápidas pero vamos sufriendo), pues el número de estados se van multiplicando. Lo bueno es que este esquema de autómata también puede reutilizarse para los enemigos, aunque en la práctica estos tienen patrones de comportamiento diferentes.

    La clave de esta reutilización está sobretodo en la IA que implementamos. Como he dicho antes, el paso de un estado a otro del autómata pasa por las reglas ponderadas que se recibe como entrada. En el caso del jugador, la pulsación de las teclas influye en la ponderación: Si estamos en reposo y pulsamos salto, disparo y correr a la vez, ¿qué acción se inicia primero? Pues ahí depende que valoremos la acción que causa menos frustración para el usuario. En los juegos estilo Contra habrá que priorizar la acción de disparar, mientras que en los juegos de plataformeo habrá darle más valor al salto.

     Pero volviendo al tema de los enemigos, la complicación para la reutilización de los autómatas ahora viene con sus IA. Si se supone que se comportan como el jugador, las normas de entrada de un estado a otro tendrán distintas ponderaciones y éstas a su vez pueden variar en función de un tipo de perfil. Sí, perfiles. Podemos hacer juegos de reglas ponderadas de formas distintas en función del perfil del enemigo.

- Podemos definir un tipo de enemigo francotirador, que se limita a estar en reposo, agacharse cada 10 segundos, levantarse, lanzar dos disparos y volverse a agachar.

- Podemos definir un tipo de enemigro "tonto", que lo único que haga es avanzar 5 pasos y realizar un disparo. Además, podemos hacer que analice si delante se acaba la plataforma para realizar un salto en movimiento hacia delante y caerse al vacío.

- Podemos definir un NPC, que se encuentre siempre en pose de reposo mirando siempre hacia el protagonista. Y de paso podemos hacer que reciba daño del fuego cruzado y que muera. Y cumpliendo con las normas que definiremos en nuestro personaje, no debería de poder morir mientras hable.

Y fijaros que con un mismo autómata, habríamos definido al protagonista, un NPC y dos tipos de enemigos, aunque las reglas de interación entre estados cambien.

No hay comentarios:

Publicar un comentario

Si te ha gustado la entrada o consideras que algún dato es erróneo o símplemente deseas dar algún consejo, no dudes en dejar un comentario. Todo feedback es bienvenido siempre que sea respetuoso. También puedes contactarme vía Twitter @Hamster_ruso si lo consideras necesario.