How I Build Complex AI Characters (with or without TrueState)

3 years ago by Pixelated Pope (@Pixelated_Pope)
Share this post:

Hey Gamemakers!  

Today I want to discuss developing complex, AI controlled characters in your game.  Specifically, I want to show you the pattern I follow when building game characters that allows me to make any character controllable by either a player or AI!  But before we get into the details, let's discuss who this article is and is not for.

This May Not Be For You

So two things will determine whether you will get much out of this article.  

First.  The complexity the entities that are not controlled by your player.  Let's look at two examples.


These little guys are "Zoomers" from Metroid 1.  They are extremely simple enemies as far as their behavior is concerned.  They are always moving, and always following the edge of whatever wall they are currently attached too.  If you think about them in the context of a state machine, they really only have 1 state: move.   The way they move is a constant; it doesn't matter where Samus is or what she is doing: they are going to behave the same way.  Realistically, these guys aren't really "AI controlled enemies" as much as they are just "Moving Hazards".  If the enemies in your game are like these guys, this article probably isn't going to be very helpful to you.


These Space Pirates, on the other hand, are much more complex.  They can walk, look around, they can jump and climb walls.  They will patrol to look for Samus and select from a number of different attacks in order to kill her.  These guys are complex in every way.  Complex behavior and complex decision making.

It's character's like these that this tutorial is about.  They react to the player, they make decisions, and move through the world in a very similar fashion to Samus herself.  So if you want to build complex characters such as these Space Pirates, read on!

The second thing that might prevent you from getting much out of this is if you are already pretty far along in your game project.  What I'm describing here is really a pattern for building characters in your game.  From playable characters and enemies, bosses and NPCs, they all can be built following this.  But if you are already months into your project, it will be difficult to adapt any existing object to this pattern.  It's possible, but a lot of work.  Hopefully this article will give you some ideas for your next project if you are a bit too far along to follow it in your current game.

The Actor Pattern

As I mentioned above, this pattern is used for every object that vaguely matches the Space Pirate's description.   The player characters, allies, enemies, NPCs, bosses, etc, etc, they would all fit.  As such, they all would share a common parent object.  This parent I call "objActor". 

The actor is defined by it's step event.  In the step event 2 things must occur -in order- every single step:

  1. Read Input
  2. Execute State

That's it.  More can go into the step event, but the vast majority of the step event will be those two operations.  For example, I also do "freezing/pausing" of the actor in this same block of code.  Obviously the devil is in the details, so let's dig into both of those one at a time.

Reading Input?

"Wait, I thought this tutorial was about building AI controlled characters?!  Why are we reading input?"

Good question!  Let me explain.

Building complex characters is hard.  How many times have you tried to build a normal player character and ran into problems like this: 

  • Character gets stuck in the wall
  • Character doesn't collide with world properly (ramps, ledges, falling)
  • Character can break the expected rules (jumping while not on the ground, crouching while in the air, not falling when performing a specific action, etc)
  • Mashing buttons produces unexpected results
  • etc

Any number of things can go wrong and get buggy while building a character for your game.  Now imagine trying to debug that character without being able to control them!   When you build character behavior and AI simultaneously you are asking for a VERY bad time.  Every bug will start with one big question: "is this the character's fault or the AI's fault?"  If all your character and AI code is bundled together, how can you even tell the difference?

The Actor Pattern solves this by making every AI controllable character PLAYABLE.  You never build an NPC, enemy, or boss without being able to play it yourself!  Not only does this make your life a lot easier, it also has the bonus effect of opening up the possibilities of making NPCs, enemies, and bosses PLAYABLE CHARACTERS!  Whether it's something like Castle Crashers that has almost every enemy in the game available as a playable character, or a more temporary mind-control ability, the possibilities are pretty exciting.

So how do we do this?  Well, it starts in the create event.

Reading Input

The first thing we need to decide on for any one actor or group of actors is the input they listen to.  Not necessarily which buttons on the keyboard or controller, but what what general "actions" or "events" the object will be listening for in its state.  For a very simple platformer character we might just have 3:

  • Left
  • Right
  • Jump

So those are the variables I put in my create event.  We want to avoid having variables on our actors that link them directly to the method of input.  So no "aButton = false;" or "dpadUp = false".  That doesn't make much sense, especially when you want to support multiple consoles or multiple button configurations. 

Ultimately what is going to consume this variable?  Who needs to "react" to it? 

Your state code.  And it makes a lot more sense to have your state code read like this:
   if(jumpPressed) state = State.jump
than this
   if(aButtonPressed) state = State.jump

Obviously reading this input can get pretty complex.  Your state may need to react to held or pressed or released, etc, and you just need to build your system to accommodate your needs, but the end result of reading input should have all of those "input event" variables populated.

Before we enter the next part of your code -state execution- you should know the state of your input: left, right, and jump.

State Execution

Whether you are using a simple Switch Statement, TrueState, or your own finite state machine, it's time to use that input to determine what your character is doing.  There are tons of tutorials on how to use FSMs out there, so I'm not going to spend too much time here, but I do want to lay down a couple rules.

1. State controls State! 
Always think of your character's states as a flowchart.  When you have a flow chart arrows come out of each box based on "choices".  This means arrows shouldn't be coming in from off the page to force your flow chart to go a specific direction.  You should only be changing the state of an object from within that object's state!  Forcing an object into a state from outside of that object's state machine is a good way to get into a lot of trouble.  A very common offender is the "hit react" state.  
Object A is getting attacked by Object B, so object B tells object A that it should change it's state to "getting hit".  But Object A is in a state that it shouldn't be possible to get hit at all... and now it's reacting to a hit?  That's not good.  A better solution is for Object B to "message" Object A that they have landed an attack on them, and then in Object A's state code, they can choose how to handle that "message".  
Regardless if you find yourself changing the state of one instance from another instance, think very very very carefully about what the implications of that are.  99% of the time, it's a mistake.

2. State Machines don't mix!
I'm getting a little bit ahead of myself with this point, but it is possible to have multiple state machines on a single object.  Learning where to draw the lines to divide things a character is doing into multiple, parallel state machines can be very important.  A good example is Samus from Super Metroid.  Samus has a gun arm, and that gun arm can be doing any number of things while Samus herself is doing any number of things.  Samus can be standing, running, jumping, or crouching and simultaneously pointing her gun arm in 8 different directions, it could be in missile mode or standard mode, etc.  You certainly don't want to create a unique state for every possible combination of gun arm position and Samus action, so a second state machine should be used.
This is more important with AI.  If you've attempted to build an AI controlled enemy in the past, I'm guessing you attempted it with a state machine of some sort.  But your states may have been called "Chase", "Wander", or "Attack" as opposed to your playable character states like "Stand", "Run", and "Jump"
This is bad because you are mixing Decision Making with Character Behavior.  A character "decides" to chase a target, and to do so it must "walk" or "jump" or both in some sequence.  That's two different state machines... and now we are REALLY getting into the thick of things!

Implementing AI - Decision States

So we have our character.  It moves around, I can press a button to make it jump or make it attack.  It seems to behave correctly, respond to input correctly, and interact with the world correctly.  Now it's time to put down the controller and let something else drive this character.

When we read input from the player, the result is all of our controller variables get set to true or false.  Is the player holding left?  Did they tap the jump button?  These questions all get answered when we read input.  That input is then responded to in the behavior states.  But what made the decisions to press right or jump?  

You did.

And why did you make that decision?  

To accomplish some sort of goal.  Avoid an enemy, collect a coin, throw a fireball to kill an enemy, etc.

And are you intelligent?

Reasonably.

So if we want "artificial intelligence" in our game, then you - the "non-artifical intelligence"- are the part of the equation that needs to be swapped out.  We must add logic to our game that replaces YOU.  Why did you hold right?  Why did you jump?  When you really start to think about it, the answers can be REALLY complicated.  Even more so when you realize that you can observe the game screen and just "reason" things that no AI character will be able to do... at least not an AI character you built in GM.

So you'll build another state machine, but this time to control decision making.  You may have an "idle" state that will sit there and wait for the player to get within line of sight.   Or it might just wait a few seconds before deciding to switch to "wander."  In the wander state, it may move left or right until it reaches a wall or a ledge and then it might switch directions.

There's a lot going on in these states.  Collision checks, distance checks, line of sight checks (which I guess is just another collision check), etc.  There could be path finding and any number of game systems could be interacted with to help the AI understand what it should be trying to do at any one time.

But ultimately the result of any of these decision states is that the input variables are populated.

Say your character is in the "idle" state.  What's the result of your input variables:

Left = false, Right = false, and Jump = false;

Now they've spotted the character.  It's to the left and up a bit.  Time to switch to the "pursue" state.

The pursue state says "well, the target is to the left, so Left = true"
Uh oh.  You've hit a wall.  "Based on my jump height, if I pressed Jump and continued to hold left, could I get up on that ledge?  Yes.
Left = true, Jump = true."

And on and on.  AI in a platformer can be incredibly difficult.  Next time you play a platformer, really observe what the enemies are doing.  Most of them will simply "stick" to their current platform.  Or they will fall off a ledge with no real plan on how to get back up.  Those that can pursue you across multiple floating platforms will often teleport to follow rather than "jump" like your player character would.  Or if they do jump, it will usually just be "at random" or in very, very specific situations.  

But all that really matters for this pattern to be functional is that you populate the same variables that you would be populating if you were reading directly from the player's input, then react to those variables in your behavior states.  If done correctly you can re-use the same decision logic for multiple types of enemies, or swap out the script to make an enemy more aggressive than another similar enemy. 

Bringing It All together

My objActor step event typically looks like this:

if(ai_script != noone)
  ai_script()
else
  read_input()

truestate_step()

If I have an ai_script, I execute it, expecting this function to populate those input variables.  Otherwise, I go to the player and ask for them.  This makes toggling my character between AI controlled and player controlled as easy as setting 1 variable.  I can even use this to temporarily take control away from the player!  Just set ai_script to an empty function, and the player's input will be ignored until you set it back to noone.  Useful when in menus or when text boxes are on screen.  I can even use these for driving the character during cutscenes.

Now if I have an issue with the way an AI controlled character behaves, I can approach the problem in two steps.

First: take control of the character myself and see if I can reproduce the issue.  If I can, then the problem is in the behavior state code, and since I can reproduce it manually, I can also confirm my fix manually.

Second: if it can't be reproduced manually, than it is likely an issue in the decision making process.  These can be a bit more difficult to solve, but at least you've eliminated a huge chunk of your code base as a culprit.

Regardless you are now in a better position to deal with these challenges, and adding new states on either layer will be much easier.  If you want to add a new special attack to your character, just build it and hook up some input to it.  Now you just need to teach your decision layer when to you use it.  Both implementations are completely isolated and can be worked on and tested individually.

Unlimited Buttons

One cool thing about this implementation is that the AI can no longer "cheat".  Ever.  Since it controls the character just like you would if you plugged a controller into it, it has to follow all of those same rules.  That doesn't necessarily HAVE to be true, though.  It is completely possible to add input variables to your actor that you never would have hooked up to a button on a controller.  This can allow you to enter specific states for the AI that don't really make sense for a playable character.

For example, scroll back up to that GIF of the Space Pirate.  He walks right and then stops and looks around.  You wouldn't normally have a "StopAndLookAround" button on the controller for your character to use, but you need the AI to decide that's what the character is doing.  So, you can add an input button that never gets hooked up in "read_input()".  Or if it does get hooked up, it's to a debug key that's always on the keyboard and only available in debug mode or something.  In this manner you don't need to restrict your AI to what's available on a Joycon or DualShock.  They can have unlimited "buttons" to play with.  It will mean that character will be more difficult to allow the player to actually play with (like Castle Crashers) but this makes a lot of sense for bosses with many complex attack patterns that would be a pain to try to map to a controller.

Conclusion

Hopefully this tutorial will help you change the way you think about building complex AI characters in your game.  And by doing so, adding features and fixing bugs in said characters should be way, way easier.  I tried to write this tutorial to be completely independent of TrueState, but this entire concept is the driving force behind TrueState+.  Being able to create multiple state machines that run parallel to each other in a single object is super powerful for AI. 

If you use TrueState and haven't checked out the TrueState+ example project, it's worth taking a look at how I did this.  In that project there is a  character with two state "layers"; one for behavior and one for decision making.  Clicking on the character toggles between player control and AI control. 

The decisions the character makes are... pretty stupid, but it demonstrates how two different state layers can communicate.  The Decision Layer is aware of the current Behavior Layer state, and can react to it.  This allows for the AI to know it's sliding down the wall and is able of executing a wall jump.  It knows what current stage of the attack combo it is in, and can choose whether to continue it or not. 

All in all, I hope you found this useful.  

Thanks for reading!  Now go make something awesome!

Get TrueState - Finite State Machine for GMS2

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

(+1)

If you ever have time, would be nice if you  could make a video of this on your yt channel. Either way this is very helpful as well. Thanks.