Goal-Oriented Action Planning for Swift. Pure Swift, zero dependencies, cross-platform, Sendable-ready.
GOAP (Goal-Oriented Action Planning) is an AI planning technique introduced by Jeff Orkin for F.E.A.R. You describe the world as a set of facts, your agent's goals as facts that should become true, and your agent's actions as preconditions plus effects. The planner then runs A* search over world states to find the cheapest sequence of actions that takes the agent from "now" to "goal" — automatically, at runtime.
That means you can change the world, add new actions, or shift the agent's goals, and behaviour adapts without rewriting branching logic. It is the antithesis of a hand-authored finite-state machine.
- Pure Swift, zero dependencies. No UIKit, SpriteKit, GameplayKit, or Foundation.
- Cross-platform. Works on iOS, macOS, tvOS, watchOS, and Linux.
- Sendable everywhere. All public types conform to
Sendable— ready for Swift 6 strict concurrency. - Two world-state representations.
BooleanWorldState— 64 boolean facts in a singleUInt64. O(1) compares, O(1) updates, ideal for performance-critical agents.RichWorldState—[String: StateValue]with bool / int / double / string values, supporting numeric conditions (>=,<, etc.) and effects (add,subtract).
- A* planner with closed-set optimisation, optimal-cost search, and a configurable expansion cap.
GOAPPlanvalue type carrying the action sequence, total cost, and the full trajectory of intermediate states.- Type-safe facts.
BooleanWorldStateaccepts anyRawRepresentablewhoseRawValueisInt, so you can use enums instead of magic indices. - Multi-goal support with
.priority(first-achievable) or.maxUtility(bestpriority - cost) selection strategies. - Dynamic action cost. Override
cost(in: State)to let action weight depend on the world ("travel to X" varies by distance). - Dynamic action sets. Pass
actionsFor: (State) -> [Action]instead of a static array — generate parameterised actions ("go to room R") at planning time.
Add to your Package.swift:
.package(url: "https://github.com/ultrainfinity/SwiftGOAP.git", from: "0.1.0")Then depend on the SwiftGOAP product:
.target(name: "MyGame", dependencies: ["SwiftGOAP"])A simple agent that picks up a gun, loads it, and shoots an enemy. Facts are an enum, so you never deal with raw bit indices.
import SwiftGOAP
enum Fact: Int { case hasGun, gunLoaded, enemyDead }
let pickupGun = BasicAction<BooleanWorldState>(
name: "pickupGun",
preconditions: BooleanWorldState.facts([(Fact.hasGun, false)]),
effects: BooleanWorldState.facts([(Fact.hasGun, true)])
)
let loadGun = BasicAction<BooleanWorldState>(
name: "loadGun",
preconditions: BooleanWorldState.facts([(Fact.hasGun, true), (Fact.gunLoaded, false)]),
effects: BooleanWorldState.facts([(Fact.gunLoaded, true)])
)
let shootEnemy = BasicAction<BooleanWorldState>(
name: "shootEnemy",
preconditions: BooleanWorldState.facts([(Fact.hasGun, true), (Fact.gunLoaded, true)]),
effects: BooleanWorldState.facts([(Fact.gunLoaded, false), (Fact.enemyDead, true)])
)
let start = BooleanWorldState.facts([
(Fact.hasGun, false), (Fact.gunLoaded, false), (Fact.enemyDead, false)
])
let goal = BooleanWorldState.facts([(Fact.enemyDead, true)])
let planner = GOAPPlanner<BooleanWorldState>()
let plan = planner.plan(from: start, goal: goal, actions: [pickupGun, loadGun, shootEnemy])
// plan?.actions.map(\.name) == ["pickupGun", "loadGun", "shootEnemy"]
// plan?.totalCost == 3
// plan?.states.count == 4 // start + 3 intermediate statesAdd a repairGun action and the planner uses it whenever it's the cheapest path. Remove pickupGun and the planner returns nil instead of a stale FSM transition.
Heal until full, using a finite supply of potions:
let drinkPotion = BasicAction<RichWorldState>(
name: "drinkPotion",
cost: 1,
preconditions: ["potions": .greaterThan(0)],
effects: ["health": .add(30), "potions": .subtract(1)]
)
let start = RichWorldState(["health": 20, "potions": 5])
let goal: [String: StateCondition] = ["health": .greaterThanOrEqual(100)]
let planner = GOAPPlanner<RichWorldState>()
let plan = planner.plan(from: start, goal: goal, actions: [drinkPotion])
// plan?.count == 3 // (20 → 50 → 80 → 110)
// plan?.totalCost == 3StateValue conforms to the literal protocols, so you can write ["health": 20] instead of ["health": .integer(20)]. Conditions support equals, notEquals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual. Effects support set, add, subtract.
Note:
.add/.subtractrequire an existing numeric value (.integeror.real). Applying them to a.bool/.textfact, or to a missing key, traps with a precondition failure — catch this in development, not in production. Use.set(.integer(0))first to initialise a counter.
Give the agent a list of goals; the planner returns the highest-priority one it can reach, together with the matching plan.
enum Survival: Int { case fed, armed, enemyDead }
let killEnemy = GOAPGoal<BooleanWorldState>(
name: "killEnemy",
conditions: BooleanWorldState.facts([(Survival.enemyDead, true)]),
priority: 10
)
let eatFood = GOAPGoal<BooleanWorldState>(
name: "eatFood",
conditions: BooleanWorldState.facts([(Survival.fed, true)]),
priority: 1
)
if let result = planner.plan(from: start, goals: [killEnemy, eatFood], actions: actions) {
print("pursuing \(result.goal.name): \(result.plan.actions.map(\.name))")
}Default behaviour: try goals in descending priority order and return the first that has a plan. Pass selectingBy: .maxUtility to instead pick the goal that maximises priority - plan.totalCost — useful when an expensive high-priority goal should defer to a cheap low-priority one:
let result = planner.plan(
from: start,
goals: [killEnemy, eatFood],
actions: actions,
selectingBy: .maxUtility
)BasicAction is a convenience. Anything that conforms to GOAPAction works — you can attach domain-specific behaviour (animation hooks, audio cues, callbacks) directly to your action type. Conform to Sendable if you plan to cross actor boundaries.
struct CombatAction: GOAPAction {
let name: String
let cost: Int
let preconditions: BooleanWorldState
let effects: BooleanWorldState
let animation: String
let perform: @Sendable () -> Void
}The planner returns GOAPPlan<CombatAction>, so you keep all that data through to execution.
The planner returns the action sequence; running it is up to you.
guard let plan = planner.plan(from: currentWorldState, goal: goal, actions: actions) else {
return // no plan exists right now
}
var state = currentWorldState
for action in plan.actions {
guard state.satisfies(action.preconditions) else { break } // world drifted, re-plan
perform(action)
state = state.applying(action.effects)
}You can also use plan.states directly — it contains the start state, every intermediate state, and the goal-satisfying final state. Handy for visualisation and debugging.
When the world drifts (a door closes, ammo is taken), drop the plan and re-plan from the new state. Re-planning is cheap.
BooleanWorldStateoperations are single 64-bit ALU ops. Plans of 5–10 actions over a dozen action types resolve in tens of microseconds.- The A* loop uses a closed set with reopen-on-better-g, so stale frontier entries are discarded immediately instead of re-expanded.
- The
GOAPPlanner.maxNodescap (default 10,000) protects against runaway searches if your action set has bad cycles or your heuristic is too weak. - The default heuristic counts unsatisfied facts. It is fast and produces sensible plans, but is not strictly admissible when one action satisfies multiple facts at once. In rare cases the returned plan may be one action longer than truly optimal — the standard GOAP trade-off.
There are GOAP implementations in C (stolk/GPGOAP), C++, C# (mountain-goap), Go, Rust, and Python — but until now, none in Swift. This package fills that gap with an idiomatic, dependency-free, Sendable-ready Swift API that works in games, simulations, and any other agent-based system.
MIT. See LICENSE.
- Jeff Orkin, Three States and a Plan: The A.I. of F.E.A.R. — the original GOAP paper.
- stolk/GPGOAP — minimal C reference.
- caesuric/mountain-goap — comprehensive C# implementation.