Example: Slime Filth Ingestation - CBornholdt/RimWorld-AI-Tutorial GitHub Wiki
Cave Slimes As Vacuum Cleaners : Complete Pawn AI Example
Lets say, for laughs, that you were a bunch of lazy troglodytes and needed local cave slime to wander around the map eating filth just to keep the floor clean. Beyond issues of poor planning and/or management, there is obvious value to having slime eat filth. From a 10000-meter view, we must do the following
- Create a new pawn Job/JobDriver for the slime eating
- Create a new JobGiver to give that newly created job
- Insert that JobGiver into the appropriate pawn's ThinkTree
Parts 1 & 2 are straightforward and will be discussed first. The 3rd piece could be accomplished several different ways, and each will be briefly considered.
Features First
Before just rushing into code, its very important to have a strong grasp of your design. Having slime eat filth is grand as a initial thought, but immediately certain design-level questions emerge
- What filth exactly will a slime eat?
- How will they eat it? Instantly? Over time?
- When and why will they eat it? (where in the ThinkTree and why is it being reached)
- Will they gain anything from this?
For the purposes of this example, lets be a little ambitious shall we? Lets have filth eating to be a primary activity for the slime, which gives us the following answers
- Any/All nearby filth
- Filth eating will take time/work, and will operate proportionally to the depth the filth
- They will eat for primary nutrition, and when lazy. Essentially whenever filth is available
- Nutrition, and the respect of the local cat maid
Implementation concerns
Designing a new Job in RW involves the creation of 2 highly coupled entities, the JobDriver and the JobGiver (this is a recurring pattern). The JobDriver coordinates the implementation/execution of the Job, while the JobGiver coordinates who can be given the job; it also determines the exact parameters for the JobDriver. First and foremost is determining what parameters the job (and thus the JobDriver) will be receiving.
Here, the Job is to go and eat filth: so the only question for that Job is what filth to eat. Hence the filth will be passed as the first parameter TargetA. However, peaking at JobDriver_CleanFilth gives another potential complication to this affair: TargetQueues.
Target Queues
A job can be passed up to 3 parameters (TargetA, TargetB, and TargetC), nonetheless it is possible to pass more than 3. It can accept 3 types of parameters, or it can accept a queue of targets in the stead of one of those parameters. The job will then utilize special Toils/logic to pop the top queue element and process until empty.
The core advantage of using queues instead of launching numerous smaller jobs is continuity of action (and some performance efficiency); the pawn will reserve as many items in the queue as possible, then move seamlessly between those reserved targets. This approach limits interruptions to the pawn behavior, and enables actions to be taken against a collection of Things instead of just one (as seen when forced ordered to clean, a pawn will also remove nearby filth as well).
NOTE If you want a set of Targets to be performed in a certain order, use TargetQueues
NOTE If the action is being taken against a group/collection of Things, use TargetQueues
JobGiver
//null return means no job is available
public class JobGiver_IngestFilth : ThinkNode_JobGiver
{
private readonly int MAX_SEARCH_DISTANCE = 200;
protected override Job TryGiveJob(Pawn pawn)
{
if(pawn.kindDef != StupidCaveCreatureDefs.RWBSlime)
return null;
var allFilth = pawn.Map.listerThings.ThingsInGroup(ThingRequestGroup.Filth);
float minDistance = MAX_SEARCH_DISTANCE * MAX_SEARCH_DISTANCE;
Thing resultFilth = null;
foreach(var filth in allFilth) {
float distance = filth.Position.DistanceToSquared(pawn.Position);
if(distance < minDistance
&& pawn.CanReach(filth.Position, PathEndMode.OnCell, Danger.Some)) {
minDistance = distance;
resultFilth = filth;
if(distance < 25) //If within 5 cells just go for it
break;
}
}
if(resultFilth == null)
return null;
Job ingestFilth = new Job(RimWorldBiomes.BiomeJobDefs.RWBIngestFilth);
foreach(var target in allFilth.Where(filth =>
filth.Position.DistanceToSquared(resultFilth.Position) < 100))
ingestFilth.AddQueuedTarget(TargetIndex.A, target);
if(ingestFilth.targetQueueA.Count >= 5) //Copying Vanilla's approach
ingestFilth.targetQueueA.SortBy(target => target.Cell.DistanceToSquared(pawn.Position));
return ingestFilth;
}
}
There are several pieces to this, and as a fully baked example numerous parameters exist to fine tune this behavior. Of note however is the first statement, which drops pawns that are not slimes; this will dovetail with our ThinkTree implementation as its quite easy to add something to the ThinkTrees of all pawns, but quite difficult to do so for only a small subset. Hence it is often easier to perform the pawn filtering in the JobGiver itself.
NOTE When writing JobGivers, pay attention to performance costs in evaluating that such a Job is not available ... JobGivers will often be called for more often when they aren't available then when they are