4 - Artificial Intelligence Progress in NanoPets


Hi all,

Sorry for this post being late, it ended up being longer than expected and I kept getting distracted from writing it. The topic is on how I approached and built the Pet AI in my game. In the future, I'll try to make my posts shorter, but this was a big enough thing that I wanted to record my thought-process and the steps I took to making it work. I don't think my solution is close to being perfect, and I'm 100% open to feedback on my implementation.

My Programming Experience

I am Good at programming. Just Good, not Great.

Given my professional experience I don't think I'm BAD at programming, but I don't see myself as an engineering genius either. I know a good amount of design patterns pretty intuitively, I know when something isn't optimized (and can usually tell how it could be better optimized), and I can usually google my way out of any programming problem.  I haven't had a lot of experience building giant complex architectures, I'm not a PRO at any of the languages I use, and I'm not as quick at building & problem-solving compared to some of the more senior engineers I've worked with. I feel like I'm at the level of most mid-range programmers: I'm not a savant, but I can build stuff and eventually figure something out with enough time and coffee. Coming to terms with your skill level is important, especially when you are taking on a giant project on your own. Its important to know the strengths you can leverage, weaknesses you can improve on, and gaps in your knowledge you are willing to say 'fuck it, I don't have the time/money to learn all that right now'. 

Working on NanoPets, I have a limited amount of time and resources at my disposal, and I need to utilize both as best as possible if I want to hit my goals and deadlines on time. Nothing made me realize this more than thinking about and working on the AI in NanoPets.

Ideas and Brainstorming

At the time when I first started thinking about NanoPets AI, I already had an extremely basic enum-based AI system in two classes called NanoBehavior.cs and NanoMovementController.cs. Running a small handful of states worked really well, but the more states I added to it, the more complicated switching between each state got. Every state had to be made aware of the other states for a smooth transition, so by the time I had my nano Eating, Wondering, Going To The Bathroom, and Playing With Ball, adding in Sleeping was close to a nightmare. In every different state, I had to add in a lot very specific hard-wiring in both NanoBehavior and NanoMovementController so that the pet wouldnt just go to sleep halfway through their dinner, or just leave in the middle of a shower to go to bed. At that point I realized that my simple AI system was reaching it's limits, and I needed a more robust system

I started first thinking about the AI in NanoPets when I was in Japan last May. I had several goals I wanted to achieve with pet at:

  • I wanted pets to move around and interact with things in the apartment on their own 
  • I wanted pets to do things based on their physical and mental needs at the time
  • I wanted their  AI to be easily interrupt-able with new events happening in the apartment, or with player interaction
  • I wanted pets to behave completely differently depending on any number of different states they could be in, like being sick or having a temper tantrum
  • I wanted the AI system to be modular, easy modifiable, and fairly agnostic to the new pet states and behaviors it was running 
    • I.E. - I wanted to make it as easily as possible for me to just simply ADD onto the AI to the system without having to change anything in the underlying system
  • I wanted pets to be as expressive as possible and SHOW their needs/feelings so players know exactly what they're doing and thinking

Laying out these goals from the get-go made it really easy for me to see that my AI was going to be pretty different from most traditional video game AI. I started looking up popular Game AI designs like FSM (Finite State Machine), HSM (Hierarchical State Machine) and Behavior Trees, but there were glaring limitations to these systems for what I wanted.

At first I tried implementing a simple FSM. The benefit of an FSM was that it would be very easy to add on game states. FSMs look really nice and organized, but right away I ran into some big problem: unlike the most basic Enemy AI, any state my pet is in can transition to any other state. This meant that my FSM would be a HUGE web of possible transitions between each of the different states. Another issue was that having modifiers on the different states depending on the pet's current status would be difficult to deal with (for example, an 'eating' state is going to be different if the pet is sick, depressed, or feeling mischievous), and possibly might require duplicate states and even MORE transitions between them. I quickly abandoned FSM, and it just seemed a slightly-better structured version of my original enum system but didn't address my overall design goals.

HSM seemed like it might be a better alternative. HSM would've been good for compartmentalizing different behaviors depending on a pet's health/mental status by being different meta-states (I.E. - there would be a Sick meta-state that included a state machine for all the different pet behaviors when they're sick). However, there was still the hell-web of transitions between every possible state. Behavior Trees didn't look promising either, since the overall design seemed far too restrictive on open ended decision-making. 

I didn't even dive too deeply into Behavior Trees until I came to a big realization: Most of these popular AI designs are created for video game 'Enemies'. Enemies are very single-minded on their goal, which is to find you and kill you. An enemy will usually have a small set of states, such as Patrol, MoveToPlayer, and Shoot, which means it's really easy to transition between these states. There can be more enemy states than this, but enemies ultimately have one single goal to fulfill. On the other hand, pets in NanoPets don't necessarily have a single goal. They can have all kinds of goals at different times, like fulfilling their need of hunger and sleep, or unleashing their anger when they're throwing a temper tantrum, or even NOT doing goals when they're experiencing physical/mental illnesses. These popular AI systems were limited because they're meant for AI agents that have limited behaviors by nature. This made me realize I had to go elsewhere for game AI designs and ideas, and almost immediately I knew where I should've been looking all along.

The Sims and Utility-Based AI

Fucking DUH! My pets were just like The Sims! The Sims walked around a house, interacting with things based on their needs, and had ways of expressing their needs directly to the player. I started doing research into The Sims AI, more specifically into Utility-Based AI.

The basic idea of Utility-Based AI is that your AI agent has a large number of goals they want to achieve. These goals have a certain priority based on their severity, which can be determined with a utility curve (for example, giving an exponential utility curve to a Hunger goal mean that as the agent gets more hungry, their NEED to satisfy increases exponentially). The way they achieve these goals is by looking through the world and figuring out what actions fulfills the top, or near top, priority at that moment. This idea gets more advanced with Goal-Oriented Action Planning (GOAP), which takes into consideration the positive and negative impact actions have on all of your goals and figures out the best set of actions. I skipped over GOAP since it wasn't necessary for pets in my game; they don't need to be 1000% rational beings, just kinda do stuff that makes sense based on their physical/mental needs.

After reading a bit more, this was the brilliant system I came up with on my whiteboard:

First Attempt at Pet AI


This is a really messy board of random gibberish, but here's how I initially imagined the AI to work:

Pets (the AI agent) has a set of physical/mental attributes such as Hunger, Hygiene, Energy, Love, Comfort, Discipline, etc,  that are all stored and controlled in the Nano Well-Being class (changed to Pet Health Controller later). This class will also include their physical/mental illnesses. From these attributes, and using pre-determined priority (utility) curves on each of these attributes, I create a list of Pet Needs (goals)  sorted by level of priority. 


Something I picked up from the Sims was interactable objects being able to advertise themselves, i.e. every object has one or more actions pets can perform on them, each with level of satisfaction towards one or more pet attributes. This was key because it meant I can easily add new pet-interactable objects in the world without needing to change anything with my pet. So when the pet is idle, they can just scan through their Needs and check against the list of all all available actions both in the apartment (ex: eat food, play with ball) and available to themselves (ex: complain about being hungry). The pet then determines the best next action based on a Fulfillment Score, which was basically how high of a priority a Needs is multiplied by how much an available action satisfies that need.

Once an action is chosen, the Pet Brain figures out a sequence of actions, or a Pet Activity, to fulfill that need and executes using the Pet Controller. For example, if the pet wants to Play With Ball, the Pet Brain will figure out the best way to go about that (1. Move to ball, 2. Hit ball around, 3. Repeat 1 and 2 for a timed duration or until the pet's Boredom is satisfied), and will run commands on the Pet Controller, which controls the pets actual physical movements and appearance. There were problems with this setup, which I'll get to later on.


Another crucial part of this setup was to incorporate interruptions. Since a player should be able to pickup the pet at any time, or the player may bring the pet to the bathroom, the Pet Brain needed to be able to handle these kinds of interruptions and continue on the previous activity when the interruption is complete.

So all in all, my system was a Utility-Based AI that executed actions using a ad-hoc pseudo state machine. It surprisingly came out working pretty well: The Pet Health Controller was able to generate a list of needs with the help of Unity's AnimationCurves as priority curves (editing these directly in the Inspector was really convenient), and the Pet Brain was able to figure out the most logical next actions to perform in the apartment. Determining the next action was simple, effective, and easy to debug when issues came up. The part where my AI solution started to fall apart was in my weird ad-hoc state machines Pet Activities that were built from actions a pet could do.

Fixing a flaw in My Pet AI

At first, the original intent for my Pet Activities system was to be able to create a linear sequence of actions for a pet to perform, some actions included things like MoveToObject, Interact, ExpressEmotion, etc. Right off the bat the biggest issue was it's inflexibility: actions had to be limited and generic to fit each of the activities, there was no ability for conditional branches in the activity sequence, and allowing for interruptions in every action became a pain. At this point, I was considering making each activity it's own full finite state machine, but pet actions would still be pretty inflexible and making a bunch of nearly-identical actions with slight variations depending on the Activity seemed like overkill.

I decide to have each PetActivity as a state machine, but a very simple enum-based one. Each PetActivity is an interface with Initialize (Setting up fulfillment advertisement), Start (Starts the activity), Update (Updates the activity on each frame), End (Finishes the activity at completion), Kill (Stop the activity gracefully before completion), Interrupt (Pauses the activity to allow another activity to happen), and Resume (Attempts to pick up from where the activity left off at the last interruption), all of which take the PetController as a parameter. Internally, these pets have a private enum-based state that's controlled and changed based on when the activity is started/updated/ended as well as the PetController state (are they at a location? are they in the middle of eating? etc). At first this seems like a pretty inelegant solution, but the Activities are so small that having a simple enum-based state within these activities gives me WAY more control over the small things a pet does during an activity, as well as making it really easy to debug.  It's not perfect, there's a good amount of duplicate code between the PetActivities (almost all activities require moving to an object), but it's a good trade-off for having tighter control on small pet activities and being able to insert smaller behavior details within a single activity which wouldn't be achievable with more generic pet actions.

After that change, my board looked like this:


Not too different from before (NOTE: The ACTIONS on the center-bottom refers to available activities on any given apartment object, NOT a PetAction which aren't used anymore, forgot to rename it).

Conclusion - Still a Work In Progress

At the moment, this AI achieves being able to react to their own internal needs and act on fulfilling those needs using objects in the apartment. It's easy to create a new activity with the PetActivity interface API and slap it onto any new object i want to create for the apartment. All in all, I'm really happy with this solution and it's working pretty much exactly as I expected. As someone who doesn't normally create organized code structures like this, I was happy that I was able to achieve my goals while creating something with a good balance between complexity and simplicity/ease-of-use.

I'm also happy I was able to finish this AI in a relatively short amount of time. I think if I had more time, I could've put it towards doing more research and figuring out a more elegant structure that followed proven patterns more closely. But as I mentioned at the start, I don't have unlimited time on this project and I don't have a lot of experience building complex code architectures from scratch, so this was a great stepping stone for me for both a project management and personal growth standpoint.

There is still a lot of features that haven't been implement yet which could affect how AI would work. For instance, physical/mental illnesses haven't been implemented yet, so there may be some unknown problems hiding. I might write a follow-up post on this, but honestly this post took me WEEKS because of it's length and running into a lot of distractions. I'm trying to improve on writing these posts, so definitely think next time I'll cut down the length a bit.

Thanks!

Leave a comment

Log in with itch.io to leave a comment.