Home

Awesome

OpenGOAP

OpenGOAP is an open source tool to design and monitor goal orientated action planning in Unity. OpenGOAP

Features

Installation

Tested on Windows 10 using Unity 2021.2.11f1

Getting Started

How OpenGOAP works

GOAPGoal

A GOAPGoal has a dictionary of boolean conditions that need to be met to satisfy the goal, and optionally a dictionary of boolean preconditions that must be met before it can be considered (beyond having a viable plan). Each GOAPGoal belongs to an actionLayer (string), which tells the planner only consider GOAPActions for this goal that have the same actionLayer (by default this is set to All, where all GOAPActions are considered). For this component the main interface is the following:

GOAPAction

A GOAPAction has a dictionary of boolean preconditions that need to be met before it can run, and a dictionary of boolean effects that will occur as a result of running to completion. For this component the main interface is the following:

WorldState

A WorldState is composed of two StateSets, one global and one local. The global StateSet is common to multiple GameObjects, whereas the local StateSet is specific to the GameObject the WorldState is attached to. A StateSet is simply several dictionaries of strings mapped to values (analogous to a blackboard for behaviour trees). The WorldState differentiates between local and global states by assuming a g_ prefix for all global states. I.e, if you call WorldState.AddState("InDanger", true), this would be added to the local StateSet, whereas WorldState.AddState("g_InDanger", true) would be added to the global StateSet and apply to all other GameObjects sharing the same Stateset. By default, an absent boolean key is assumed to be equivalent to the key being present with a false value. This can be turned off using SetGlobalDefaultFalse and SetLocalDefaultFalse for the global ahd local StateSets, respectively. (The motivation for this is to avoid the need of requiring many boolean states to properly define a particular WorldState. For example, if the goal is to harvest wood, a viable plan could be to take wood from the wood store and put it back in. To avoid this it's simpler to have a woodExtractedFromStore = true effect added to a Action_TakeWoodFromStore, rather than having woodExtractedFromStore = false on all other approaches.)

HarvestWood Example

Consider an agent that has a goal of harvesting wood. The goal script could look something like this:

using UnityEngine;
using GOAP;

public class Goal_HarvestWood : GOAPGoal
{

    protected override void SetupDerived(){
        conditions["WoodHarvested"] = true; // GOAPPlanner will consider the goal complete when this condition is in the WorldState
        actionLayer = "Wood"; // Only actions in this layer will be considered by the GOAPPlanner for this goal
    }

    public override float GetPriority()
    {
        /*
         * Priority depends on the number of known people, 
         * the current global wood level (g_wood), and the 
         * amount of wood obtained from a single harvest (WoodExtractValue)
         */
    
        float demand = worldState.GetFloatState("People");
        demand *= worldState.GetFloatState("WoodExtractValue");
        return 1/(1+(worldState.GetFloatState("g_Wood")/demand));
    }
}

We now need a series of GOAPActions, atleast one of which has "WoodHarvested" == true in their effects dictionary. One of these could be taking wood to the wood store:

using UnityEngine;
using GOAP;

public class Action_TakeWoodToStore : GOAPAction
{
 
    protected override void SetupActionLayers(){
        actionLayers.Add("Wood"); // This action is in the same layer as Goal_HarvestWood. GOAPActions can belong to many actionLayers.
    }
    
    protected override void SetupEffects(){
        effects["WoodHarvested"] = true; // This action satisfies the conditions of Goal_HarvestWood
        effects["g_WoodAvailableAtStore"] = true; // Lets other GameObjects know wood is at the store
    }
    
    protected override void SetupConditions(){
        preconditions["HoldingWood"] = true; // Cannot perform this action unless holding wood
    }
    
    public override float GetCost(){
        return worldState.GetFloatState("Fatigue"); // action cost increases with fatigue
    }

    protected override void OnActivateDerived(){
        /* Called when the action is first selected by the GOAPPlanner.
           Some code here could identify the wood store position */
    }

    protected override void OnDeactivateDerived(){
        /* Called when the action is deselected by the GOAPPlanner. */
    }

    public override void OnTick()
    {
        /*
         * Move towards wood store
         * If at wood store deposit wood and call 
         * AddTemporaryState("WoodHarvested, true") 
         * TemporaryStates are automatically removed when the action completes
         * This is useful for states that are no longer relevant after the action completes, and saves you needing to 
         * remember to remove it manually.
         */
    }
}

Note the use of GOAPAction.AddTemporaryState("WoodHarvested, true"). In this case WoodHarvested is only true at the instant the action has been completed. AddTemporaryState is just a wrapper for WorldState.AddState, but the state is automatically removed as part of GOAPAction.OnDeactivate, ensuring the state is removed as soon as the action completes or is cancelled.

Action_TakeWoodToStore has a precondition of "HoldingWood" == true, and so we could have another action Action_PickUpWood, which picks up the nearest wood, given the precondition "WoodNearby" is true. This preconditon in turn could be in the effects dictionary of both Action_ChopDownTree and Action_LookAround. The latter could have a higher cost than the former, and so would only be selected by the GOAPPlanner if, say, the GameObject did not have an axe. Action_LookAround could have no preconditions, and so would always be viable from the current WorldState.

To have a GameObject utilise these behaviours simply add the goal and action scripts, along with a GOAPPlanner and WorldState script to the GameObject as components. The global StateSet is kept on a separate (empty) GameObject. This can be added in the inspector on the WorldState component, or added in code via WorldState.SetGlobalState(StateSet).

Adding many GOAPGoals and GOAPActions can get messy in the inspector. To help with this the ComponentGrouper component can be added, which allows components to be grouped together and hidden. This was taken from the amazing people here (the Kitsuba version).

Visualisation and Debugging

The GOAPPlanner has a boolean Display Planner in the inspector. If this is set to true, when clicking on the GameObject in the Hierarchy navigation bar, a GUIPlanner window will be displayed showing the current active plan, and the priorities of all goals. For a given GOAPGoal, if PreconditionsSatisfied() == false, the goal will be greyed out.

Additional debugging can be done by adding a GOAPLogger to the scene (Packages/OpenGOAP/Runtime/Prefabs/GOAPLogger). This contains a logger for the active plan and another for the planner, which can be assigned to a GameObject's GOAPPlanner in the inspector. These can be turned on or off individually on the loggers themselves, in the inspector, by selecting Show Logs. The Planner logger will print log statements for each step through the A* process when finding the optimum plan. The ActivePlan logger will essentially print log statements with the same information as the GUIPlanner window, but this can still be useful (for example for identifying if plans are being selected/completing instantly, repeatedly, due to a condition in WorldState not being reset correctly).

OpenGOAP

Running Tests

TODO

Further Info