Specialization project

Goal oriented AI

For the specialization project I chose to do a goal oriented AI with a reference to The Sims.

The Sims is a game that I have grown up with and spent countless hours on. Needless to say, this series lies close to my heart. So when deciding what to do for our specialization project, one of the first things that popped up in my mind was to make something with The Sims as a reference.

Image from The Sims 3

The Sims is basically a time management game, where the player is trying to make characters survive and earn as much money as possible before they die of old age (with disregard to the first game, whereas the characters never died of old age). Of course there is a loot of different ways to play The Sims (like just building the prettiest houses, or finding all ways the characters can die), but from my point of view this is the basic goal.

To play the game, you make choices for the characters. This choices can be what job they is going to have, when to eat, when to sleep, who to marry, and so on. You can as well chose not to play, and just watch as the characters go on with their lives and do stuff on their own. This AI that decides what to choose feels in The Sims really solid and the characters can survive on their own for quiet some time (at least until they set the house on fire trying to make a sandwich).

The AI that is used in this game is called Goal Oriented Action Planning (GOAP). GOAP is an algorithm that makes an AI character decide on what to do based on a set of goals and actions to reach the goals. The goal that is chosen in The Sims is based on how the character is feeling at the moment. This can be hungry, sleepy, a need for social communication, and so on. The AI is then running the GOAP algorithm to decide what the best goal to make the character less dissatisfied, and what actions to take to reach that goal.

With this information at hand, I choose to try and do a GOAP AI with some of the basic goals in The Sims. I choose to do this in Unreal engine to be able to easily make a small visual testing area for my AI, and also to be able to code in C++ which I feel the most comfortable with.


Image from my Unreal project on GOAP

The Algorithm

My first approach to making the algorithm was to build the world and placing items. After that I made classes for each object type that contained information about the specific object type. For example, the coffee cup satisfied the need for coffee with seven points out of 20 after five seconds.

After having all items put out I started writing the algorithm after reference to the Goal oriented algorithm from the book "Artificial intelligence for games second edition" by Ian Millington and John Funge. Having a reference is important to not waste time trying to invent something that already exists.

Next I formed the code to work for my project, and before I knew it I had something that looked like a GOAP with the character selecting what objects to move to. This was the next step, making the character move to the objects. This was more difficult than getting the algorithm up and running because of my lack of experience with Unreal engine. It took me several hours before realizing that it was impossible to do without using blueprints. Sure, I could move the model through C++ code, but then I would need to implement a collision system and animations on my own, and there was simply no time for me to figure out how the animations worked, and making a Grid with a A* pathfinding algorithm that also worked. So at this point I was a bit stressed and wanted to get this working as fast as possible. So I chose to try and understand the blueprints enough to make the movement work as intended.

In the end I made the movement start in C++ code and used Blueprint functions for the actual movement. The Algorithm worked as intended after some tweaking to the timing on how often the needs should be lowered, and on how much it should be lowered vs. raised to not make the character always low on a specific thing (the character always chose the phone because that need never got satisfied enough and was always low).

Lastly I wanted to give the observer some feedback on what the AI was thinking. I tried to change the colour on the object the AI was going towards, but never manage to do this unfortunately. Then I tried to print some text instead on what item type the AI had chosen, but I never manage to get this to work ether.

Workflow and result

Firstly when I started the project, I made a list of items, which I made a list of tasks based on. I was in other words following scrum even though I was working alone. I then scheduled all the tasks and time estimated them. My feeling here is that I have plenty of time and started working on my tasks.

The first things I did after that was trying to get into how the Unreal Engine worked. At this time I thought the engine would help me build the world faster and thus have more time on the algorithm. Later down the project I run into a lot of problems because of the engine and not knowing how to fetch information and manipulate the world in C++ and not in Blueprint. Finding information online about how to do things in C++ was also a challenge, and documentation was lacking in information on how to fetch world information through code.

In the middle of the project, I started working from home due to the corona virus. This made me fall behind on my schedule and loose several days due to setting up the project on my home computer. Working from home made it all more difficult and it affected my end result allot. Suddenly I was supposed to work on the project 8 hours a day instead of 4 hours a day, and also forced to learn to work at home.

What I would have added if I had more time:

  • Different objects of the same type to give different amount of need satisfaction. For example a phone and a computer. A phone can satisfy the social, while the computer can satisfy both entertainment and social, but the social need should maybe be satisfied faster on the phone (like in The Sims).
  • Queue several actions after each other, to reach a goal. For example to eat, the AI should first go to the fridge then put it in a microwave, then sit at the table and eat. This instead of just going to the fridge and get satisfied from that.
  • Lots of feedback. Animations that interacted with the objects, sound while doing the different things, and maybe a bubble above the AI that shows an image on what it's planning to do next or making the chosen object flash in white.

Working from home meant for me more difficult to get help from colleges and teachers. It also meant having more difficulties concentrating on what I was supposed to do, and ended up a few times not working on the prioritized tasks. All of this made the result of this project not what I was expecting when I started working on it. But I still feel I have learned more about scrum working by myself, planing tasks and breaking down the big tasks to smaller ones. I also got to work in a more advanced engine. And of course I learned about Goal oriented algorithms and GOAP.

Calculating what action to choose

This code shows two functions that I wrote to calculate what action the AI should do based on what need is the most critical one.

std::shared_ptr<Action> AMyCharacter::ChooseAction(std::vector<std::shared_ptr<Action>>& aActions, std::vector<std::shared_ptr<Goal>>& aGoals)
{
  std::shared_ptr<Action> bestAction = aActions[0];
  float bestValue = myWorldModel->CalculateDiscontentment(aActions[0], aGoals, myPollingStation, this->GetOwner(), myNeeds);

  for (std::shared_ptr<Action> action : aActions)
  {
    float value = myWorldModel->CalculateDiscontentment(action, aGoals, myPollingStation, this->GetOwner(), myNeeds);
    if (value > bestValue)
    {
      bestValue = value;
      bestAction = action;
    }
  }

  return bestAction;
}

float WorldModel::CalculateDiscontentment(std::shared_ptr<Action> aAction, std::vector< std::shared_ptr<Goal>>& aGoals, std::shared_ptr<CPollingStation>& aPollingStation, AActor* aActor, std::array<unsigned int, 6>& aAiNeeds)
{
  float discontentment = 0;
  for (std::shared_ptr<Goal> goal : aGoals)
  {
    if (goal->name == aAction->name)
    {
      float distance = 1.f;
      auto needs = aPollingStation->GetNeeds();
      for (auto& obj : needs)
      {
        if (aAction->name == obj.type)
        {
          auto pos = aActor->GetTransform().GetLocation();

          dististance = FVector::Distance(obj.position, pos);
        }
      }

      int newValue = goal->levelOfImportance + aAction->GetGoalChange(goal);
      newValue -= aAiNeeds[(int)aAction->name]*10;
      discontentment += newValue;
    }
  }
  return discontentment;
}