La estructura de datos de la lista de acciones buena para UI, AI, animaciones y más

los lista de Acción es una estructura de datos simple útil para muchas tareas diferentes dentro de un motor de juego. Se podría argumentar que la lista de acciones siempre debe usarse en lugar de alguna forma de máquina de estados.

La forma más común (y la forma más simple) de organización del comportamiento es una máquina de estados finitos. Por lo general implementado con interruptores o matrices en C o C ++, o con un montón de Si y más Declaraciones en otros idiomas, las máquinas de estado son rígidas e inflexibles. La lista de acciones es un esquema de organización más fuerte en el sentido de que modela de manera clara cómo las cosas suelen suceder en la realidad. Por esta razón, la lista de acciones es más intuitiva y flexible que una máquina de estados finitos..


vista rápida

La lista de acciones es solo un esquema de organización para el concepto de acción cronometrada. Las acciones se almacenan en un orden de primero en entrar, primero en salir (FIFO). Esto significa que cuando se inserta una acción en una lista de acciones, la última acción insertada en el frente será la primera en eliminarse. La lista de acciones no sigue explícitamente el formato FIFO, pero en su núcleo siguen siendo los mismos.

Cada bucle de juego, la lista de acciones se actualiza y Cada acción en la lista se actualiza en orden.. Una vez finalizada la acción, se elimina de la lista..

Un acción es algún tipo de función a la que llamar que hace algún tipo de trabajo de alguna manera. Aquí hay algunos tipos diferentes de áreas y el trabajo que las acciones podrían realizar dentro de ellas:

  • Interfaz de usuario: muestra secuencias cortas como "logros", reproduce secuencias de animaciones, hojea ventanas, muestra contenido dinámico: mover; girar; dar la vuelta; descolorarse; interpolación general.
  • Inteligencia artificial: Comportamiento en cola: mover; Espere; patrulla; huir; ataque.
  • Nivel de lógica o comportamiento: Plataformas móviles; movimientos de obstáculos; niveles cambiantes.
  • Animación / Audio: Reproducir; detener.

Las cosas de bajo nivel como la búsqueda de ruta o el flocado no se representan de manera efectiva con una lista de acciones. Las áreas de juego de combate y otras áreas de juego altamente especializadas también son cosas que probablemente no deberías implementar a través de una lista de acciones..


Clase de lista de acciones

Aquí hay un vistazo rápido a lo que debería estar dentro de la estructura de datos de la lista de acciones. Tenga en cuenta que más detalles específicos seguirán más adelante en el artículo.

 class ActionList public: void Update (float dt); void PushFront (Acción * acción); void PushBack (Acción * acción); void InsertBefore (Acción * acción); void InsertAfter (Acción * acción); Acción * Eliminar (Acción * acción); Acción * Comenzar (vacío); Acción * Fin (vacío); bool IsEmpty (void) const; float TimeLeft (void) const; bool IsBlocking (void) const; privado: duración del flotador; tiempo de flotación transcurrido; float percentDone; bloqueo bool; carriles sin firmar; Acción ** acciones; // puede ser un vector o lista enlazada;

Es importante tener en cuenta que el almacenamiento real de cada acción no tiene que ser una lista enlazada real, algo así como la C++ std :: vector Funcionaría perfectamente bien. Mi preferencia es agrupar todas las acciones dentro de un asignador y enlazar listas junto con listas vinculadas de manera intrusiva. Por lo general, las listas de acciones se utilizan en áreas menos sensibles al rendimiento, por lo que es probable que una gran optimización orientada a los datos sea innecesaria al desarrollar una estructura de datos de la lista de acciones..


La acción

El quid de todo este shebang son las acciones en sí mismas. Cada acción debe ser completamente autónoma, de modo que la lista de acciones en sí misma no sepa nada sobre los aspectos internos de la acción. Esto hace que la lista de acciones sea una herramienta extremadamente flexible. A una lista de acciones no le importará si está ejecutando acciones de la interfaz de usuario o gestionando los movimientos de un personaje modelado en 3D.

Una buena manera de implementar acciones es a través de una única interfaz abstracta. Algunas funciones específicas están expuestas desde el objeto de acción a la lista de acciones. Aquí hay un ejemplo de cómo podría verse una acción base:

 class Action public: virtual Update (float dt); OnStart virtual (nulo); OnEnd virtual (vacío); bool está terminado; bool isBlocking; carriles sin firmar; flotador transcurrido duración del flotador privado: ActionList * ownerList; ;

los OnStart () y OnEnd () Las funciones son integrales aquí. Estas dos funciones se ejecutarán siempre que una acción se inserte en una lista y cuando la acción finalice, respectivamente. Estas funciones permiten que las acciones sean totalmente autónomas..

Acciones de bloqueo y no bloqueo

Una extensión importante de la lista de acciones es la capacidad de indicar acciones como bloqueando y sin bloqueo. La distinción es simple: una acción de bloqueo finaliza la rutina de actualización de la lista de acciones y no se actualizan más acciones; una acción no bloqueante permite actualizar la acción subsiguiente.

Se puede usar un solo valor booleano para determinar si una acción está bloqueando o no bloqueando. Aquí hay un psuedocode demostrando una lista de acciones actualizar rutina:

 void ActionList :: Update (float dt) int i = 0; while (i! = numActions) Acción * acción = acciones + i; acción-> Actualizar (dt); if (action-> isBlocking) break; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (action);  ++ i; 

Un buen ejemplo del uso de acciones no bloqueantes sería permitir que algunos comportamientos se ejecuten todos al mismo tiempo. Por ejemplo, si tenemos una cola de acciones para correr y agitar las manos, el personaje que realiza estas acciones debería poder hacer ambas cosas a la vez. Si un enemigo está huyendo del personaje, sería muy ridículo si tuviera que correr, luego deténgase y agite sus manos frenéticamente, luego siga corriendo..

Resulta que el concepto de acciones de bloqueo y no bloqueo combina de manera intuitiva la mayoría de los tipos de comportamientos simples que se requieren para implementar dentro de un juego..


Ejemplo de caso

Veamos un ejemplo de cómo se vería ejecutar una lista de acciones en un escenario del mundo real. Esto ayudará a desarrollar la intuición sobre cómo usar una lista de acciones, y por qué las listas de acciones son útiles..

Problema

Un enemigo dentro de un simple juego 2D de arriba hacia abajo necesita patrullar de un lado a otro. Siempre que este enemigo esté dentro del alcance del jugador, debe lanzar una bomba hacia el jugador y hacer una pausa en su patrulla. Debería haber un pequeño tiempo de reutilización después de lanzar una bomba donde el enemigo se queda completamente quieto. Si el jugador todavía está dentro del alcance, se debe lanzar otra bomba seguida de un tiempo de reutilización. Si el jugador está fuera de rango, la patrulla debe continuar exactamente donde lo dejó..

Cada bomba debe flotar en el mundo 2D y cumplir con las leyes de la física basada en fichas implementada dentro del juego. La bomba solo espera hasta que su fusible se termine, y luego explota. La explosión debe consistir en una animación, un sonido y la eliminación de la caja de colisión de la bomba y el sprite visual..

La construcción de una máquina de estados para este comportamiento será posible y no demasiado difícil, pero llevará algún tiempo. Las transiciones de cada estado deben codificarse a mano, y guardar los estados anteriores para continuar más adelante puede causar un dolor de cabeza.

Solución de lista de acciones

Por suerte este es un problema ideal para resolver con listas de acción. Primero, imaginemos una lista de acción vacía. Esta lista de acción vacía representará una lista de elementos "por hacer" para que el enemigo los complete; una lista vacía indica un enemigo inactivo.

Es importante pensar cómo "compartimentar" el comportamiento deseado en pequeñas pepitas. Lo primero que se debe hacer es bajar los comportamientos de la patrulla. Asumamos que el enemigo debe patrullar a la izquierda por una distancia, luego patrullar a la derecha por la misma distancia, y repetir.

Esto es lo que patrulla izquierda la acción podría verse como:

 clase PatrolLeft: acción pública Actualización virtual (float dt) // Mover el enemigo a la izquierda enemigo-> posición.MoverLeft (); // Temporizador hasta que finalice la acción + = dt; si (transcurrido> = duración) es Finalizado = verdadero;  virtual OnStart (void); // no hacer nada OnEnd (void) virtual // Insertar una nueva acción en la lista list-> Insert (new PatrolRight ());  bool isFinished = false; bool isBlocking = true; Enemigo * enemigo; duración del flotador = 10; // segundos hasta que termine el flotador transcurrido = 0; // segundos ;

PatrolRight Se verá casi idéntico, con las instrucciones invertidas. Cuando una de estas acciones se coloca en la lista de acciones del enemigo, el enemigo de hecho patrullará la izquierda y la derecha infinitamente.

Aquí hay un breve diagrama que muestra el flujo de una lista de acciones, con cuatro instantáneas del estado de la lista de acciones actual para patrullar:

La siguiente adición debe ser la detección de cuando el jugador está cerca. Esto se podría hacer con una acción de no bloqueo que nunca se completa. Esta acción verificará si el jugador está cerca del enemigo y, de ser así, creará una nueva acción llamada ThrowBomb directamente delante de sí mismo en la lista de acciones. También colocará un Retrasar acción justo después de la ThrowBomb acción.

La acción de no bloqueo se ubicará allí y se actualizará, pero la lista de acciones continuará actualizando todas las acciones posteriores más allá de ella. Acciones de bloqueo (como Patrulla) se actualizará y la lista de acciones dejará de actualizar cualquier acción posterior. Recuerde, esta acción está aquí solo para ver si el jugador está dentro del rango y nunca abandonará la lista de acciones.!

Aquí es cómo podría verse esta acción:

 class DetectPlayer: public Action Virtual Update (float dt) // Lanza una bomba y pausa si el jugador está cerca si (PlayerNearby ()) this-> InsertInFrontOfMe (new ThrowBomb ()); // Pausa durante 2 segundos, esto-> InsertInFrontOfMe (nueva pausa (2.0));  virtual OnStart (void); // no hacer nada virtual OnEnd (void) // no hacer nada bool isFinished = false; bool isBlocking = false; ;

los ThrowBomb La acción será una acción de bloqueo que lanza una bomba hacia el jugador. Probablemente debería ser seguido por una ThrowBombAnimation, que está bloqueando y reproduce una animación enemiga, pero he dejado esto por concisión. La pausa detrás de la bomba tendrá lugar en la animación, y espere un poco antes de terminar..

Veamos un diagrama de cómo podría verse esta lista de acciones durante la actualización:


Los círculos azules están bloqueando acciones. Los círculos blancos son acciones no bloqueantes..

La bomba en sí misma debe ser un objeto de juego completamente nuevo, y tener tres o más acciones en su propia lista de acciones. La primera acción es un bloqueo. Pausa acción. Siguiendo esto debería haber una acción para reproducir una animación por una explosión. La bomba sprite, junto con la caja de colisión, tendrá que ser eliminado. Por último, se debe reproducir un efecto de sonido de explosión..

En total, debe haber alrededor de seis a diez tipos diferentes de acciones que se usan juntas para construir el comportamiento necesario. La mejor parte de estas acciones es que pueden ser reutilizado en el comportamiento de cualquier tipo de enemigo, no solo el que se muestra aquí.


Más sobre Acciones

Carriles de acción

Cada lista de acciones en su forma actual tiene una sola carril En el que pueden existir acciones. Un carril es una secuencia de acciones a actualizar. Un carril puede ser bloqueado o no bloqueado.

La perfecta implementación de carriles hace uso de máscaras de bits. (Para obtener más información sobre qué es una máscara de bits, consulte la Guía rápida para los programadores y la página de Wikipedia para una introducción rápida). Utilizando un único entero de 32 bits, se pueden construir 32 carriles diferentes..

Una acción debe tener un número entero para representar todos los diversos carriles en los que reside. Esto permite que 32 carriles diferentes representen diferentes categorías de acciones. Cada carril puede ser bloqueado o no bloqueado durante la rutina de actualización de la propia lista.

Aquí hay un ejemplo rápido de Actualizar Método de una lista de acciones con carriles de máscara de bits:

 void ActionList :: Update (float dt) int i = 0; carriles sin firmar = 0; while (i! = numActions) Acción * acción = acciones + i; if (lanes & action-> lanes) continuar; acción-> Actualizar (dt); if (action-> isBlocking) lanes | = action-> lanes; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (action);  ++ i; 

Esto proporciona un mayor nivel de flexibilidad, ya que ahora una lista de acciones puede ejecutar 32 tipos diferentes de acciones, donde de antemano se necesitarían 32 listas de acciones diferentes para lograr lo mismo.

Acción de retraso

Es muy útil tener una acción que no haga más que demorar todas las acciones durante un período de tiempo específico. La idea es retrasar todas las acciones subsiguientes hasta que haya transcurrido un tiempo..

La implementación de la acción de retardo es muy simple:

 Demora de clase: Acción pública pública: nula Actualización (float dt) transcurrido + = dt; si (transcurrido> duración) es Finalizado = verdadero; ;

Sincronizar accion

Un tipo de acción útil es uno que bloquea hasta que es la primera acción en la lista. Esto es útil cuando se están ejecutando algunas acciones distintas de no bloqueo, pero no está seguro de en qué orden terminarán. sincronizar La acción garantiza que no se estén ejecutando acciones previas de bloqueo antes de continuar.

La implementación de la acción de sincronización es tan simple como uno podría imaginar:

 class Sync: public Action public: void Update (float dt) if (ownerList-> Begin () == this) isFinished = true; ;

Características avanzadas

La lista de acciones descrita hasta ahora es una herramienta bastante poderosa. Sin embargo, hay un par de adiciones que se pueden hacer para que la lista de acciones brille. Estos son un poco avanzados y no recomiendo implementarlos a menos que pueda hacerlo sin demasiados problemas..

Mensajería

La capacidad de enviar un mensaje directamente a una acción, o permitir que una acción envíe mensajes a otras acciones y objetos del juego, es extremadamente útil. Esto permite que las acciones sean extraordinariamente flexibles. A menudo, una lista de acciones de esta calidad puede actuar como un "lenguaje de guiones de un hombre pobre".

Algunos mensajes muy útiles para publicar desde una acción pueden incluir lo siguiente: iniciado; terminó pausado reanudado terminado; cancelado; obstruido. El bloqueado es bastante interesante: cada vez que se coloca una nueva acción en una lista, puede bloquear otras acciones. Estas otras acciones querrán saberlo y, posiblemente, informar a otros suscriptores sobre el evento también..

Los detalles de implementación de los mensajes son específicos del idioma y no triviales. Como tal, los detalles de la implementación no se tratarán aquí, ya que la mensajería no es el tema central de este artículo..

Acciones Jerárquicas

Hay algunas formas diferentes de representar jerarquías de acciones. Una forma es permitir que una lista de acciones sea una acción dentro de otra lista de acciones. Esto permite la construcción de listas de acciones para agrupar grandes grupos de acciones bajo un único identificador. Esto aumenta la facilidad de uso y hace que la lista de acciones más complejas sea más fácil de desarrollar y depurar.

Otro método es tener acciones cuyo único propósito es generar otras acciones justo antes de sí mismas dentro de la lista de acciones propietarias. Yo mismo prefiero este método a lo mencionado anteriormente, aunque puede ser un poco más difícil de implementar.


Conclusión

El concepto de una lista de acciones y su implementación se han discutido en detalle para ofrecer una alternativa a las máquinas de estado ad hoc rígidas. La lista de acciones proporciona un medio simple y flexible para desarrollar rápidamente una amplia gama de comportamientos dinámicos. La lista de acciones es una estructura de datos ideal para la programación de juegos en general..