Creating Natural Artificial Intelligence

In our game, many AI-controlled characters walk around the level, becoming victims that the player can scare, anger, etc. These victims are divided into two types: guests and workers. Guests simply mock the player, while workers stop the player's antics. Currently, we have a very uninteresting system consisting of random points across the map that the AI would go to randomly, without any coherence, often resulting in several people going to the same place, and overall it was just aimless wandering. The solution to this problem was creating a task system where the AI would need to perform tasks around the level to liven up the scene.

The general idea was to have places on the map that are of interest to the AI (POI). These POIs would contain spots that the AI can occupy, and these spots would contain tasks that it must perform. Then, instead of random wandering from place to place, the AI would go to one place, perform a task for a certain amount of time, then go to another place, perform another task, and so on.

I started by creating a template of one such point of interest to work with. The first one I created was a dining table with chairs, where the AI would go to eat. The POI consists of a parent object with a number of child objects that represent spots the AI can occupy. In the case of the dining table, the parent object is the table, and the spots are the chairs where guests sit, as well as an additional spot near the table where a worker can stand. The parent object has a POI script that holds references to the various worker and guest spots, as well as the types of tasks that the POI performs.

Each of the spots also contains 3 transforms: a transform for the visual object, one for the initial position of the visual object, and one for the position the visual object needs to be in to be used. This is so that when the AI comes to sit on the chairs, the chair can be pulled out from under the table to be used, and pushed back when the AI leaves. Then there is a global POIManager, which is a singleton existing in the scene that stores a reference to all POIs in the scene, and handles getting POIs and tasks for the AI.

As for the tasks themselves, the idea was to create a flexible task system allowing many types of tasks. To do this, I created a base class TaskTracker that tracks information such as the number of completed tasks, all possible subtasks, has methods to get important information like the task's target location or completion, and methods to affect the task, such as telling it that it has started, stopped, or completed.

Next, I needed to create an actual subclass of this class for the real tasks the AI would use. Currently, there is only one task type, which I call occupant tasks. These are tasks performed at points of interest, where the AI goes to the point of interest, stays there for a certain time, and then the task completes. For this, I started by creating an OccupantTaskOutline script that inherits from ScriptableObject so we can easily create variations of these tasks.

This scriptable object contains the information needed to create a task: the amount of time it takes to complete, who can perform the task, and the task that is created upon its completion—more on that later. I then created a TaskTracker subclass called OccupantTaskTracker, which takes an OccupantTaskOutline and a POISpot—the location of the task. The class itself is very simple; it has a method to get the spot’s location for the task so the AI can go there. Upon arriving, the task tells the location to prepare to be occupied by the AI; in the case of chairs, this means the chairs slide out. After the chairs have moved, the task tells the victim to occupy the spot, causing the AI to "sit" on the chair. Currently there is no animation, so the AI just stands inside the chairs. At this point, the task starts, which starts a timer that adds progress to the task. After the specified time, the task completes, and the task tells the victim to vacate the spot, and the chair to return to its original position.

Upon task completion, the location creates a new task, and this is where the output task from the task outline comes in. The new task is created as usual, but if it is a worker task, it is sent to the worker spots of the POI, and if it is a guest task, to the guest spots. The purpose of this system is to allow both recurring tasks that, upon completion, will create another instance of themselves, and chain tasks that create different tasks after their completion in a series. For example, a dinner task at the tables creates a cleanup task for a worker, and the cleanup task in turn creates a dinner task for guests.

Now, tasks need to be created and acquired by the AI in the first place. As I mentioned, the POI stores the types of tasks that exist in that location. These are task outlines, and when the game starts, if it has an outline for a guest task, it creates that task in all guest spots, and if it has a worker task, it creates it in all worker spots. Then, when the AI needs a new task, it requests one from the POI manager, which in turn looks through all POIs, gets all valid tasks for that AI type, and returns a random one. The AI then reserves that task so no one else can take it, and starts moving to the target location. Once the task is done, it disposes of it and gets a new one, repeating the whole process.

That's a general overview of the whole system. This is an incredibly overcomplicated way to implement this functionality, but the point was to create a system that can be easily extended to other task types. For example, I am now working on creating another type of task where the AI will search for an object left by the player in the level, collect it, and return it to its original place. Because I did the heavy lifting of building the system foundations, it's incredibly easy for me to create new tasks with very little technical debt.