// GDnD wiki example // Demonstrates: a GOAP agent that plans, executes one action at a time, and replans when a precondition breaks // Related pages: [[goal-oriented-action-planning]], [[ai-state-machine-pattern]], [[behaviour-trees-vs-goap]] using System.Collections.Generic; using UnityEngine; // A woodcutter whose only goal is "WoodDelivered". It is never told HOW: it is // given four actions and the planner chains them. Costs make the axe route // (2 + 4 + 1 = 7) cheaper than gathering branches by hand (8 + 1 = 9), so the // worker fetches the axe first, unless the axe is gone. // Try it: in Play mode, open the WorkerAgent component's three-dot menu and choose: "Break Axe", "Restock Axe Rack", "Drop Wood". public class WorkerAgent : MonoBehaviour { [SerializeField] private Transform axeRack; [SerializeField] private Transform tree; [SerializeField] private Transform branchPile; [SerializeField] private Transform storehouse; [SerializeField] private float moveSpeed = 3f; [Header("Read-only, for watching in the Inspector")] [SerializeField] private string currentPlan; [SerializeField] private string currentFacts; [SerializeField] private int woodDelivered; private readonly WorldState world = new(); private readonly Dictionary goal = new() { ["WoodDelivered"] = true }; private List actions; private List plan; private int step; private float workTimer; private float retryTimer; private void Awake() { world.Set("AxeOnRack", true); actions = new List { new GoapAction("GetAxe", 2f, axeRack, 0.5f) .Needs("AxeOnRack", true).Needs("HasAxe", false) .Gives("HasAxe", true).Gives("AxeOnRack", false), new GoapAction("ChopWood", 4f, tree, 2f) .Needs("HasAxe", true).Needs("HasWood", false) .Gives("HasWood", true), new GoapAction("GatherBranches", 8f, branchPile, 4f) .Needs("HasWood", false) .Gives("HasWood", true), new GoapAction("DeliverWood", 1f, storehouse, 0.5f) .Needs("HasWood", true) .Gives("HasWood", false).Gives("WoodDelivered", true), }; } private void Update() { currentFacts = world.ToString(); if (plan == null) { retryTimer -= Time.deltaTime; if (retryTimer <= 0f) MakePlan(); return; } GoapAction action = plan[step]; // The heart of replanning: the world may have changed since we planned. if (!world.Satisfies(action.Preconditions)) { Debug.Log($"{name}: '{action}' is no longer possible {world}. Replanning."); plan = null; return; } // Execute: walk to the action's target, then work there for its duration. Vector3 goalPos = action.Target.position; transform.position = Vector3.MoveTowards(transform.position, goalPos, moveSpeed * Time.deltaTime); if (Vector3.Distance(transform.position, goalPos) > 0.05f) return; workTimer += Time.deltaTime; if (workTimer < action.Duration) return; // Done: only now do the action's effects become true in the real world. workTimer = 0f; world.CopyFrom(action.ApplyTo(world)); step++; if (step >= plan.Count) { if (world.Satisfies(goal)) { woodDelivered++; world.Set("WoodDelivered", false); // goal reached: want more wood } plan = null; } } private void MakePlan() { plan = GoapPlanner.Plan(world, goal, actions); step = 0; workTimer = 0f; if (plan == null) { currentPlan = "(no plan)"; retryTimer = 1f; // nothing works right now; try again in a second Debug.Log($"{name}: no plan from {world}"); return; } currentPlan = string.Join(" > ", plan); Debug.Log($"{name}: new plan {currentPlan}"); } // Disturb the world from the Inspector to watch the worker cope. [ContextMenu("Break Axe")] private void BreakAxe() => world.Set("HasAxe", false); [ContextMenu("Restock Axe Rack")] private void RestockAxe() => world.Set("AxeOnRack", true); [ContextMenu("Drop Wood")] private void DropWood() => world.Set("HasWood", false); }