Minimal GOAP woodcutter (Unity / C#)

A small, readable Goal-Oriented Action Planning example for goal-oriented-action-planning. A worker has one goal, WoodDelivered, and four actions. Nobody scripts the order: an A* planner chains the actions from the current world state to the goal, the worker carries out the plan one action at a time, and it replans as soon as the next action’s preconditions stop holding.

About 265 lines of plain C#. No packages, no NavMesh, no input code.

Files

ScriptResponsibilityKey wiki concept
WorldState.csNamed bool facts (a missing fact counts as false), goal checks, a state keygoal-oriented-action-planning, csharp-collections
GoapAction.csName, cost, preconditions, effects; plus the target and duration used when executinggoal-oriented-action-planning
GoapPlanner.csA* over world states: nodes are states, edges are actions, g = cost, h = unmet goal factspathfinding-algorithms
WorkerAgent.csDefines the actions, asks for a plan, walks to each target, applies effects, replansai-state-machine-pattern, behaviour-trees-vs-goap

The actions

ActionCostNeedsGives
GetAxe2AxeOnRack, not HasAxeHasAxe, not AxeOnRack
ChopWood4HasAxe, not HasWoodHasWood
GatherBranches8not HasWoodHasWood
DeliverWood1HasWoodWoodDelivered, not HasWood

From the start state {AxeOnRack} the planner can reach the goal two ways: GetAxe > ChopWood > DeliverWood (cost 7) or GatherBranches > DeliverWood (cost 9). It picks the cheaper one. Once the worker is holding the axe, later trips are just ChopWood > DeliverWood (cost 5).

Scene setup (about 5 minutes)

  1. New scene (2D or 3D; the worker only moves its Transform).
  2. Create four visible markers, e.g. GameObject → 2D Object → Sprites → Square (or 3D cubes), and name them AxeRack, Tree, BranchPile, Storehouse. Spread them a few units apart and give each a different colour.
  3. Create one more sprite or capsule named Worker and add WorkerAgent.
  4. Drag the four markers into the Axe Rack, Tree, Branch Pile and Storehouse fields.
  5. Press Play and open the Console. The worker logs each new plan, and the Inspector shows Current Plan, Current Facts and Wood Delivered.

What to try

All three disturbances are on the WorkerAgent component’s three-dot (⋮) menu in the Inspector, while in Play mode.

  1. Break Axe while the worker is walking to the tree. ChopWood needs HasAxe, so the worker logs “no longer possible”, replans, finds the rack empty, and falls back to GatherBranches.
  2. Restock Axe Rack, then wait for the next delivery. The next plan uses the axe again, because it is cheaper. Notice the worker does not switch mid-plan: it only replans when a plan finishes or fails.
  3. Drop Wood while the worker is carrying wood to the storehouse. DeliverWood fails, and the worker goes back for more.
  4. Change a cost in WorkerAgent.Awake (for example make GatherBranches cost 3) and predict the first plan before pressing Play.

How it maps onto the theory

  • Planning is search. GoapPlanner is the same A* idea as pathfinding, but each node is a world state and each edge is an action whose preconditions hold. The heuristic counts unmet goal facts; with a one-fact goal and every action costing at least 1, it never overestimates, so the first goal state taken off the open list is a cheapest plan.
  • Effects are only promises while planning. The planner applies effects to copies of the world state. In WorkerAgent, an effect becomes real only after the worker has walked to the target and worked there for the action’s duration.
  • Replanning is cheap insurance. Before every frame of an action the agent re-checks that action’s preconditions against the live world state.
  • Planner and movement stay separate. The planner decides what to do; Vector3.MoveTowards (swap in a NavMeshAgent or steering-behaviours) decides how to get there.

Extensions

  1. A soldier instead of a woodcutter. Facts HasWeapon, WeaponLoaded, TargetDead; actions PickUpWeapon, Reload, Attack, plus a costly Melee fallback. Reuse WorldState, GoapAction and GoapPlanner unchanged.
  2. Several goals. Add a Rested goal with a Sleep action and pick the goal each time you plan by a priority or a utility score (see utility-ai).
  3. Procedural preconditions. Give GoapAction a Func<bool> check that the planner also calls, e.g. “is the tree still standing?”, for facts too expensive or too numerous to keep in the world state.
  4. Replan on opportunity. Replan every few seconds, not only on failure, and switch if the new plan is cheaper than the rest of the current one.
  5. Faster open list. Replace the linear scan with a small binary heap and measure with more actions. (.NET 6’s PriorityQueue is not in the class libraries that ship with Unity 6000.3, so write your own or use a package.)

Background

The approach follows Jeff Orkin’s account of the planner in F.E.A.R. (Orkin, “Three States and a Plan: The A.I. of F.E.A.R.”, GDC 2006): STRIPS-style goals and actions, a cost per action, A* searching over world states, and replanning when the world invalidates a plan. This example is a teaching simplification, not a reconstruction of that system.

Source files

4 items under this folder.