GOAP in Space
By Adal
The original ideas were posted on /r/gamedev
here.
This post illustrates the revisions I have made to my GOAP implementation since that post, and as a sort of Sine Fine
dev-blog slash diary for my future self.
In Sine Fine, you play the role of the eponymous Sine Fine, the supercomputer tasked with finding a new home for humanity, while what is left of it waits in a frozen vault somewhere in the Solar System, for a second chance.
You will build and control interstellar probes and ships, but due to the central theme of the game being based around the idea of ships not travelling faster than light but instead at slower than light speeds, some of these ships will need to be automated. The second reason is that I have been a big fan of Distant World, another pillar of the genre, and I was truly in awe of all the many ship seemingly going on their own around the galaxy that I wanted something like that for my own game. In a distant past, I actually taught a course about Game AI. I hear GOAP is still trendy, so that’s what I set out to do.
In short, my GOAP implementation had to answer the following requirements:
- Be suited for a space game (no trees to cut in space, as per the classic examples).
- Be dynamic: ships will determine which action to take dynamically, based on the knowledge they have at that moment. For example, a ship might want to collect some resources from a nearby site. But where is it and how to reach it?
- Be as much as possible separable from the rest of the game, so that it might live on as a standalone library that others might want to use.
Structure
This the structure I have settled on:
- Goals: these are objectives a ship might have, like collecting some resources or bringing it back.
PropertyState
is a wrapper around aDictionary<string, object>
that allows setting and unsetting values (the Blackboard in other implementations). Here are the non-generic interfaces, for simplicity.
public interface IGoal
{
string Name { get; }
IEnumerable<ICondition> Conditions { get; }
bool Evaluate(PropertyState state);
}
- Conditions: indicate when the agent knows the goal has been met.
public interface ICondition
{
string Name { get; }
bool Evaluate(PropertyState state);
/// <summary>
/// A concept in the game world
/// </summary>
IConcept SourceConcept { get; }
/// <summary>
/// A value to be found in <c>SourceConcept</c> that it should have for the condition to be satisfied.
/// </summary>
object Required { get; }
object ActualValue { get; }
}
IConcepts
or “concepts” in general, are the new addition to the system. I thought that using only base types, like strings, float, would have made for very messy code. So I thought the overcomplicated approach and used this Concept system to indicate a sort of abstract idea of a concept in the game world. Like a resource site, or resource vault (these are the only two things that the agent knows of at the moment!).
public interface IConcept : IKey
{
// IKey provides a
// string Key {get;}
object Actual(PropertyState state);
}
- Then the
Agent
has a set of sensors and capabilities.
public interface IAgent
{
string Name { get; }
PropertyState State { get; }
ISensor<T> GetSensor<T>(string key);
bool HasCapability(ICapability capability);
// with only two goals possible at the moment, other methods are not needed yet
IGoal GetGoal();
}
- Sensors: allow the agent to sense information from the game world.
public interface ISensor : IKey
{
object Sense();
}
- Capabilities: represent what an agent can do in the game world. Like move, load and unload resources.
public interface ICapability {
string Name { get; }
/// <summary>
/// SourceKey represents the key entry referencing a concept in the game world that the Agent can act upon.
/// </summary>
string SourceKey { get; }
/// <summary>
/// RequiredKey represents the key entry referencing to a concept the Agent needs to know in order to act.
/// </summary>
string RequiredKey { get; }
IAction BuildAction(PropertyState state);
}
This is the main change from the previous version. BuildAction
is a method that will generate the action corresponding to this capability. It is the only solution I found to the problem of knowing exactly what to do, which will only be clear at run time. For this reason I have included both SourceKey
and RequiredKey
. These represent entries in the PropertyState
, the keys of the dictionary, that are also used when building the action. For example:
string sourceKey = SelfKey.Location;
string requiredKey = SelfKey.RequiredLocation;
Capability move = new(nameof(LocationChange), sourceKey, requiredKey, Action.MoveAction);
Both sourceKey
and requiredKey
here are mostly static “magic strings” that represent the names of the keys used in PropertyState
. In a bug-free implementation they will always stay the same. Looking for a better alternative if you can think of any.
Anyway, in this example the value corresponding to sourceKey
will be at runtime the current location of the agent / ship, while requiredKey
will hold the destination of the ship. At the moment these are just string representing names of planets. Eventually, to allow ships to travel between star system, a more complex object will be needed instead of just a string.
Here is the corresponding action:
public static Action MoveAction(PropertyState state)
{
return new Action.Builder(nameof(MoveAction))
.WithPrerequisite(Prerequisite.NotAlreadyInLocationPrerequisite(state.Get<string>(SelfKey.RequiredLocation)))
.WithEffect(Effect.LocationChangeEffect(state.Get<string>(SelfKey.RequiredLocation)))
.WithEffect(Effect.UnsetDestination)
.Build();
}
In prose, the action has one prerequisite and two effects:
- It must not already be at the destination location (otherwise it will keep wanting to move).
- If the prerequisite is satisfied, it will change the (property)state of the system by effectively changing the location of the agent and unsetting the entry for the destination.
So putting it all together, here is a CollectGoal
:
public static Goal CollectResource(IResource resource)
{
Goal goal = new Goal.Builder($"{nameof(CollectResource)}.{resource.Id}")
.WithCondition(new ResourceIdentificationCondition(resource.Id, ResourceStatus.Available))
.WithCondition(new ResourceLocationCondition<ResourceSource>(
Concept.ResourceSource(resource.Id),SelfKey.Location))
.WithCondition(new FloatCondition<ResourceValue>(
SelfKey.Resource.Cargo(resource.Id), Concept.ResourceCargo(resource.Id),
new ResourceValue(resource, 20), FloatOperations.GreaterThanOrEqual))
.Build();
return goal;
}
In prose, it is describing a goal that has these three conditions:
ResourceIdentification
: the Agent must know the location of a resource site and it must be available (maybe one day there could be resource sites that the agent knows of, but are depleted).ResourceLocation
: the agent must be at the same location where the resource site is.FloatCondition
: the Agent must have been able to load at least 20 units into its cargo space.
When all three are met, the agent has satisfied its goal and can now pursue another goal, such as bring the resources back. The system currently works in silico and I must now implement the part that makes these decisions happen in the game world.
At the time of writing I have not integrated a comment system yet on this website, but if you have any hop on our discord (link on the main page). Thanks for reading so far!