Awesome
OpenGOAP
OpenGOAP is an open source tool to design and monitor goal orientated action planning in Unity.
Features
- World state consisting of local (
GameObject
specific) and global states - A* planner for selecting the current optimum plan
- Action layers to improve performance (e.g. avoid checking if sleeping is a viable action when running from danger)
- GUI to visualise the current active plan and goals in priority order
- Optional logger for additional debugging
Installation
Tested on Windows 10 using Unity 2021.2.11f1
- Install Git (e.g. for Windows https://gitforwindows.org/)
- In Unity open the package manager (
Window
-->Package Manager
) - Use the plus button to add a new package, and choose
Add package from git URL
- Add the URL
https://github.com/toastisme/OpenGOAP.git
Getting Started
How OpenGOAP works
- A
GameObject
using GOAP has aGOAPPlanner
,WorldState
, and a series ofGOAPGoal
andGOAPAction
components. - The
GOAPPlanner
finds theGOAPGoal
with the highest priority that has a viable action plan and executes that plan. - An action plan is a list of
GOAPActions
, and is viable if the finalGOAPAction
satisfies the conditions of theGOAPGoal
, and each preceedingGOAPAction
satisfies the conditions of the action that follows it, where the firstGOAPAction
's conditions are satisfied by the currentWorldState
. - To find the optimum viable action plan the
GOAPPlanner
uses the A* search algorithm to find the series of actions which have the minimum cost.
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:
SetupDerived()
called when theGameObject
is first initialisedOnActivate()
called the when goal is first selected by theGOAPPlanner
OnDeactivate()
called when the goal is deselected by theGOAPPlanner
(either due to completing the goal or finding a better one)GetPriority()
value between 0 and 1PreconditionsSatisfied(WorldState)
can be used to avoid searching for action plans due to some known requirementConditionsSatisfied(WorldState)
true if the aim of the goal is satisfied (i.e the currentWorldState
satisfies theGOAPGoal
conditions
)OnTick()
called every frame by theGOAPPlanner
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:
SetupDerived()
called when theGameObject
is first initialised. Used for e.g. getting components required for the actionSetupEffects()
called when theGameObject
is first initialised. Used to populate the effects boolean dictionarySetupActionLayers()
called when theGameObject
is first initalised. Used to populate whichactionLayers
the action belongs toOnActivateDerived()
called the when action is first selected by theGOAPPlanner
OnDeactivateDerived()
called when the action is deselected by theGOAPPlanner
(either due to completing the action or changing plan)GetCost()
value between 0 and 1PreconditionsSatisfied(WorldState)
Can this action run based onWorldState
EffectsSatisfied(WorldState)
Are the action's effects all present inWorldState
SatisfiesConditions(Dictionary<string, bool>)
Does this action satisfy all boolean conditions in the dictionaryOnTick()
called every frame by theGOAPPlanner
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).
Running Tests
- In your project packages folder, open the manifest file and add
"com.davidmcdonagh-opengoap"
to"testables"
(see the Enabling tests for a package section here). - In the Editor open `Window -> General -> Test Runner
- Under
PlayMode
you should now see tests for OpenGOAP
TODO
- Partial plans
- Fixed sequences of actions
Further Info
- Full documentation
- See also OpenBehaviourTree