AI · guide
GOAP
World keys, conditions and effects, the bounded backwards search, and why only the head of a plan is ever committed.
Edit this page on GitHubDocuments
- GoapWorldKey
- GoapComparison
- GoapCondition
- GoapEffect
- IGoapWorldSource
- GoapReading
- GoapWorldSources
- BlackboardWorldSource
- GoapKeyDefinition
- GoapWorldKeys
- GoapMoveMode
- IGoapTargetSensor
- GoapTargetSensors
- GoapTargetLookup
- GoapAction
- GoapGoal
- GoapDomain
- GoapDomainLibrary
- PlanFailure
- GoapRejection
- GoapConsidered
- GoapSettings
- GoapTarget
- IActionCostModel
- ActionCostModels
- GoapCapabilities
- GoapPlan
- GoapPlanner
- GoapSnapshot
- GoapPlanRequest
- GoapRequestState
- GoapPlanQueue
- ReplanContext
- IReplanPolicy
- ReplanPolicies
- GoapMemory
- GoapSourceKind
- GoapKeyContent
- GoapConditionContent
- GoapEffectContent
- GoapActionContent
- GoapGoalContent
- GoapDomainContent
- GoapDomainContentCompiler
- NavigationCostModel
- GoapWiring
- GoapGraphProjection
- GoapDomainDocument
- GoapDomainView
- GoapDomainEditorFactory
- GoapDomainImporter
- GoapDomainImportSettings
- PositionLookup
What it is#
GOAP is the third of the three planners: an agent is given *goals* rather than behaviour, and a search works out a sequence of actions that would satisfy one. A goal is a set of conditions over world keys; an action declares conditions it needs and effects it has, and the resolver chains backwards from the goal.
It produces the same thing the other two do — an IAgentAction index — so a project writes
MoveToTask once and gets it in a tree, in a utility set and in a plan.
What it is for#
The characters whose interest is that you cannot predict them. A shopkeeper who fetches a ladder because the thing you asked for is on a high shelf; a survivor who lights a fire because it is cold and there is wood; anything where the *combination* is the content and enumerating the combinations by hand is the thing you are trying not to do.
You do *not* want it for a guard patrol. A procedure is a behaviour tree, a judgement is a utility set, and a plan is neither — reaching for a search when the answer is three nodes of a selector buys a node budget, a re-plan policy and a queue for nothing.
Using it#
Three tables: world keys, actions and goals.
using Vixen.Ai;using Vixen.Core;public static class Orchard { public static GoapDomain Build(ushort pickUp, ushort eat) { var keys = new GoapWorldKeys( new(Symbol.Intern("pears-on-ground"), GoapWorldSources.Constant(1)), new(Symbol.Intern("pears-carried"), GoapWorldSources.Constant(0)), new(Symbol.Intern("hunger"), GoapWorldSources.Constant(80)) ); var ground = new GoapWorldKey(0); var carried = new GoapWorldKey(1); var hunger = new GoapWorldKey(2); return new( Symbol.Intern("orchard"), keys, [ new GoapAction( Symbol.Intern("pick-up-pear"), pickUp, [new(ground, GoapComparison.Greater, 0)], new GoapEffect(carried, Increases: true) ), new GoapAction( Symbol.Intern("eat-pear"), eat, [new(carried, GoapComparison.Greater, 0)], new GoapEffect(hunger, Increases: false) ) ], [new GoapGoal(Symbol.Intern("not-hungry"), [new(hunger, GoapComparison.Less, 20)])] ); }}An agent runs it by naming it: AiAgent.Planning(index), where the index is what
AiSystem.Domains.Add returned.
The matching rule is a direction#
A condition wanting a key greater is served by an action with a positive effect on that key; one wanting it smaller by a negative one. That is the whole of it.
⚠ An effect is a direction and not an amount, and that is what makes GOAP authorable. "Eating reduces hunger by 40" makes every plan a simulation of arithmetic nobody can predict and makes the graph depend on numbers a designer tunes. "Eating reduces hunger" stays true while the numbers move.
⚠ There is no equality comparison, and that is not an omission. An equality has no direction, so nothing could ever be said to serve it — a condition the resolver could match only by accident and a graph edge it could never build.
The graph is built once#
Which action's effect can serve which action's condition is a fact about the action set, so it is computed when the domain is constructed and never inside a search. What is per agent is the condition evaluations and the costs.
The search is bounded, and the bound is reported#
⚠ A GOAP search is exponential in depth and the engine must not hang on a badly authored action
set. GoapSettings carries a node budget and a depth limit; exceeding either produces
PlanFailure.BudgetExhausted or PlanFailure.DepthExceeded naming the goal.
| Failure | What it means |
|---|---|
| AlreadyMet | the goal is true. Not a failure, and worth telling apart from one |
| Unreachable | nothing this agent can do leads there |
| BudgetExhausted | the search ran out of nodes |
| DepthExceeded | every chain hit the depth limit |
⚠ **A failure says *which* goal and not much else, which is why the search can also write down what it
turned down. GoapPlanner.Traced collects a GoapConsidered per rejected action — conditions
unmet, not capable, already in the chain, too deep — and it is what the editor's viewer accents. It is
null by default: a resolve runs on a worker thread inside a per-frame budget, so a list every
search filled would be an allocation and a write per node to serve a panel nobody has open.
⚠ A plan is a chain, so an action with two unmet conditions is served one at a time — and that is correct rather than a simplification.** Only the head is committed: the head is by construction runnable now, running it changes the world, and the next resolve plans from what the world then is.
Only the head is committed#
An agent that *follows* a sequence walks into a door that closed after the plan was made; one that re-plans every frame is a search per agent per frame. So the tail is advisory — it is kept, it is what the viewer draws, and the head's conditions are re-checked against the live world before it starts.
IReplanPolicy decides when to think again: Reactive (the step ended or there is no plan),
Proactive(interval) (that, and on an interval, so a better plan can be found), Manual (the game
says when — and still re-plans with nothing to do, or an agent stands there for ever).
Resolves do not run on the frame that asked for them#
GoapPlanQueue is NavPathQueue's arrangement for its reason. ⚠ The world is read at Submit, on
the thread that owns the agent — what reaches the search is a GoapSnapshot, a few arrays of
numbers, so a resolve may run on a worker thread without touching a World or a Blackboard from it.
⚠ The frame's planning cost is ResolvesPerStep × NodeBudget. Neither number bounds anything on
its own, and a project raising one should know it is raising the product.
An action happens somewhere#
An action declares a target key, resolved by a sensor to a position or an entity, plus a stopping
distance and a GoapMoveMode. ⚠ Movement is not modelled as actions in the graph — a MoveTo(x)
per destination makes the graph a function of the world's contents. The planner produces a target and
the existing movement stack gets there.
⚠ The distance cost is a straight line by default, not a path length. A path query per candidate
action per resolve is a nav search per edge of the search graph. NavigationCostModel in
Vixen.Ai.Nodes is the one that asks the mesh, and it is a navigation query per action per resolve —
affordable at a few dozen agents and not affordable for a crowd.
Capabilities are per agent#
GoapCapabilities is a mask over the domain's actions, carried on the AiAgent. ⚠ A domain per
capability set would be a graph rebuild per permutation — a wounded guard and a healthy one share
one graph and plan differently.
Examples#
A domain as a file. The tables are authored and the graph is not:
version: 1name: Orchardblackboard: - { name: carried, type: Int }keys: - { name: pears-on-ground, source: Registered, from: pears-nearby } - { name: pears-carried, source: Blackboard, from: carried } - { name: hunger, source: Registered, from: hunger }actions: - name: PickUpPear task: MoveTo fields: { Key: pear } target: nearest-pear conditions: [{ key: pears-on-ground, comparison: Greater, value: 0 }] effects: [{ key: pears-carried, increases: true }] - name: EatPear task: Wait fields: { Seconds: "2" } conditions: [{ key: pears-carried, comparison: Greater, value: 0 }] effects: [{ key: hunger, increases: false }]goals: - name: NotHungry priority: 1 conditions: [{ key: hunger, comparison: Less, value: 20 }]Wiring it up — the world sources and the target sensors are the game's:
resolver.AddWorldSource("hunger", GoapWorldSources.From((in AgentContext c) => c.World.Get<Hunger>(c.Entity).Value));resolver.AddWorldSource("pears-nearby", GoapWorldSources.From((in AgentContext c) => orchard.CountNear(c.Entity)));var sensors = GoapWiring.Sensors();sensors.Add(Symbol.Intern("nearest-pear"), GoapWiring.FromKey(pearKey));GoapDomainContentCompiler.TryCompile(content, resolver, out var diagnostics, out var domain);Asking for a plan directly, which is what a test or a tool does:
var planner = new GoapPlanner(domain, new() { NodeBudget = 256, DepthLimit = 6 });var plan = new GoapPlan();if (planner.Resolve(in context, plan) == PlanFailure.None) { // plan.Head is runnable now; plan.Steps is what the viewer draws.}See also#
- Behaviour trees — the planner for a procedure, and where a plan is not one.
- Utility — the planner for a judgement, and the one whose actions these share.
- The blackboard — where a world key usually projects from.