Devlog #3: Goblin brains
Goblins, and for that matter all creatures, need code to tell them how to act. In Goblin Camp our goblins must be able to fend for themselves. They need to feed themselves, drink, sleep, build shelter, fight off attackers and perform all kinds of jobs at camp. They ought to do these tasks while also being aware of their surroundings, they shouldn’t seem like robots or zombies that ignore all the things going on around them.
In this post I’ll go through the systems I’ve written. They can be divided into two main systems. The first is a very traditional finite state machine, while the second is a more interesting and complex utility system.
I focus on how goblins behave in this post since they are the main characters, but the same systems drive the behaviour of all the creatures of Goblin Camp, from the birds flying in the sky to the deathfolk oggling at your gateway rock.
High level planning
Goblins form high level plans to establish a rough idea of how to fulfill their needs. For example, a guard goblin should grab a weapon and some armour if they don’t already have them, hang around at their assigned guard post, and react to any alarms around camp. These three things translate into the 3 states a guard can be in. These states make it easy to write code in the style of “If goblin is in the ‘Guard’ state and a better weapon is available, reserve the weapon and transition to the ‘Equip weapon’ state.”
Sleeping is a more involved example. A sleepy goblin will prefer sleeping in their own house, but if they don’t have one the plan depends on how cold it is. In warm weather, the plan will be to pick a nearby tree and sleep in its shade, while during the cold months they’ll formulate a plan to gather branches and build an improvised lean-to to sleep in, which in itself needs planning to find a suitable place to build one so it doesn’t block access to any existing buildings.
Each plan can only ever be in one state at a time, and will change from one state to another depending on what goes on in the game. Generally anything that behaves like this is called a finite state machine.
Each plan is independent, they don’t interact with other plans at all. This keeps their implementations very simple, and it’s easy to add new ones whenever a new plan is needed. A goblin will have multiple simultaneous plans: They’ll have a job they need to perform, they might be sleepy and thirsty, perhaps they’re being menaced by a wolf.
Whenever a plan changes from one state to another, it will add one or more behaviours to the goblin. From the above examples, if the guard goblin has decided to equip a weapon the code will add behaviours to walk to the weapon, pick it up and equip it.
Low level behaviours
Every behaviour is a fairly simple, single thing to do. Examples are: “Move somewhere”, “Pick up an item”, “Attack an enemy”, “Wait”, “Equip a weapon”, “Catch a fish”.
A goblin can only do one thing at a time though so they need a way to choose from a large set of behaviours. They could have 5 behaviours that want to move them to 5 different places, and they can only be in one place at a time. To make goblins act sensibly they also need to be able to react to sudden changes around them, so they have to ready to change their minds in the middle of a plan.
A utility system is in place to allow goblins to decide which behaviour is most beneficial to them at any given moment. In particular the system is modelled after the Infinite Axis Utility System. Plans give behaviours an overall priority to give a rough ordering (idling at the gateway rock is a lower priority plan than performing a job, for ex.). More importantly each behaviour has a set of considerations attached.
Considerations
A consideration is a basically a function that evaluates to a number between 0 and 1. They are things such as “How far away is the item we want to pick up”, “How thirsty is the goblin” or “How many enemies are nearby”. Each consideration also has a response curve that determines what score the behaviour gets based on the value of the consideration function. This lets you tailor the same function for different purposes. Some behaviours should be more likely to happen if an item is nearby, while others should be less likely. We can reuse the same consideration for many behaviours just with different response curves.
A behaviour can have any number of considerations attached, and since the considerations are each independent adding new ones and modifying existing ones is very simple. Evaluating consideratons can be done in parallel since they only read information about the goblin and its surroundings and output a score value. This easily scales up to hundreds of goblins and other creatures all simultaneously evaluating and scoring their behaviours.
Thinking and reacting
One of the main features of this system is the reactivity it provides. Let’s look at a concrete example.
A goblin has been assigned to fish. They formulate a plan with these behaviours: “Go to fishing spear”, “Equip fishing spear”, “Go to water”, “Catch a fish”. When the goblin has equipped a spear and is on their way to the water a ravenous crane descends from the sky and attacks them. Because goblins always have a behaviour to defend themselves, that behaviour now takes precedence over anything else and the goblin stops moving and starts defending themselves with their spear. A guard goblin arrives as well and they defeat the crane together. During the battle the fisher goblin became thirsty, and a set of behaviours to find water and drink have been added. During the battle any thirst behaviour is not important enough to be chosen, but once the battle is over drinking is the next most important thing.
Now the fisher goblin goes to the nearest water source and has a drink. Once they’ve had their drink the drinking behaviours are removed, and we’re left with the original behaviours added by the fishing plan. Before they were interrupted they were still moving towards the water to fish, but the consideration for the “Catch a fish” behaviour scores highest now because they are already next to water. So they directly start fishing.
Modelling this kind of behaviour using finite state machines is possible, but accounting for all the various scenarios quickly becomes infeasible and often the solution is to just cancel whatever was going on and switch to the new state, or wait until the current state ends. In the above example the fisher goblin would cancel fishing altogether when attacked and drop their fishing spear. If they were to become thirsty while fishing they’d likely keep on fishing regardless, potentially becoming dangerously thirsty while doing so. With the utility system AI it is simple to switch between behaviours and keep the old ones active, the system automatically rearranges them according to their scores, allowing goblins to react to changes and continue jobs where they left off.
On the other hand, adding in new plans with new behaviours remains easy and simple regardless of how many there are already. Since behaviours arrange themselves automatically based on their combined consideration scores, the complexity of the system doesn’t increase when new behaviours are added. I can layer on as many behaviours as I wish and the goblins will keep behaving correctly. This allows us to keep adding more complex interactions between the goblins while keeping the code maintainable and bug free.