Planificación de acción orientada a objetivos para una inteligencia artificial más inteligente

La planificación de acción orientada a objetivos (GOAP) es un sistema de AI que le dará opciones a sus agentes y las herramientas para tomar decisiones inteligentes sin tener que mantener una máquina de estados finitos grande y compleja..

Ver la Demo

En esta demostración, hay cuatro clases de caracteres, cada una de las cuales usa herramientas que se rompen después de ser usadas por un tiempo:

  • Minero: Minas mineral en las rocas. Necesita una herramienta para trabajar..
  • Logger: corta árboles para producir troncos. Necesita una herramienta para trabajar..
  • Cortador de madera: corta árboles en madera utilizable. Necesita una herramienta para trabajar..
  • Herrero: Forja herramientas en la forja. Todos usan estas herramientas.

Cada clase descubrirá automáticamente, utilizando la planificación de acciones orientada a objetivos, qué acciones deben realizar para alcanzar sus objetivos. Si su herramienta se rompe, irán a una pila de suministros que tenga una hecha por el herrero.

Que es goap?

La planificación de acción orientada a objetivos es un sistema de inteligencia artificial para agentes que les permite planificar una secuencia de acciones para satisfacer un objetivo en particular. La secuencia particular de acciones depende no solo del objetivo sino también del estado actual del mundo y del agente. Esto significa que si se proporciona el mismo objetivo para diferentes agentes o estados del mundo, puede obtener una secuencia de acciones completamente diferente, lo que hace que la IA sea más dinámica y realista. Veamos un ejemplo, como se ve en la demostración anterior..

Tenemos un agente, un triturador de madera, que toma troncos y los corta en leña. El helicóptero se puede suministrar con el objetivo. MakeFirewood, y tiene las acciones ChopLog, GetAxe, y Coleccionar ramas.

los ChopLog La acción convertirá un tronco en leña, pero solo si el cortador de madera tiene un hacha. los GetAxe La acción le dará un hacha al cortador de madera. Finalmente, el Coleccionar ramas La acción también producirá leña, sin requerir un hacha, pero la leña no será tan alta en calidad.

Cuando le damos al agente la MakeFirewood Objetivo, obtenemos estas dos secuencias de acción diferentes:

  • Necesita leña -> GetAxe -> ChopLog = hace leña
  • Necesita leña -> Coleccionar ramas = hace leña

Si el agente puede obtener un hacha, entonces puede cortar un tronco para hacer leña. Pero tal vez no puedan conseguir un hacha; Entonces, solo pueden ir y recoger ramas. Cada una de estas secuencias cumplirá el objetivo de MakeFirewood

GOAP puede elegir la mejor secuencia en función de las condiciones previas disponibles. Si no hay un hacha a mano, entonces el cortador de madera tiene que recurrir a recoger ramas. Recoger sucursales puede llevar mucho tiempo y producir leña de baja calidad, por lo que no queremos que funcione todo el tiempo, solo cuando tiene que hacerlo..

¿Para quién es GOAP?

Ya estás familiarizado con Finite State Machines (FSM), pero si no es así, echa un vistazo a este excelente tutorial.. 

Es posible que se haya topado con estados muy grandes y complejos para algunos de sus agentes de FSM, donde finalmente llega a un punto en el que no desea agregar nuevos comportamientos porque causan demasiados efectos secundarios y brechas en la IA..

GOAP convierte esto:

Estados de la máquina de estados finitos: conectados en todas partes.

Dentro de esto:

GOAP: agradable y manejable.


Al desacoplar las acciones entre sí, ahora podemos centrarnos en cada acción individualmente. Esto hace que el código sea modular, y fácil de probar y mantener. Si desea agregar otra acción, puede simplemente introducirla y no tiene que cambiar ninguna otra acción. Intenta hacer eso con un FSM!

Además, puede agregar o eliminar acciones sobre la marcha para cambiar el comportamiento de un agente para que sean aún más dinámicos. ¿Tiene un ogro que de repente comenzó a rabiar? Dales una nueva acción de "ataque de rabia" que se elimina cuando se calman. Simplemente agregar la acción a la lista de acciones es todo lo que tiene que hacer; El planificador de GOAP se encargará del resto..

Si descubre que tiene un FSM muy complejo para sus agentes, entonces debe darle una oportunidad a GOAP. Una señal de que su FSM se está volviendo demasiado complejo es cuando cada estado tiene una gran cantidad de declaraciones if-else que evalúan el estado al que deben ir a continuación, y agregar un nuevo estado le hace gemir ante todas las implicaciones que podría tener..

Si tiene un agente muy simple que solo realiza una o dos tareas, entonces GOAP puede ser un poco torpe y un FSM será suficiente. Sin embargo, vale la pena ver los conceptos aquí y ver si serían lo suficientemente fáciles para que usted se conecte a su agente..

Comportamiento

Un acción Es algo que hace el agente. Por lo general, solo se trata de reproducir una animación y un sonido, y cambiar un poco de estado (por ejemplo, agregar leña). Abrir una puerta es una acción diferente (y una animación) que levantar un lápiz. Una acción está encapsulada, y no debería tener que preocuparse por lo que son las otras acciones.

Para ayudar a GOAP a determinar qué acciones queremos utilizar, cada acción recibe una costo. Una acción de alto costo no se elegirá sobre una acción de menor costo. Cuando secuenciamos las acciones, sumamos los costos y luego seleccionamos la secuencia con el costo más bajo.

Permite asignar algunos costos a las acciones:

  • GetAxe Costo: 2
  • ChopLog Costo: 4
  • Coleccionar ramas Costo: 8

Si observamos nuevamente la secuencia de acciones y sumamos los costos totales, veremos cuál es la secuencia más barata:

  • Necesita leña -> GetAxe (2) -> ChopLog(4) = hace leña(total: 6)
  • Necesita leña -> Coleccionar ramas(8) = hace leña(total: 8)

Obtener un hacha y cortar un tronco produce leña a un costo menor de 6, mientras que recoger las ramas produce madera a un costo mayor de 8. Entonces, nuestro agente elige obtener un hacha y cortar madera.

¿Pero no se ejecutará esta misma secuencia todo el tiempo? No si introducimos condiciones previas...

Condiciones previas y efectos

Las acciones tienen condiciones previas y efectos. Una condición previa es el estado que se requiere para que la acción se ejecute, y los efectos son el cambio al estado después de que la acción se haya ejecutado.

Por ejemplo, el ChopLog La acción requiere que el agente tenga un hacha a mano. Si el agente no tiene un hacha, necesita encontrar otra acción que pueda cumplir esa condición previa para permitir que ChopLog acción ejecutar Por suerte, el GetAxe La acción hace eso, este es el efecto de la acción..

El planificador de GOAP

El planificador de GOAP es un fragmento de código que analiza las condiciones previas y los efectos de las acciones y crea colas de acciones que cumplirán un objetivo. El agente proporciona ese objetivo, junto con un estado mundial, y una lista de acciones que el agente puede realizar. Con esta información, el planificador de GOAP puede ordenar las acciones, ver cuáles pueden ejecutarse y cuáles no, y luego decidir qué acciones son las mejores para realizar. Por suerte para ti, he escrito este código, por lo que no tienes que.

Para configurar esto, vamos a agregar condiciones previas y efectos a las acciones de nuestro cortador de madera:

  • GetAxe Costo: 2. Condiciones previas: "un hacha está disponible", "no tiene un hacha". Efecto: "tiene un hacha".
  • ChopLog Costo: 4. Condiciones previas:"tiene un hacha". Efecto: "hacer leña"
  • Coleccionar ramas Costo: 8. Condiciones previas: (ninguna). Efecto: "hacer leña".

El planificador de GOAP ahora tiene la información necesaria para ordenar la secuencia de acciones para hacer leña (nuestro objetivo). 

Comenzamos suministrando al Planificador de GOAP el estado actual del mundo y el estado del agente. Este estado mundial combinado es:

  • "no tiene hacha"
  • "un hacha está disponible"
  • "el sol está brillando"

Mirando nuestras acciones disponibles actuales, la única parte de los estados que son relevantes para ellas es el "no tiene un hacha" y los estados "un hacha está disponible"; El otro podría ser usado para otros agentes con otras acciones..

De acuerdo, tenemos nuestro estado mundial actual, nuestras acciones (con sus condiciones previas y sus efectos) y el objetivo. Vamos a planear!

OBJETIVO: "hacer leña" Estado actual: "no tiene un hacha", "hay un hacha disponible" ¿Se puede ejecutar ChopLog de acción? NO - requiere condición previa "tiene un hacha" No se puede usar ahora, intente con otra acción. ¿Puede la acción GetAxe correr? SÍ, las condiciones previas "un hacha está disponible" y "no tiene un hacha" son ciertas. Empuje la acción a la cola, actualice el estado con efecto de la acción Nuevo estado "tiene un hacha" Eliminar el estado "un hacha está disponible" porque acabamos de tomar uno. ¿Puede la acción ChopLog correr? SÍ, la condición previa "tiene un hacha" es verdadera Acción PUSH en la cola, estado de actualización con efecto de acción Estado nuevo "tiene un hacha", "hace leña" Hemos alcanzado nuestro OBJETIVO de "hace leña" Secuencia de acción: GetAxe -> ChopLog

El planificador también ejecutará otras acciones y no se detendrá cuando encuentre una solución para el objetivo. ¿Qué pasa si otra secuencia tiene un costo menor? Se ejecutará a través de todas las posibilidades para encontrar la mejor solución..

Cuando se planea, se acumula un árbol. Cada vez que se aplica una acción, se elimina de la lista de acciones disponibles, por lo que no tenemos una cadena de 50 GetAxe Acciones consecutivas. El estado se cambia con el efecto de esa acción..

El árbol que el planificador construye se ve así:

Podemos ver que en realidad encontrará tres caminos hacia la meta con sus costos totales:

  • GetAxe -> ChopLog (total: 6)
  • GetAxe -> Coleccionar ramas(total: 10)
  • Coleccionar ramas (total: 8)

A pesar de que GetAxe -> Coleccionar ramas Funciona, el camino más barato es. GetAxe -> ChopLog, entonces este es devuelto.

¿Cómo se ven realmente las condiciones previas y los efectos en el código? Bueno, eso depende de usted, pero me ha resultado más fácil almacenarlos como un par clave-valor, donde la clave es siempre una Cadena y el valor es un objeto o tipo primitivo (flotante, int, booleano o similar). En C #, podría verse así:

HashSet< KeyValuePair > condiciones previas; HashSet< KeyValuePair > efectos;

Cuando la acción se está llevando a cabo, ¿cómo se ven estos efectos y qué hacen? Bueno, no tienen que hacer nada, en realidad solo se utilizan para la planificación, y no afectan el estado del agente real hasta que se ejecutan de verdad.. 

Vale la pena enfatizar: planificar acciones no es lo mismo que ejecutarlas. Cuando un agente realiza la GetAxe acción, probablemente estará cerca de un montón de herramientas, reproducirá una animación de doblar y recoger, y luego almacenará un objeto hacha en su mochila. Esto cambia el estado del agente. Pero, durante GOAP planificación, el cambio de estado es solo temporal, de modo que el planificador pueda encontrar la solución óptima.

Condiciones previas de procedimiento

A veces, las acciones deben hacer un poco más para determinar si pueden ejecutarse. Por ejemplo, el GetAxe la acción tiene la condición previa de que "un hacha está disponible" que deberá buscar en el mundo, o en las inmediaciones, para ver si hay un hacha que el agente pueda tomar. Podría determinar que el hacha más cercana está demasiado lejos o detrás de las líneas enemigas, y dirá que no puede correr. Esta condición previa es de procedimiento y necesita ejecutar algún código; No es un simple operador booleano que podamos simplemente cambiar.

Obviamente, algunas de estas condiciones previas de procedimiento pueden tardar un tiempo en ejecutarse, y se deben realizar en algo que no sea el subproceso de render, idealmente como un subproceso de fondo o como Coroutines (en Unidad).

Usted podría tener efectos de procedimiento también, si así lo desea. Y si desea introducir resultados aún más dinámicos, puede cambiar el costo de acciones sobre la marcha!

GOAP y estado

Nuestro sistema GOAP tendrá que vivir en una pequeña máquina de estados finitos (FSM), por la única razón de que, en muchos juegos, las acciones deberán estar cerca de un objetivo para poder realizarlas. Terminamos con tres estados:

  • Ocioso
  • Mover a
  • Realizar una acción

Cuando está inactivo, el agente determinará qué objetivo desea cumplir. Esta parte se maneja fuera de GOAP; GOAP solo le dirá qué acciones puede ejecutar para lograr ese objetivo. Cuando se elige una meta, se pasa al Planificador de GOAP, junto con el estado de inicio del mundo y del agente, y la agenda devolverá una lista de acciones (si puede cumplir esa meta).

Cuando el planificador esté listo y el agente tenga su lista de acciones, intentará realizar la primera acción. Todas las acciones deberán saber si deben estar dentro del alcance de un objetivo. Si lo hacen, entonces el FSM pasará al siguiente estado: Mover a.

los Mover a El estado le dirá al agente que necesita moverse hacia un objetivo específico. El agente hará el movimiento (y reproducirá la animación de caminata), y luego avisará al FSM cuando esté dentro del alcance del objetivo. Este estado se desprende y la acción se puede realizar..

los Realizar una acción el estado ejecutará la siguiente acción en la cola de acciones devueltas por el Planificador de GOAP. La acción puede ser instantánea o puede durar muchos fotogramas, pero cuando se realiza, se desprende y luego se realiza la siguiente acción (nuevamente, después de verificar si la siguiente acción debe realizarse dentro del alcance de un objeto).

Todo esto se repite hasta que no quedan acciones por realizar, momento en el que volvemos a la Ocioso Estado, consigue un nuevo objetivo, y planifica de nuevo..

Un ejemplo de código real

¡Es hora de echar un vistazo a un ejemplo real! No te preocupes no es tan complicado, y le proporcioné una copia de trabajo en Unity y C # para que la pruebe. Solo hablaré de esto brevemente aquí para que pueda tener una idea de la arquitectura. El código usa algunos de los mismos ejemplos de WoodChopper que arriba.

Si desea profundizar, diríjase aquí para obtener el código: http://github.com/sploreg/goap

Tenemos cuatro obreros:

  • Herrero: convierte el mineral de hierro en herramientas..
  • Registrador: utiliza una herramienta para cortar árboles para producir registros..
  • Minero: extrae rocas con una herramienta para producir mineral de hierro..
  • Cortador de madera: utiliza una herramienta para cortar leños para producir leña.

Las herramientas se desgastan con el tiempo y deberán reemplazarse. Afortunadamente, el herrero hace herramientas. Pero el mineral de hierro es necesario para hacer herramientas; ahí es donde entra el Minero (que también necesita herramientas). El cortador de madera necesita registros, y esos vienen del registrador; ambos necesitan herramientas también.

Las herramientas y los recursos se almacenan en pilas de suministros. Los agentes recolectarán los materiales o herramientas que necesitan de las pilas y también entregarán su producto en ellos..

El código tiene seis clases principales de GOAP:

  • GoapAgent: entiende el estado y usa el FSM y GoapPlanner para operar.
  • GoapAction: acciones que los agentes pueden realizar.
  • GoapPlanner: planifica las acciones para el GoapAgent.
  • FSM: la máquina de estados finitos.
  • FSMState: un estado en el FSM.
  • IGoap: la interfaz que utilizan nuestros verdaderos actores laboristas. Se vincula con eventos para GOAP y el FSM..

Veamos el GoapAction clase, ya que esta es la subclase que

clase abstracta pública GoapAction: MonoBehaviour private HashSet> condiciones previas; HashSet privado> efectos; bool privado en rango = falso; / * El costo de realizar la acción. * Averiguar un peso que se adapte a la acción. * Cambiarlo afectará a qué acciones se eligen durante la planificación. * / Public float cost = 1f; / ** * Una acción a menudo tiene que realizar en un objeto. Este es ese objeto. Puede ser nulo * / objetivo público de GameObject; GoapAction () preconditions = new HashSet público> (); efectos = nuevo HashSet> ();  public void doReset () inRange = false; target = null; Reiniciar ();  / ** * Restablecer cualquier variable que deba restablecerse antes de que la planificación vuelva a suceder. * / public abstract void reset (); / ** * ¿Se realiza la acción? * / public abstract bool isDone (); / ** * Comprobar procedimentalmente si esta acción puede ejecutarse. No todas las acciones * necesitarán esto, pero algunas pueden. * / public abstract bool checkProceduralPrecondition (agente GameObject); / ** * Ejecutar la acción. * Devuelve True si la acción se realizó correctamente o false * si sucedió algo y ya no se puede realizar. En este caso *, la cola de acción debería despejarse y no se puede alcanzar el objetivo. * / public abstract bool perform (agente GameObject); / ** * ¿Esta acción debe estar dentro del alcance de un objeto de juego objetivo? * Si no es así, el estado moveTo no tendrá que ejecutarse para esta acción. * / public abstract bool requireInRange (); / ** * ¿Estamos dentro del alcance del objetivo? * El estado MoveTo lo configurará y se restablecerá cada vez que se realice esta acción. * / public bool isInRange () return inRange;  public void setInRange (bool inRange) this.inRange = inRange;  public void addPrecondition (clave de cadena, valor de objeto) preconditions.Add (new KeyValuePair(valor clave) );  public void removePrecondition (clave de cadena) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp en condiciones previas) if (kvp.Key.Equals (clave)) remove = kvp;  if (! default (KeyValuePair) .Equals (remove)) preconditions.Remove (remove);  public void addEffect (clave de cadena, valor de objeto) effects.Add (new KeyValuePair(valor clave) );  public void removeEffect (clave de cadena) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp en efectos) if (kvp.Key.Equals (key)) remove = kvp;  if (! default (KeyValuePair) .Equals (remove)) effects.Remove (remove);  HashSet público> Condiciones previas obtener volver condiciones previas;  HashSet público> Efectos obtener efectos de retorno; 

Nada demasiado sofisticado aquí: almacena condiciones y efectos. También sabe si debe estar dentro del alcance de un objetivo y, si es así, el FSM sabe que debe empujar el Mover a Estado cuando sea necesario. Sabe cuándo se hace, también; Eso es determinado por la clase de acción implementada..

Aquí está una de las acciones:

clase pública MineOreAction: GoapAction private bool mined = false; IronRockComponent privado targetRock; // de donde obtenemos el mineral de private float startTime = 0; duración de minería flotante pública = 2; // segundos public MineOreAction () addPrecondition ("hasTool", true); // necesitamos una herramienta para hacer esto addPrecondition ("hasOre", false); // si tenemos mineral no queremos más addEffect ("hasOre", true);  Public Override void reset () mined = false; targetRock = nulo; tiempo de inicio = 0;  anulación pública bool isDone () return mined;  anulación pública bool requireInRange () return true; // sí, necesitamos estar cerca de una roca anulación pública bool checkProceduralPrecondition (agente GameObject) // encuentra la roca más cercana a la que podamos extraer IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) como IronRockComponent []; IronRockComponent más cercano = nulo; float closestDist = 0; foreach (IronRockComponent rock in rocks) si (más cercano == nulo) // primero, así que elíjalo ahora más cercano = rock; closestDist = (rock.gameObject.transform.position - agent.transform.position) .magnitude;  else // ¿está éste más cerca que el anterior? float dist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; si < closestDist)  // we found a closer one, use it closest = rock; closestDist = dist;    targetRock = closest; target = targetRock.gameObject; return closest != null;  public override bool perform (GameObject agent)  if (startTime == 0) startTime = Time.time; if (Time.time - startTime > miningDuration) // terminado en minería BackpackComponent backpack = (BackpackComponent) agent.GetComponent (typeof (BackpackComponent)); mochila.numOre + = 2; minado = verdadero; ToolComponent tool = backpack.tool.GetComponent (typeof (ToolComponent)) como ToolComponent; tool.use (0.5f); if (tool.destroyed ()) Destroy (backpack.tool); mochila.tool = nulo;  devuelve true; 

La mayor parte de la acción es la checkProceduralPreconditions método. Busca el objeto de juego más cercano con un IronRockComponent, y guarda esta roca objetivo. Luego, cuando se ejecuta, obtiene esa roca objetivo guardada y realizará la acción en ella. Cuando la acción se reutiliza en la planificación nuevamente, todos sus campos se restablecen para que puedan calcularse nuevamente.

Estos son todos los componentes que se agregan a la Minero objeto de entidad en la unidad:


Para que su agente funcione, debe agregarle los siguientes componentes:

  • GoapAgent.
  • Una clase que implementa IGoap (en el ejemplo anterior, eso es Miner.cs).
  • Algunas acciones.
  • Una mochila (solo porque las acciones la usan; no está relacionada con GOAP).
Puede agregar las acciones que desee y esto cambiaría el comportamiento del agente. Incluso puedes darle todas las acciones para que pueda extraer minerales, forjar herramientas y cortar madera..

Aquí está la demo en acción otra vez:

Cada trabajador se dirige al objetivo que necesita para cumplir su acción (árbol, roca, tajo o lo que sea), realiza la acción y, a menudo, regresa a la pila de suministros para entregar sus productos. El herrero esperará un poco hasta que haya mineral de hierro en una de las pilas de suministros (agregada por el minero). El herrero luego se apaga y hace herramientas, y dejará las herramientas en la pila de suministros más cercana a él. Cuando la herramienta de un trabajador se rompe, se dirigirán a la pila de suministros cerca del Herrero donde están las nuevas herramientas..

Puede obtener el código y la aplicación completa aquí: http://github.com/sploreg/goap.

Conclusión

Con GOAP, puede crear una gran serie de acciones sin el dolor de cabeza de los estados interconectados que a menudo vienen con una máquina de estados finitos. Se pueden agregar y eliminar acciones de un agente para producir resultados dinámicos, así como para mantenerlo sano al mantener el código. Terminarás con una inteligencia artificial flexible, inteligente y dinámica..