Custom GOAP Implementation: Because why settle for easy when you can have more fun?

Custom GOAP Implementation: Because why settle for easy when you can have more fun?

Before we begin, let’s set the scene: there’s already a perfectly good (and free!) GOAP (Goal-Oriented Action Planning) implementation available here: https://github.com/crashkonijn/GOAP. But in a glorious fit of curiosity (or was it recklessness?), I decided to build my own system from scratch. Why? To truly grasp how it works, and because I might have downed a bit too much coffee that day.

The result? A system broken down into simple, modular components that are (fingers crossed) easy to debug. Let’s dive into this homemade GOAP approach, tailor-made to blend seamlessly with our Refuge universe.

GOAP: The Theory, Plan Smart, or Crash and Burn

GOAP (Goal-Oriented Action Planning) is a clever method of giving AI some real autonomy without coding a rigid step-by-step sequence. The idea? Let the AI dynamically choose which actions to perform to reach a given objective, based on the current state of the world.

A GOAP system typically stands on four main pillars:

  1. World state: A set of facts (booleans, values, etc.) describing the environment.
  2. Goals: Desired final states (e.g., “Have enough water”).
  3. Actions: Each has prerequisites (what must be true to do it) and effects (what changes in the world as a result).
  4. The planner: A mastermind that figures out the optimal route (sequence of actions) to achieve a goal.

This model allows for adaptive AI that can react to changes. If a resource disappears or an action fails, the agent can recalculate a new plan, no need to rewrite everything from scratch. Magical? Not exactly. But it sure does feel that way.

Overview of the GOAP Implementation in Refuge

Our custom GOAP setup in Refuge is arranged in a modular structure with the following components:

  • GoapAgent: The agent’s brain, coordinating planning and execution.
  • Goal: A clear intention with a priority level.
  • Action: An operation the agent can perform. It has preconditions, effects, a duration, and a cost.
  • GoapBrain: The planner in charge of figuring out the optimal plan.
  • GoapNode: A node in the planning graph.
  • GoapPlan: The final plan, expressed as a queue of actions.
  • State: A dictionary of key-value pairs describing the world state.
  • Target/TargetRequest: Representation of potential action targets.
  • Behaviors: Handle the real-world execution of an action (like moving or interacting).

We split decision-making (planning) from actual execution (behaviors), keeping the system neat, flexible, and easier to maintain.

Architecture Diagram

Class-by-Class Breakdown

Goal: The Desire That Drives

A Goal is an objective the agent aims to fulfill. In Refuge, a Goal isn’t just a vague notion like “spread sunshine and rainbows.” No, it’s a very specific world state you want (for instance, “collect food” or “reduce stress levels”).

Each Goal includes:

  • A State defining the conditions that mark the goal as achieved.
  • A priority level, because even AI can’t do everything at once.
  • An IsRelevant method to determine if this goal is worth pursuing given the current world state.

The agent checks these goals at each planning tick. If multiple goals are valid, the system picks the one with the highest priority. Think of it as the moral compass of the agent, but in C#.

(Note: In Refuge, goals are designed to be easily extended. You want a goal called “score epic loot”? Just code a new class and define the target state. Simple as that.)

Action: The Go-Getters

If Goal is the “what,” an Action is the “how.” An action is a task the agent can execute to alter the world and inch closer to its objective. But it’s not a free-for-all. Every Action is tidy, with rules and constraints.

An Action contains:

  • Preconditions: What needs to be true for it to be performable (e.g., a target must be available, the agent must be close enough, or not exhausted).
  • Effects: How this action changes the world state (e.g., after “Drink,” the agent is no longer thirsty).
  • A cost: Used in planning to favor more efficient paths.
  • An execution time (if it’s not instantaneous).
  • A TargetRequest: The kind of target this action needs (if any).

Each action also has callbacks (OnEnter, OnExit, Tick) to handle ongoing behavior. That’s where the magic happens: while running, an action can call a corresponding Behavior to move around, interact with objects, etc.

Everything is data-driven, so you can add new actions without rewriting the planner. Just define your conditions and effects, and the planner happily picks it up.

In short, an Action is the Swiss Army knife of the system. And like any good tool, it’s all about how you use it.

State: The World’s Journal

State is basically the world’s diary, or a big bucket of facts. It stores the current (or desired) state of the world using key-value pairs. In Refuge, we opted for a Dictionary<string, object> to stay flexible.

A State can hold anything: a boolean for “Has water,” an integer for resource counts, or a reference to some target. As long as it’s serializable, it’s fair game.

Key methods include:

  • Contains: Checks if one State meets the conditions of another.
  • Apply: Merges another State in, updating values.
  • Clone: Because we want to test hypothetical changes without messing up the real world.

State appears everywhere:

  • As the initial world conditions
  • In action effects
  • In goal conditions
  • In the plan-building nodes

So if Action is the muscle, and Goal the motivation, then State is the logical brain guiding the process. It’s quiet but absolutely essential.

Target / TargetRequest: The Action’s Focus

Rarely does an action happen in a cosmic vacuum. Usually, it needs a target, some resource, an object, or a location. Enter Target and TargetRequest:

  • Target wraps a position, an entity, or any point of interest the action should aim for, providing a unified interface.
  • TargetRequest expresses a need, like “I need a place to sleep” or “I’m looking for a crate of supplies.” It defines the type of target we’re seeking, any additional filters, and how we assign the resulting target.

Workflow:

  1. The action presents a TargetRequest.
  2. The system attempts to fulfill this request by handing over a Target.
  3. The action executes using that specific target.

This is a flexible, generic approach ideal for a dynamic game like Refuge, where actions might aim at all kinds of shifting objects.

In a nutshell, if State describes the world, TargetRequest asks, “So, what am I actually interacting with?”

Behaviors: Making It Happen

Behaviors are the worker bees of GOAP. Once an action is planned and approved, it needs someone to handle it, and that’s the job of Behavior.

Each action can be linked to a specific Behavior, which does the nitty-gritty:

  • Move to a location (MoveBehavior)
  • Play an animation
  • Wait some duration
  • Trigger an interaction with an object

A Behavior is all about bringing the action to life. It works hand in hand with the action, calling the right methods in Tick(), handling transitions (EnterState, ExitState), and telling the agent when it’s done.

Behaviors can also have sub-states (like MoveState) to structure their internal processes.

They’re designed to be reusable across different actions, giving you a broad, flexible toolkit without duplicating code. Basically, they’re like Unity scripts... that actually do something useful.

In short, without Behavior, your AI would stand around philosophizing. With Behavior, it’s an adventurer ready to loot, eat, run... and maybe die a heroic death.

GoapBrain: The Subtle Mastermind

GoapBrain is the actual strategy engine. Its job? Find the best route to achieve whichever Goals are in play, using the available Actions.

How does it work?

  1. It takes the world’s current State.
  2. It checks the selected Goal.
  3. It builds a graph of GoapNodes, each node reflecting a possible world state after performing a certain action.
  4. It then uses A* search to locate the shortest (or cheapest) path to the goal.

All the real planning logic lives in GoapBrain. It tests out actions, sees what’s possible, tallies costs, and eventually spits out a GoapPlan for the agent to execute.

So GoapBrain is basically the puppet master: you can’t see or hear it, but without it, your AI would wander aimlessly.

GoapNode: Stepping Stones to Success

GoapNode represents an intermediate state in the plan. Think of it as a snapshot of the world after running one or more actions.

Each GoapNode holds:

  • A State describing the world at this step.
  • A reference to the action that led here.
  • A running cost accumulated so far.
  • A pointer to the previous node for reconstructing the path later.

In practice, GoapBrain creates and evaluates these nodes as it does its A* search, exploring possibilities and tallying up costs until it finds the best plan. They’re like squares on a chessboard: you look at several, but only the best path wins.

Without them, we couldn’t track our decisions, and we’d have no plan. They’re crucial for structuring the AI’s progress toward success.

GoapPlan: The (Almost) Perfect Plan

GoapPlan is simply the final representation of the route computed by GoapBrain. Once the planner is done, it compiles a GoapPlan consisting of a sequence (or queue) of actions to be performed in order.

It includes:

  • An ordered list of Action objects the agent should execute.
  • The total cost of carrying out that plan.

Once complete, the plan is handed off to the GoapAgent, which proceeds to run each action in turn. It’s like a recipe, follow each step carefully and you’ll (probably) end up with success (though in Refuge, success usually means survival rather than a delicious cake).

The strength of GOAP is this straightforward approach: if something fails, or the world changes, you can quickly compute a new plan. So it’s pretty much the perfect plan... right up until it isn’t.

GoapAgent: The Ultimate Executor

Finally, GoapAgent is the main character in this story. It perceives the world, sets its goals, calculates a plan, and most importantly, carries it out.

Its main jobs are:

  • Perception: Obtain and keep track of the current State of the world.
  • Goal Selection: Decide which Goal is relevant given the situation.
  • Planning: Ask GoapBrain for a fresh GoapPlan whenever there’s no valid plan.
  • Execution: Perform each action in the plan, invoking the necessary Behaviors and checking results.
  • Handling Surprises: Recalculate a plan if an action fails or the world changes drastically.

GoapAgent orchestrates the entire GOAP process, acting as the conductor of all the system’s components. Without it, goals would just be wishes, plans would remain theories, and actions would never get off the ground.

In short, GoapAgent is the lifeblood of the system, breathing life into all that GOAP theory in Refuge.

Conclusion

This homemade GOAP setup in Refuge has been a delightful (and caffeinated) adventure, digging into how goal-driven AI really works. Sure, there are tried-and-true solutions out there, but sometimes you just need to roll up your sleeves and get your hands dirty.

The upshot? A modular, adaptable, and flexible system, well-suited for Refuge’s unpredictable environment. And honestly, what’s more satisfying than watching your virtual agents handle problems (mostly) on their own, thanks to a well-thought-out plan?

Ultimately, this whole experience taught me one big lesson: whether it’s in code or in a zombie-ridden wasteland, having a solid plan, and plenty of coffee, makes all the difference.


Avant de commencer, remettons les choses dans leur contexte : il existe déjà une implémentation de GOAP (Goal-Oriented Action Planning) qui fonctionne très bien et qui est même gratuite. Elle est disponible ici : https://github.com/crashkonijn/GOAP. Mais bon... dans un élan de curiosité (ou de masochisme ?), j'ai décidé de réimplémenter mon propre système. Pourquoi ? Pour comprendre comment ça marche. Et aussi parce que j'avais un peu trop de café ce jour-là.

Le résultat ? Un système décomposé en composants simples, modulaires et faciles à débugguer (si si, promis). Voici un tour d'horizon de mon GOAP maison, pensé pour s'intégrer parfaitement à l'univers de Refuge.

GOAP, côté théorie : planifier intelligemment, ou l'art de ne pas foncer dans le mur

GOAP (Goal-Oriented Action Planning), c'est une manière de faire réfléchir nos IA sans leur programmer à l'avance une suite d'actions rigide. L'idée ? Laisser l'IA choisir dynamiquement les actions à effectuer pour atteindre un objectif donné, en fonction de l'état actuel du monde.

Un système GOAP repose sur quatre grands piliers :

  1. L'état du monde : une collection de faits (booléens, valeurs, etc.) qui décrivent l'environnement actuel.
  2. Les objectifs (Goals) : des états finaux désirés (ex : « avoir de l'eau »).
  3. Les actions : chacune a des préconditions (ce qu'il faut pour qu'elle soit faisable) et des effets (ce qu'elle change dans le monde).
  4. Le planificateur : un petit malin qui réfléchit au chemin optimal (séquence d'actions) pour atteindre un objectif.

Ce modèle permet une IA adaptative, capable de s'ajuster à l'évolution du monde. Si une ressource devient indisponible ou si une action échoue, l'agent peut recalculer un nouveau plan, sans avoir besoin de tout recoder à la main. Magique ? Non. Mais diablement efficace.

Vue d'ensemble de l'implémentation GOAP dans Refuge

L'implémentation custom de GOAP dans Refuge suit une architecture modulaire composée des éléments suivants :

  • GoapAgent : le cerveau de l'agent, qui coordonne la planification et l'exécution.
  • Goal : définit une intention claire à atteindre, avec une priorité.
  • Action : une opération que l'agent peut effectuer. Elle a des préconditions, des effets, une durée et un coût.
  • GoapBrain : le planificateur, responsable de la recherche d'un plan optimal.
  • GoapNode : un nœud dans le graphe de planification.
  • GoapPlan : le plan final sous forme de file d'actions.
  • State : un dictionnaire d'état du monde sous forme de clé-valeur.
  • Target/TargetRequest : représentation de cibles d'action potentielles.
  • Behaviors : permettent l'exécution concrète d'une action (par exemple : se déplacer, interagir).

Cette architecture s'appuie sur une séparation claire entre la décision (planification) et l'exécution (behaviors), facilitant la maintenance et l'évolution du système.

Schéma d'architecture

Détail classe par classe

Goal : l’intention avant l’action

La classe Goal représente un objectif que l’agent souhaite atteindre. Dans Refuge, un Goal n’est pas une simple idée vague du style « faire le bien autour de soi », non non. C’est un état clairement défini du monde que l’on veut réaliser (par exemple : « avoir de la nourriture », ou « réduire le niveau de stress »).

Chaque Goal est composé de :

  • Un State qui définit les conditions à atteindre pour que le goal soit considéré comme accompli.
  • Une priorité, parce qu’on ne peut pas tout faire en même temps (même les IA doivent prioriser !).
  • Une méthode IsRelevant qui permet à l’agent de savoir si ce goal mérite d’être poursuivi dans l’état actuel du monde.

Côté architecture, le Goal est consulté par l’agent à chaque tick de planification. Si plusieurs goals sont valides, le système choisit celui avec la plus haute priorité. Bref, c’est un peu la boussole morale de l’agent, mais en version C#.

(À noter : dans Refuge, les goals sont conçus pour être facilement extensibles. Tu veux un goal « collecter du loot épique » ? T’as juste à coder une nouvelle classe et définir l’état cible. Facile comme bonjour.)

Action : les petits soldats du plan

Si Goal est le « quoi », alors Action est le « comment ». Une Action, c’est une tâche que l’agent peut exécuter pour transformer le monde et s’approcher de son objectif. Mais attention : ici, on ne parle pas d’actions en mode YOLO. Non, chaque Action est une entité bien rangée, encadrée, avec règles et conditions.

Une Action contient :

  • Des préconditions : ce qui doit être vrai pour qu’on puisse l’effectuer (ex : avoir une cible, être proche de quelque chose, ne pas être fatigué).
  • Des effets : ce que l’action modifie dans l’état du monde (ex : après « Boire », l’agent n’a plus soif).
  • Un coût : utilisé pendant la planification pour privilégier les plans plus efficaces.
  • Un temps d’exécution (si l’action n’est pas instantanée).
  • Un TargetRequest : la cible que cette action nécessite pour s’exécuter correctement (optionnelle).

Chaque action dispose aussi de callbacks (OnEnter, OnExit, Tick) pour gérer l’exécution dans le temps. C’est là que la magie opère : pendant l’exécution, l’action peut faire appel à un Behavior pour animer un déplacement, interagir avec un objet, etc.

Le tout est encapsulé de manière à ce que les actions soient data-driven : on peut en ajouter de nouvelles sans toucher au planificateur. Ajoute une classe, définis les préconditions et effets, et voilà ! Le planificateur saura s’en servir.

Bref, Action, c’est le couteau suisse du système. Et comme tout bon outil, il faut juste bien savoir s’en servir.

State : la mémoire du monde

La classe State, c’est un peu le journal intime du monde — ou plutôt une grosse table de vérité. Elle stocke l’état actuel du monde (ou l’état désiré pour un objectif) sous forme de paires clé/valeur. Dans Refuge, on a choisi de représenter cela avec un Dictionary<string, object>, histoire de rester souple.

Un State peut contenir n’importe quoi : un booléen qui dit si on a de l’eau, un entier qui indique le nombre de ressources, ou une référence vers une cible. Tant que c’est sérialisable, c’est stockable.

Les fonctions clés de cette classe :

  • Contains : pour vérifier si un sous-ensemble d’un autre State est satisfait.
  • Apply : pour fusionner un autre State et mettre à jour les valeurs.
  • Clone : parce qu’on veut pouvoir tester des hypothèses sans casser le vrai monde.

State est utilisé partout :

  • Dans les conditions initiales du monde
  • Dans les effets d’action
  • Dans les conditions de goal
  • Dans la construction des noeuds du planificateur

En résumé, si Action est le muscle et Goal la motivation, alors State est le cerveau rationnel qui suit la logique. Il ne fait pas de bruit, mais sans lui, rien ne fonctionne.

Target / TargetRequest : les yeux de l’action

Une Action ne se fait pas toujours dans le vide intersidéral. Souvent, elle nécessite une cible : une ressource, un objet, un endroit... Bref, quelque chose à viser. C’est là qu’interviennent Target et TargetRequest.

  • La classe Target encapsule une position, une entité, ou n’importe quel point d’intérêt qu’une action pourrait viser. C’est une sorte de wrapper générique qui permet de manipuler des cibles de manière uniforme.
  • TargetRequest, lui, représente un besoin : « j’ai besoin d’un endroit où dormir », ou « je cherche une caisse de vivres ». Il définit le type de cible recherchée, les filtres éventuels, et l’assignation de la cible si une correspondance est trouvée.

Le workflow est le suivant :

  1. L’action exprime un TargetRequest.
  2. Le système essaie de satisfaire cette demande en lui fournissant un Target.
  3. L’action peut alors s’exécuter avec cette cible concrète.

C’est un système souple et générique, parfait pour un jeu comme Refuge où les actions peuvent viser toutes sortes d’éléments dynamiques.

En somme, si State décrit le monde, TargetRequest pose la question : « Et moi, je tape sur quoi ? »

Behaviors : faire, bouger, vivre

Les Behaviors, ce sont les petites mains du système GOAP. Une fois qu’une action est planifiée et validée, il faut bien quelqu’un pour l’exécuter — et ce quelqu’un, c’est le Behavior.

Chaque action peut être associée à un Behavior spécifique, qui se charge de l’exécution concrète :

  • Se déplacer vers une cible (MoveBehavior)
  • Jouer une animation
  • Attendre un certain temps
  • Déclencher une interaction avec un objet

Le rôle du Behavior, c’est donc d’apporter l’aspect « moteur » du système. Il travaille main dans la main avec l’action, en appelant les bonnes méthodes dans Tick(), en gérant les transitions (EnterState, ExitState), et en notifiant l’agent lorsque c’est terminé.

Les Behaviors peuvent également posséder leurs propres sous-états (MoveState, par exemple), afin de mieux structurer l’enchaînement d’étapes internes.

Ils sont conçus pour être réutilisables entre différentes actions, permettant une grande souplesse sans dupliquer le code. En bref, c’est un peu comme des scripts Unity... mais qui font des trucs utiles.

Conclusion : sans Behavior, ton IA est un philosophe immobile. Avec Behavior, elle devient un aventurier prêt à looter, manger, courir... et mourir de manière glorieuse.

GoapBrain : le stratège silencieux

Le GoapBrain, c’est le véritable planificateur stratégique du système. Sa mission ? Trouver le meilleur chemin pour atteindre les objectifs définis par les Goals en utilisant les Actions disponibles.

Comment ça marche en pratique ?

  1. Le cerveau récupère l’état actuel du monde (State initial).
  2. Il prend le Goal sélectionné par l’agent.
  3. Il construit progressivement un graphe composé de GoapNodes, chaque nœud représentant un état intermédiaire après une action spécifique.
  4. Il utilise une recherche A* pour trouver le chemin le plus court (ou le moins coûteux) vers l'objectif.

C’est dans le GoapBrain que réside la logique pure de planification. Il teste les différentes actions disponibles, explore les possibilités, évalue les coûts, et finit par proposer un GoapPlan qui sera exécuté par l’agent.

Bref, le GoapBrain, c’est l’éminence grise : tu ne le vois pas, tu ne l’entends pas, mais sans lui, ton IA tournerait en rond sans fin.

GoapNode : les étapes vers la victoire

Le GoapNode représente un état intermédiaire dans la planification. Chaque nœud est une « photographie » du monde à un instant précis après l’exécution d’une ou plusieurs actions.

Chaque GoapNode contient :

  • Un State (l'état du monde à ce stade).
  • Une référence vers l’action qui a permis d’atteindre ce node.
  • Un coût cumulé depuis le début du plan.
  • Un lien vers le nœud précédent pour reconstruire le chemin complet à la fin.

En pratique, le GoapBrain crée et évalue ces nœuds pendant la recherche A*, explorant les chemins possibles et calculant les coûts jusqu’à trouver le plan optimal. En gros, les GoapNodes, c’est comme les cases sur un échiquier : on en explore plusieurs, mais seul le meilleur chemin est retenu.

Sans eux, pas de mémoire des choix faits, et donc pas de plan. Ils sont essentiels pour structurer la réflexion de l’IA vers la victoire.

GoapPlan : le plan parfait (ou presque)

Le GoapPlan est simplement la représentation finale du chemin calculé par le GoapBrain. Une fois que le planificateur a terminé ses calculs, il construit un GoapPlan composé d'une file d’actions à exécuter dans l’ordre.

Ce plan contient :

  • Une liste ordonnée d’actions (Action) à réaliser par l’agent.
  • Le coût total accumulé pour réaliser le plan.

Une fois établi, le plan est remis à l’agent (GoapAgent) qui se charge de son exécution, action par action. C’est un peu comme une recette de cuisine : chaque étape doit être suivie scrupuleusement pour obtenir le résultat voulu (sauf que là, au lieu d'un gâteau, on obtient généralement de la survie ou des ressources).

La robustesse de GOAP réside dans cette simplicité apparente : si quelque chose échoue ou si le monde change, le plan peut être facilement recalculé. C’est donc un plan parfait... du moins jusqu’à ce que quelque chose se mette à mal tourner.

GoapAgent : l'exécutant ultime

Enfin, le GoapAgent est l’acteur principal sur la scène. Il représente l’entité capable de percevoir le monde, définir ses objectifs, calculer un plan, et surtout, exécuter ce plan.

Voici ses principales responsabilités :

  • Perception : Obtenir et maintenir à jour l’état actuel du monde (State).
  • Sélection d'objectif : Déterminer quel Goal est pertinent en fonction de l’état actuel.
  • Planification : Demander au GoapBrain un nouveau GoapPlan lorsqu’il n’a plus de plan valide.
  • Exécution : Exécuter les actions du plan une par une, en invoquant les Behaviors associés et en surveillant les résultats.
  • Gestion des imprévus : Réagir et recalculer un plan si une action échoue ou si le monde change significativement.

Le GoapAgent orchestre ainsi tout le processus GOAP, servant de chef d’orchestre aux différentes composantes du système. Sans lui, les objectifs ne seraient que des souhaits, les plans resteraient des hypothèses, et les actions ne seraient jamais plus que des intentions.

En bref, GoapAgent est le cœur vivant du système, donnant vie à toute la mécanique théorique de GOAP dans Refuge.

Conclusion

Cette implémentation maison de GOAP dans Refuge a été une aventure passionnante qui a permis d'approfondir la compréhension des mécanismes de l'intelligence artificielle orientée objectifs. Bien sûr, il existe des solutions déjà éprouvées et fonctionnelles, mais parfois, il faut mettre les mains dans le cambouis pour vraiment saisir les subtilités d'un concept.

Le résultat ? Un système modulaire, évolutif et flexible, parfaitement adapté au contexte dynamique et imprévisible de Refuge. Et puis, honnêtement, quoi de plus satisfaisant que de voir ses agents virtuels se débrouiller tout seuls (enfin presque) grâce à un plan bien pensé ?

Finalement, cette expérience a démontré une chose essentielle : en programmation comme en survie post-apocalyptique, l'important, c'est d'avoir un bon plan... et du café.