Last updated: 2026-09-26

U
Undergraduate level

Goal-Oriented Action Planning (GOAP)

Most of the AI we met on the Game AI page is reactive: steering behaviours respond to what is around the agent right now, and pathfinding answers "how do I get there?". Goal-Oriented Action Planning answers a more interesting question: "what should I do next, and in what order?" Instead of hand-authoring every behaviour sequence, you give the agent a set of actions and a goal, and let a planner work out the sequence for itself [1].

From Reactions to Intentions Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

Finite state machines and behaviour trees are wonderful until the design grows. Every new behaviour means new transitions or new branches, and the combinations multiply: a guard who can fight with a rifle, a pistol or fists, take cover, call for help and retreat needs a lot of hand-wired logic. GOAP, introduced to games by Jeff Orkin for F.E.A.R. (2005), flips the problem around [1] [2]:cf. the FSM explosion on ai.html

  • Actions declare themselves โ€” each action states what must be true before it can run (preconditions) and what becomes true afterwards (effects).
  • Goals are just desired facts โ€” "the threat is removed", "I am safe", "the alarm is raised".
  • A search algorithm finds the sequence โ€” the same A* family you already use for pathfinding, but searching through world states instead of nav polygons.

The famous result: F.E.A.R.'s soldiers ran on a state machine with only three states (goto, animate, use smart object), because the interesting decisions had moved into the planner [1].

The Core Idea FoundationalKnowledge that endures for decades โ€” core principles

A GOAP system needs four ingredients:

  • World state โ€” a set of symbols the agent believes to be true or false: armed, has_ammo, in_range.
  • Goal โ€” the subset of symbols we want to make true.
  • Actions โ€” operators with preconditions, effects and a cost. Cost is your tuning dial: raising the cost of FindAmmo makes melee more attractive.
  • Planner โ€” a search from the current state towards the goal (or, in mature implementations, backwards from the goal).
graph LR S["weapon_holstered
enemy_visible"] -->|DrawWeapon| A["armed"] A -->|FindAmmo| B["armed
has_ammo"] B -->|LoadWeapon| C["loaded"] C -->|Approach| D["in_range"] D -->|Attack| G(("Goal:
threat_removed")) style G fill:#2ECC71,color:#fff

A Worked Example FoundationalKnowledge that endures for decades โ€” core principles

Give a guard five actions and the goal threat_removed:

ActionPreconditionsEffectsCost
DrawWeaponweapon_holsteredarmed, ยฌweapon_holstered1
FindAmmoโ€”has_ammo3
LoadWeaponarmed, has_ammoloaded2
Approachenemy_visiblein_range2
Attackarmed, loaded, in_rangethreat_removed1

No sequence is authored anywhere, yet the planner discovers DrawWeapon โ†’ Approach โ†’ FindAmmo โ†’ LoadWeapon โ†’ Attack. Delete FindAmmo from the action set (the level has no ammo pickups) and the same code simply finds a different plan โ€” or reports that no plan exists, which is your cue to fall back to a flee goal.

This is the approved approach. When the planner reports that no plan satisfies the goal, treat that as your cue to fall back to a different goal โ€” such as fleeing โ€” rather than as an error state.

A Minimal Planner Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

The heart of GOAP is small. This is a forward uniform-cost search โ€” production planners add a heuristic (making it A*), regressive search from the goal, and per-agent action sets:

import heapq
from itertools import count

class Action:
    def __init__(self, name, cost, preconditions, effects):
        self.name = name
        self.cost = cost
        self.preconditions = preconditions  # dict of symbol -> bool
        self.effects = effects              # dict of symbol -> bool

    def applicable(self, state):
        return all(state.get(k, False) == v for k, v in self.preconditions.items())

    def apply(self, state):
        new_state = dict(state)
        new_state.update(self.effects)
        return new_state

def plan(start, goal, actions):
    """Cheapest action sequence that satisfies the goal (uniform-cost search)."""
    tie = count()  # tie-breaker so dict states never get compared
    frontier = [(0.0, next(tie), start, [])]
    seen = set()
    while frontier:
        cost, _, state, path = heapq.heappop(frontier)
        if all(state.get(k, False) == v for k, v in goal.items()):
            return path
        key = frozenset(state.items())
        if key in seen:
            continue
        seen.add(key)
        for action in actions:
            if action.applicable(state):
                heapq.heappush(frontier, (cost + action.cost, next(tie),
                                          action.apply(state), path + [action.name]))
    return None  # No plan satisfies the goal

actions = [
    Action("DrawWeapon", 1, {"weapon_holstered": True},
                            {"weapon_holstered": False, "armed": True}),
    Action("FindAmmo",   3, {}, {"has_ammo": True}),
    Action("LoadWeapon", 2, {"armed": True, "has_ammo": True}, {"loaded": True}),
    Action("Approach",   2, {"enemy_visible": True}, {"in_range": True}),
    Action("Attack",     1, {"armed": True, "loaded": True, "in_range": True},
                            {"threat_removed": True}),
]

start = {"weapon_holstered": True, "enemy_visible": True}
goal = {"threat_removed": True}
print(plan(start, goal, actions))
# ['DrawWeapon', 'Approach', 'FindAmmo', 'LoadWeapon', 'Attack']
#include <algorithm>
#include <map>
#include <queue>
#include <set>
#include <string>
#include <vector>

using WorldState = std::map<std::string, bool>;

static bool satisfies(const WorldState& state, const WorldState& conditions) {
    return std::all_of(conditions.begin(), conditions.end(), [&](const auto& kv) {
        auto it = state.find(kv.first);
        return (it != state.end() ? it->second : false) == kv.second;
    });
}

struct Action {
    std::string name;
    float cost;
    WorldState preconditions;
    WorldState effects;

    bool applicable(const WorldState& state) const {
        return satisfies(state, preconditions);
    }

    WorldState apply(WorldState state) const {
        for (const auto& [k, v] : effects) state[k] = v;
        return state;
    }
};

struct PlanNode {
    float cost;
    WorldState state;
    std::vector<std::string> path;
    bool operator>(const PlanNode& o) const { return cost > o.cost; }
};

// Cheapest action sequence that satisfies the goal (uniform-cost search)
std::vector<std::string> plan(const WorldState& start, const WorldState& goal,
                              const std::vector<Action>& actions) {
    std::priority_queue<PlanNode, std::vector<PlanNode>, std::greater<>> frontier;
    std::set<WorldState> seen;
    frontier.push({0, start, {}});

    while (!frontier.empty()) {
        PlanNode current = frontier.top();
        frontier.pop();
        if (satisfies(current.state, goal)) return current.path;
        if (!seen.insert(current.state).second) continue;

        for (const Action& action : actions) {
            if (!action.applicable(current.state)) continue;
            auto path = current.path;
            path.push_back(action.name);
            frontier.push({current.cost + action.cost,
                           action.apply(current.state), std::move(path)});
        }
    }
    return {};  // No plan satisfies the goal
}
import java.util.*;

public record Action(String name, double cost,
                     Map<String, Boolean> preconditions,
                     Map<String, Boolean> effects) {

    public boolean applicable(Map<String, Boolean> state) {
        return preconditions.entrySet().stream()
            .allMatch(e -> state.getOrDefault(e.getKey(), false).equals(e.getValue()));
    }

    public Map<String, Boolean> apply(Map<String, Boolean> state) {
        var next = new HashMap<>(state);
        next.putAll(effects);
        return next;
    }
}

public class Planner {
    private record Node(double cost, Map<String, Boolean> state, List<String> path) {}

    /** Cheapest action sequence that satisfies the goal (uniform-cost search). */
    public static List<String> plan(Map<String, Boolean> start, Map<String, Boolean> goal,
                                    List<Action> actions) {
        var frontier = new PriorityQueue<Node>(Comparator.comparingDouble(Node::cost));
        var seen = new HashSet<Map<String, Boolean>>();
        frontier.add(new Node(0, start, List.of()));

        while (!frontier.isEmpty()) {
            Node current = frontier.poll();
            boolean satisfied = goal.entrySet().stream()
                .allMatch(e -> current.state().getOrDefault(e.getKey(), false)
                                              .equals(e.getValue()));
            if (satisfied) return current.path();
            if (!seen.add(current.state())) continue;

            for (Action action : actions) {
                if (!action.applicable(current.state())) continue;
                var path = new ArrayList<>(current.path());
                path.add(action.name());
                frontier.add(new Node(current.cost() + action.cost(),
                                      action.apply(current.state()), path));
            }
        }
        return null;  // No plan satisfies the goal
    }
}
using System.Collections.Generic;
using System.Linq;

public record GoapAction(string Name, float Cost,
                         Dictionary<string, bool> Preconditions,
                         Dictionary<string, bool> Effects)
{
    public bool Applicable(IReadOnlyDictionary<string, bool> state) =>
        Preconditions.All(kv => state.GetValueOrDefault(kv.Key) == kv.Value);

    public Dictionary<string, bool> Apply(IReadOnlyDictionary<string, bool> state)
    {
        var next = new Dictionary<string, bool>(state);
        foreach (var (k, v) in Effects) next[k] = v;
        return next;
    }
}

public static class Planner
{
    private sealed record Node(float Cost, Dictionary<string, bool> State, List<string> Path);

    // Cheapest action sequence that satisfies the goal (uniform-cost search)
    public static List<string>? Plan(Dictionary<string, bool> start,
                                     Dictionary<string, bool> goal,
                                     IReadOnlyList<GoapAction> actions)
    {
        var frontier = new PriorityQueue<Node, float>();
        var seen = new HashSet<string>();
        frontier.Enqueue(new Node(0, start, new List<string>()), 0);

        while (frontier.Count > 0)
        {
            var current = frontier.Dequeue();
            if (goal.All(kv => current.State.GetValueOrDefault(kv.Key) == kv.Value))
                return current.Path;

            var key = string.Join(",", current.State.OrderBy(kv => kv.Key));
            if (!seen.Add(key)) continue;

            foreach (var action in actions)
            {
                if (!action.Applicable(current.State)) continue;
                var path = new List<string>(current.Path) { action.Name };
                frontier.Enqueue(new Node(current.Cost + action.Cost,
                                          action.Apply(current.State), path),
                                 current.Cost + action.Cost);
            }
        }
        return null;  // No plan satisfies the goal
    }
}
Action = Struct.new(:name, :cost, :preconditions, :effects) do
  def applicable?(state)
    preconditions.all? { |k, v| state.fetch(k, false) == v }
  end

  def apply(state)
    state.merge(effects)
  end
end

# Cheapest action sequence that satisfies the goal (uniform-cost search)
def plan(start, goal, actions)
  frontier = [[0.0, start, []]]
  seen = []
  until frontier.empty?
    frontier.sort_by!(&:first)
    cost, state, path = frontier.shift
    return path if goal.all? { |k, v| state.fetch(k, false) == v }
    next if seen.include?(state)

    seen << state
    actions.each do |action|
      next unless action.applicable?(state)

      frontier << [cost + action.cost, action.apply(state), path + [action.name]]
    end
  end
  nil # No plan satisfies the goal
end

actions = [
  Action.new("DrawWeapon", 1, { weapon_holstered: true },
             { weapon_holstered: false, armed: true }),
  Action.new("FindAmmo",   3, {}, { has_ammo: true }),
  Action.new("LoadWeapon", 2, { armed: true, has_ammo: true }, { loaded: true }),
  Action.new("Approach",   2, { enemy_visible: true }, { in_range: true }),
  Action.new("Attack",     1, { armed: true, loaded: true, in_range: true },
             { threat_removed: true })
]

start = { weapon_holstered: true, enemy_visible: true }
goal = { threat_removed: true }
p plan(start, goal, actions)
# => ["DrawWeapon", "Approach", "FindAmmo", "LoadWeapon", "Attack"]

Making It Production-Ready Applied / MethodologicalKnowledge with a 5โ€“10 year half-life โ€” stable practice

  • Replan on surprise โ€” plans are cheap. When a precondition breaks mid-plan (the ammo crate is empty), throw the plan away and search again from the new state.
  • Sensors feed the world state โ€” perception systems write symbols (enemy_visible), the planner only reads them. This separation keeps both sides testable.
  • Costs are behaviour โ€” a cowardly enemy is one whose TakeCover is cheap; an aggressive one discounts Attack. Same actions, different personalities.
  • Prefer regressive search โ€” searching backwards from the goal usually visits far fewer states, because most of the world is irrelevant to any one goal [2].
  • Watch the state space โ€” boolean symbols keep the search tractable; if you need numeric state (health, distance), quantise it or handle it in preconditions.

Planning Beyond Games FoundationalKnowledge that endures for decades โ€” core principles

GOAP is a games-friendly repackaging of STRIPS-style planning from 1970s AI research [3] โ€” a reminder that good ideas travel between fields. And the travel needn't stop at the games border: a project plan is also "a sequence of actions, each with preconditions, effects and costs, searching for a goal state". We explore that thought โ€” using GOAP to plan software projects, and the wider habit of borrowing ideas across domains โ€” on Cross-Pollination of Ideas in the Software Engineering track. For what GOAP gives you that a rule-based logic engine (Prolog-style backward chaining) doesn't โ€” an ordered, costed plan versus a yes/no derivability answer โ€” worked through on the same small domain both ways, see GOAP vs Rule-Based Reasoning.1971, still holding up

  • Game AI โ€” where GOAP sits on the spectrum from scripted behaviour to finite state machines to genuine planning, and why F.E.A.R.'s soldiers still feel sharp nearly two decades later.
  • Game Programming Patterns โ€” the State pattern this page's opening compares GOAP against, given a concrete object shape with enter/update/exit.
  • GOAP vs Rule-Based Reasoning โ€” the same small domain solved both as a costed plan and as a pure logic query, showing exactly what a planner gives you that backward-chaining resolution doesn't.
  • Cross-Pollination of Ideas โ€” the wider habit of borrowing a working structure from one domain into another, including this page's own observation that a build system is GOAP wearing a different hat.

References

  1. Orkin, J. (2006). "Three States and a Plan: The A.I. of F.E.A.R." Game Developers Conference 2006. https://www.gamedevs.org/uploads/three-states-plan-ai-of-fear.pdf
  2. Millington, I. (2019). AI for Games (3rd ed.). CRC Press. Chapter 5: Decision Making. https://www.routledge.com/AI-for-Games/Millington/p/book/9781138483972
  3. Fikes, R.E. & Nilsson, N.J. (1971). "STRIPS: A New Approach to the Application of Theorem Proving to Problem Solving." Artificial Intelligence, 2(3โ€“4), 189โ€“208. https://doi.org/10.1016/0004-3702(71)90010-5

Graduate Attributes

Modelled on this page

  • Subject Specialist: Technical distinctions between reactive steering, finite state machines and goal-oriented planning are drawn with precise terminology.
  • Critical Thinker: The limitations of hand-wired logic are weighed against the complexity of automated planning to justify a design choice.
  • Globally Engaged: Concepts from academic AI research are connected to practical applications in game development and software engineering.

University of Reading Graduate Attributes framework.