Tech Stuff #2: AI

When I started this game, I knew AI was going to be a hard thing for me. I’m used to writing graphics code and low level system where the code execution paths are nearly the same every frame. For all the games I worked on in the past, I never looked into how the NPCs made decisions.

I wanted the people in the game to do all manner of tasks. They’d live in houses, walk to their jobs, do the work, cut down a tree, go eat lunch, collect firewood, and go get a new shirt from the market. I had no idea how to make this happen or where to start. I tinkered with getting the AI to do simple things, and then built on that.

I broke down all the things an AI could do into simple core tasks, which I call ‘Actions’. These are tasks such as moving to a location, picking up an object, storing or retrieving something out of inventory, creating new objects, or playing animations.

The AI then maintains a queue of actions to complete. Lets say I want to get an AI to collect food from storage and bring it to their home. The code would look something like this:

// head to the storage facility or market ai.AddAction(MoveTo(storageLocation, Animation_Walk)); // get something out of inventory and display it. ai.AddAction(PlayAnimation(Animation_BendDown)); ai.AddAction(RetrieveFromStorage(storageLocation, foodType)); ai.AddAction(PlayAnimation(Animation_StandUp)); // go home ai.AddAction(MoveTo(homeLocation, Animation_Walk)); // drop inventory ai.AddAction(PlayAnimation(Animation_BendDown)); ai.AddAction(DepositInventory(homeLocation)); ai.AddAction(PlayAnimation(Animation_StandUp));

Each item in the queue is executed until it finishes – MoveTo completes when the AI gets to the target. PlayAnimation completes once the animation finishes. Other actions like DepositInventory execute immediately and the next action is run.

I call these chains of execution ‘Behaviors’. The game has a lot of different behaviors, from how to hunt animals, to how to work at a building and use one resource to make another resource. Adding new behaviors and actions is easy. I could make two people walk toward each other, play an animation and play audio that shows they’re talking to each other, then go about their business with just a few lines of code. (But I’d have to make the animation and audio first….)

This came out as a very nice system and is very extensible. But then there is the hard part. How does the AI choose which behavior it should be using at any given time?

Each AI has a set of needs that may or many not be urgent. They might need to stock their home with food or firewood. They might be sick and need a doctor. They might be cold and need to go home and warm up, or hungry and need to eat. They might have low health and decide to visit an herbalist. They AI might be a child and depend on parents for keeping the home stocked.

In the best case all needs are met and the AI should just do some work so the town can prosper – if they have a specific profession and workplace this is easy – but if there is no work available they need to help around town with general labor.

These things don’t sound like nice programming tasks. This is where I ask myself if I’ve made too complex of a simulation….

But to deal with it I turned to something I do know. State machines. There are different states of working versus attaining needs, and there are different versions of each of those for when an AI is a child, student or adult. Each time the action queue is empty, it signals the AI to make a new choice or change states based on needs.

The needs are prioritized based on severity – freezing to death is more important to take care of compared to getting a new tool. Deciding what job to work on is handled simply by ranking jobs based on urgency and distance from the AI.

Each state is simple. Here’s what the normal and attain needs states looks like:

void State_AdultWorking() { // check to see if the ai is doing something if (IsBusy()) return; // do more work, idle, or change state if (AreNeedsMet()) { if (!GetNewTask()) ChangeState(AdultIdle); } else ChangeState(AdultAttainNeeds); } void State_AdultAttainNeeds() { if (IsBusy()) return; if (AreNeedsMet()) ChangeState(AdultWorking); else if (IsFreezing() && CanAttainWarmth()) ChangeState(State_AttainWarmth); else if (IsSick() && CanAttainDoctor()) ChangeState(State_AttainDoctor)); else if (IsHungry() && CanAttainFood()) ChangeState(State_AttainFood); else if (IsUnhealthy() && CanAttainHealth()) ChangeState(State_AttainHealth); else if (!HasClothing() && CanAttainClothing()) ChangeState(State_AttainClothing); else if (!HasTools() && CanAttainTools()) ChangeState(State_AttainTools); .... // lots more needs omitted. else GetNewTask(); // couldn't attain any needs! try doing work instead, will retry later }

I implemented all this and then decided this wasn’t really AI – it’s a conglomeration of state machines, fifos, priority queues, and a few conditionals. But magically when hundreds of AI’s are running the state machine and executing behaviors, it looks awesome and the town is alive with activity.

This system could easily be extended to give the AI’s more personality. I could have different states for lazy workers that idle more often than not, or people that won’t work when their health is low or if they have to walk too far. But for now everyone is a well behaved, hard-working citizen.