Advertisement
  1. Game Development
  2. Unity 3D

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

Scroll to top
Read Time: 19 min

() translation by (you can also view the original English article)

La planificación de acción orientada a objetivos (GOAP, por sus siglas en inglés) 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 Demostracion

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.

¿Qué 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 chopper se puede suministrar con el objetivo MakeFirewood y tiene las acciones ChopLog, GetAxe y CollectBranches.

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

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

  • Necesita leña -> GetAxe -> ChopLog = hace leña
  • Necesita leña -> CollectBranches = 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 las sucursales puede llevar mucho tiempo y producir leña de mala calidad, por lo que no queremos que funcione todo el tiempo, solo cuando es necesario.

¿Para quién es GOAP?

En este momento, probablemente ya estés familiarizado con Finite State Machines (FSM), pero si no, revisa 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.

En 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 pueda 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 con su agente.

Acciones

Una acción es algo que el agente hace. 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 cuáles son las otras acciones.

Para ayudar a GOAP a determinar qué acciones queremos utilizar, a cada acción se le asigna un 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:

  • Costo de GetAxe: 2
  • Coste de ChopLog: 4
  • CollectBranches 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 -> CollectBranches (8) = hace leña (total: 8)

Obtener un hacha y cortar un tronco produce leña al menor costo de 6, mientras que recoger las ramas produce madera al mayor costo 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 ...

Precondiciones 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, la acción ChopLog 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 se ejecute la acción ChopLog. Afortunadamente, la acción GetAxe 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 hacerlo.

Para configurar esto, agreguemos condiciones y efectos a las acciones de nuestro cortador de madera:

  • Costo de GetAxe: 2. Condiciones previas: "un hacha está disponible", "no tiene un hacha". Efecto: "tiene un hacha".
  • Costo de ChopLog: 4. Condiciones previas: "tiene un hacha". Efecto: "hacer leña"
  • CollectBranches Cost: 8. Precondiciones: (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"

En cuanto a nuestras acciones disponibles actuales, la única parte de los estados que son relevantes para ellos 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!

1
GOAL: "make firewood"
2
Current State: "doesn’t have an axe", "an axe is available"
3
Can action ChopLog run?
4
    NO - requires precondition "has an axe"
5
    Cannot use it now, try another action.
6
Can action GetAxe run?
7
    YES, preconditions "an axe is available" and "doesn’t have an axe" are true.
8
    PUSH action onto queue, update state with action’s effect
9
New State
10
    "has an axe"
11
    Remove state "an axe is available" because we just took one.
12
Can action ChopLog run?
13
    YES, precondition "has an axe" is true
14
    PUSH action onto queue, update state with action’s effect
15
New State
16
    "has an axe", "makes firewood"
17
We have reached our GOAL of  "makes firewood"
18
Action sequence: GetAxe -> ChopLog

El planificador también ejecutará otras acciones, y no se detendrá cuando encuentre una solución a la meta. ¿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 planea, construye un árbol. Cada vez que se aplica una acción, se extrae de la lista de acciones disponibles, por lo que no tenemos una serie de 50 acciones GetAxe 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 -> CollectBranches (total: 10)
  • CollectBranches (total: 8)

Aunque GetAxe -> CollectBranches funciona, la ruta más barata es GetAxe -> ChopLog, por lo que esta es devuelta.

¿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 (float, int, Boolean o similar). En C #, podría verse así:

1
HashSet< KeyValuePair<string,object> > preconditions;
2
3
HashSet< KeyValuePair<string,object> > effects;

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 postulan de verdad.

Vale la pena enfatizar: planificar acciones no es lo mismo que ejecutarlas. Cuando un agente realiza la acción GetAxe, es probable que esté cerca de una pila de herramientas, reproduzca una animación de doblar hacia abajo y recoger, y luego almacene un objeto hacha en su mochila. Esto cambia el estado del agente. Pero, durante la planificación de GOAP, el cambio de estado es solo temporal, por lo que el planificador puede encontrar la solución óptima.

Precondiciones de Procedimiento

A veces, las acciones deben hacer un poco más para determinar si pueden ejecutarse. Por ejemplo, la acción GetAxe 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 cambiar.

Obviamente, algunas de estas condiciones previas de procedimiento pueden tardar un tiempo en ejecutarse, y deben realizarse en algo diferente al 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 las 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:

  • Libre
  • MoverA
  • EjecutarAccion

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 un objetivo, se pasa al Planificador de GOAP, junto con el estado de inicio del mundo y del agente, y el planificador devolverá una lista de acciones (si puede cumplir ese objetivo).

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: MoveTo.

El estado MoveTo le dirá al agente que necesita moverse a 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.

El estado PerformAction 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 cuadros, pero cuando finaliza, 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 queden acciones por realizar, en cuyo momento volvemos al estado Inactivo, obtenemos un nuevo objetivo y volvemos a planificar.

Un Verdadero Ejemplo de Codigo

¡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 utiliza algunos de los mismos ejemplos de WoodChopper que anteriormente.

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

Tenemos cuatro laboradores:

  • 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 utiliza el FSM y GoapPlanner para operar.
  • GoapAction: acciones que pueden realizar los agentes.
  • GoapPlanner: planifica las acciones para el GoapAgent.
  • FSM: la máquina de estados finitos.
  • FSMState: un estado en el FSM.
  • IGoap: la interfaz que usan nuestros verdaderos actores laboristas. Se relaciona con eventos para GOAP y el FSM.

Veamos la clase GoapAction, ya que es la que subclase:

1
public abstract class GoapAction : MonoBehaviour {
2
3
    private HashSet<KeyValuePair<string,object>> preconditions;
4
    private HashSet<KeyValuePair<string,object>> effects;
5
6
    private bool inRange = false;
7
8
    /* The cost of performing the action.

9
     * Figure out a weight that suits the action.

10
     * Changing it will affect what actions are chosen during planning.*/
11
    public float cost = 1f;
12
13
    /**

14
     * An action often has to perform on an object. This is that object. Can be null. */
15
    public GameObject target;
16
17
    public GoapAction() {
18
        preconditions = new HashSet<KeyValuePair<string, object>> ();
19
        effects = new HashSet<KeyValuePair<string, object>> ();
20
    }
21
22
    public void doReset() {
23
        inRange = false;
24
        target = null;
25
        reset ();
26
    }
27
28
    /**

29
     * Reset any variables that need to be reset before planning happens again.

30
     */
31
    public abstract void reset();
32
33
    /**

34
     * Is the action done?

35
     */
36
    public abstract bool isDone();
37
38
    /**

39
     * Procedurally check if this action can run. Not all actions

40
     * will need this, but some might.

41
     */
42
    public abstract bool checkProceduralPrecondition(GameObject agent);
43
44
    /**

45
     * Run the action.

46
     * Returns True if the action performed successfully or false

47
     * if something happened and it can no longer perform. In this case

48
     * the action queue should clear out and the goal cannot be reached.

49
     */
50
    public abstract bool perform(GameObject agent);
51
52
    /**

53
     * Does this action need to be within range of a target game object?

54
     * If not then the moveTo state will not need to run for this action.

55
     */
56
    public abstract bool requiresInRange ();
57
    
58
59
    /**

60
     * Are we in range of the target?

61
     * The MoveTo state will set this and it gets reset each time this action is performed.

62
     */
63
    public bool isInRange () {
64
        return inRange;
65
    }
66
    
67
    public void setInRange(bool inRange) {
68
        this.inRange = inRange;
69
    }
70
71
72
    public void addPrecondition(string key, object value) {
73
        preconditions.Add (new KeyValuePair<string, object>(key, value) );
74
    }
75
76
77
    public void removePrecondition(string key) {
78
        KeyValuePair<string, object> remove = default(KeyValuePair<string,object>);
79
        foreach (KeyValuePair<string, object> kvp in preconditions) {
80
            if (kvp.Key.Equals (key))
81
                remove = kvp;
82
        }
83
        if ( !default(KeyValuePair<string,object>).Equals(remove) )
84
            preconditions.Remove (remove);
85
    }
86
87
88
    public void addEffect(string key, object value) {
89
        effects.Add (new KeyValuePair<string, object>(key, value) );
90
    }
91
92
93
    public void removeEffect(string key) {
94
        KeyValuePair<string, object> remove = default(KeyValuePair<string,object>);
95
        foreach (KeyValuePair<string, object> kvp in effects) {
96
            if (kvp.Key.Equals (key))
97
                remove = kvp;
98
        }
99
        if ( !default(KeyValuePair<string,object>).Equals(remove) )
100
            effects.Remove (remove);
101
    }
102
103
    
104
    public HashSet<KeyValuePair<string, object>> Preconditions {
105
        get {
106
            return preconditions;
107
        }
108
    }
109
110
    public HashSet<KeyValuePair<string, object>> Effects {
111
        get {
112
            return effects;
113
        }
114
    }
115
}

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

Aqui esta una de las acciones:

1
public class MineOreAction : GoapAction
2
{
3
    private bool mined = false;
4
    private IronRockComponent targetRock; // where we get the ore from

5
6
    private float startTime = 0;
7
    public float miningDuration = 2; // seconds

8
9
    public MineOreAction () {
10
        addPrecondition ("hasTool", true); // we need a tool to do this

11
        addPrecondition ("hasOre", false); // if we have ore we don't want more

12
        addEffect ("hasOre", true);
13
    }
14
    
15
    
16
    public override void reset ()
17
    {
18
        mined = false;
19
        targetRock = null;
20
        startTime = 0;
21
    }
22
    
23
    public override bool isDone ()
24
    {
25
        return mined;
26
    }
27
    
28
    public override bool requiresInRange ()
29
    {
30
        return true; // yes we need to be near a rock

31
    }
32
    
33
    public override bool checkProceduralPrecondition (GameObject agent)
34
    {
35
        // find the nearest rock that we can mine

36
        IronRockComponent[] rocks = FindObjectsOfType ( typeof(IronRockComponent) ) as IronRockComponent[];
37
        IronRockComponent closest = null;
38
        float closestDist = 0;
39
        
40
        foreach (IronRockComponent rock in rocks) {
41
            if (closest == null) {
42
                // first one, so choose it for now

43
                closest = rock;
44
                closestDist = (rock.gameObject.transform.position - agent.transform.position).magnitude;
45
            } else {
46
                // is this one closer than the last?

47
                float dist = (rock.gameObject.transform.position - agent.transform.position).magnitude;
48
                if (dist < closestDist) {
49
                    // we found a closer one, use it

50
                    closest = rock;
51
                    closestDist = dist;
52
                }
53
            }
54
        }
55
        targetRock = closest;
56
        target = targetRock.gameObject;
57
        
58
        return closest != null;
59
    }
60
    
61
    public override bool perform (GameObject agent)
62
    {
63
        if (startTime == 0)
64
            startTime = Time.time;
65
66
        if (Time.time - startTime > miningDuration) {
67
            // finished mining

68
            BackpackComponent backpack = (BackpackComponent)agent.GetComponent(typeof(BackpackComponent));
69
            backpack.numOre += 2;
70
            mined = true;
71
            ToolComponent tool = backpack.tool.GetComponent(typeof(ToolComponent)) as ToolComponent;
72
            tool.use(0.5f);
73
            if (tool.destroyed()) {
74
                Destroy(backpack.tool);
75
                backpack.tool = null;
76
            }
77
        }
78
        return true;
79
    }
80
    
81
}

La mayor parte de la acción es el método checkProceduralPreconditions. 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 al objeto de entidad Miner en Unity:


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.

Aqui esta la demostracion en accion de nuevo:

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.

Conclusion

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.