GOAP System: Vikings vs Tiger
1. Introduction and Project Justification
1.1. Summary
This project is about an AI system I built using the GOAP (Goal-Oriented Action Planning) architecture in Unity 6. The main idea is to simulate a small autonomous world set in a Viking settlement, where three agents (a Lumberjack, a Farmer and a Warrior) have to cooperate and manage shared resources to survive against a dynamic threat, which in this case is a tiger.
The tiger is a normal Finite State Machine (FSM), where the transitions are fixed and hardcoded, this system lets each agent plan its own sequence of actions in real time. The planner (GOAPPlanner) builds a graph of possible states step by step, based on the actions available and their preconditions/effects, and then picks the cheapest plan that reaches the goal.
1.2. Architecture
I went with GOAP because it lets me handle a lot of decision-making complexity without the code turning into a mess. In this scenario, the agents depend a lot on each other:
- The Lumberjack or the Farmer can’t work safely if the tiger is around.
- The Warrior can’t patrol forever either — his stamina goes down over time and with combat, so at some point he has to switch to the Eat action instead.
Every agent has its own set of actions (all inheriting from the abstract class GOAPAction), and they all share the same planner, which connects these actions dynamically based on their Preconditions and Effects (stored as Dictionary<string,int>).
1.3. Scenario description
The simulation happens in a 3D Viking-style settlement with these characters:
- The Warrior (protector agent): his main job is to patrol safe areas and take down the tiger. He has his own Stamina (float), which goes down over time and during combat, and a Health (float) stat that’s separate from the generic hp system used by the other agents (GAgent).

- The Farmer (food agent): in charge of the food supply. As soon as he spots the tiger, he drops everything and runs straight home.

- The Lumberjack (resource agent): gathers wood. The project includes a «call for help» action (CallForHelpAction) that, if it’s attached to this agent, turns on the global helpRequested flag and warns the Warrior.

- The Tiger (dynamic threat): built as a Finite State Machine, not as a GOAP agent. Basically, the tiger walks on the scenario to random points and when It see a person he tries to kill him, and, by default, it retreats because of low HP, going to a safe zone defined in the Inspector.

2. World State (Data Architecture)
The global world state lives in a static class called GOAPWorld, stored as a Dictionary<string,int> that every agent shares:
- food (int): food stored in the warehouse. GameManager sets the starting value in Start().
- wood (int): wood stored in the warehouse. Same thing, set in Start().
- tigerSpotted (int, 0/1): flag for when the tiger has been spotted.
- helpRequested (int, 0/1): flag for when someone called for help.
The tiger’s position isn’t saved inside that dictionary, it’s a separate field: GOAPWorld.HelpPosition (Vector3), updated by CallForHelpAction. On top of that, each agent keeps its own local beliefs (also a Dictionary<string,int>), like tigerNear, woodChopped, foodHarvested or isExhausted, which get merged with the global world state every time it plans.

3. Agents
A. The Lumberjack
Goals set in Start(): isSafe (priority 200) and woodDelivered (priority 100). Beliefs updated every frame in Update(): tigerNear (turns on if tigerSpotted or helpRequested are 1 in the global world state) and woodChopped (turns on if he’s carrying more than 0 wood).
| GOAP Action | Precondition | Effect | Cost | Priority | Technical note |
| Chop Tree | — (real gate: inventory < 150) | woodChopped = 1 | 4 | 100 (goal: woodDelivered) | Goes to the closest available tree; keeps going automatically until the inventory is full (+25 per tree). |
| Deliver Wood | woodChopped = 1 | woodDelivered = 1 | 2 | 100 (woodDelivered) | Goes to the drop-off point and empties the inventory into GOAPWorld.wood. |
| Hide / Call for Help | tigerNear = 1 | isSafe = 1 | 1 | 200 (isSafe) | Goes home and runs HideInHouse(). The project also has other versions (FleeAction / CallForHelpAction) to either run straight home or warn the Warrior first. |
B. The Farmer
Goals set in Start(): isSafe (priority 200) and foodSupplied (priority 100). Beliefs: tigerNear (same as the Lumberjack) and foodHarvested (turns on if he’s carrying food).
| GOAP Action | Precondition | Effect | Cost | Priority | Technical note |
| Harvest | — (real gate: inventory < 100) | foodHarvested = 1 | 3 | 100 (goal: foodSupplied) | Goes to the closest available Farm node; keeps going until full (+10 per node). |
| Deliver Food | foodHarvested = 1 | foodSupplied = 1 | 2 | 100 (foodSupplied) | Goes to the drop-off point and empties the inventory into GOAPWorld.food. |
| Run Home | tigerNear = 1 | isSafe = 1 | 1 | 200 (isSafe) | Drops whatever he’s doing and runs straight home to hide; doesn’t call for help. |
C. The Warrior
Goals set in Start(): killTiger (200, top priority), hasEnergy (100) and isPatrolling (50). His own stats: stamina and health (both float), separate from the generic hp system in GAgent. One thing worth mentioning: there’s no actual GOAP action for «responding to a call» — the tigerNear belief gets added directly inside Warrior.Update() whenever tigerSpotted or helpRequested are active in the global world state, without going through the planner at all.
| GOAP Action | Precondition | Effect | Cost | Priority | Technical note |
| Patrol | — | isPatrolling = 1 | 1 | 50 (isPatrolling) | Picks a random waypoint; keeps repeating because the isPatrolling belief never sticks around. |
| Attack Tiger | tigerNear = 1 | killTiger = 1 | 2 | 200 (killTiger, top priority) | Chases and hits the tiger every 1.5s (40 damage); stays active until the tiger dies or runs off. |
| Eat | isExhausted = 1 (turns on when stamina hits 0) | hasEnergy = 1 | 1 | 100 (hasEnergy) | Uses up 10 global food and starts a gradual health/stamina regen. |
D. The Tiger (TigerFSM)
Built as a Finite State Machine (not GOAP), with three states: Roaming, Attacking and Recovering.
- Roaming: wanders between waypoints; if it spots (15m radius) a villager or the Warrior, it switches to Attacking.
- Attacking: chases and hits (15 damage every 2s) both villagers and the Warrior. It doesn’t run from the Warrior at all — it just treats him like any other target.
- Recovering: only kicks in if HP ≤ fleeHpThreshold. That threshold is forced to 20 by default («the tiger fights near the death»), so in practice it retreats because of HP and go to the safeZoneTarget in the Inspector.
- When HP hits 0 it gets disabled, fires the OnTigerDied event, and respawns 20s later at full health, back at its spawn spot.