In this article, we continue our discussion of artificial intelligence techniques in games and introduce the idea of state machines. A state machine is a method for modeling character behavior that has the following features:
* Each character can be in one of a number of predefined states, such as "attack", "defend", "run away", "patrol", etc., each of which has one or more actions associated with it.
* The character monitors what is happening in its environment, things such as the distance to player, the actions being taken by the player, the location of obstacles and items, etc.
* Under certain conditions, the character will change its state; for example, if the player is close to a character and the player is attacking, then the character may change its state to "defend".
In this article, we discuss implementing state machines to control enemy behavior in a platform game.
Initial setup
We start with a layout and window size of 800 by 600, a Sprite with the Solid behavior for the ground and walls of the layout, and a Sprite with the Jump-Thru behavior for platforms.
We create an Sprite called "Enemy" with the Platform behavior, and change the "Default Controls" setting to "No", because we do not want these objects to be controlled by the keyboard. Add an instance variable to Enemy called "State", and it is a good idea to add a description to this variable that list the different values and the corresponding actions. For this example, we will use the following: 0 represents "no movement", 1 represents "move left", 2 represents "move right".
Then in the event sheet, we set up the events to control each enemy based on their current state. For each enemy, there will be a number of subevents to check the value of the instance variable called "State". In particular, if State=0 then there is no action, if State=1 we simulate the platform control pressing Left, and if State=2 we simulate the platform control pressing Right.
Also, each enemy in the layout should have its State value set to something, probably a value different than 0.
Responding to the Environment
Next, we need the enemy to move in an intelligent way around its environment.
Walls
The first events we will add will cause the enemy to reverse direction when it encounters a wall. The Platform behavior has a useful condition for this occurence: Platform - Is By Wall. Select this condition, and select the "Left" option; in this case, when there is a wall directly to an enemy's left, we want the enemy to move to the right, so we will set the enemy's State variable to 2. Similarly, using the Platform - Is By Wall condition and selecting the "Right" option, in this case we set the enemy's State variable to 1, which will cause the enemy to move left.
Now, if we set up part of our layout to look like this: (brown rectangles are Solid objects, green rectangles are Jump-Thru objects, and the red Sprite is our A.I.-controlled Enemy)
our enemy will move back and forth, changing directions once it reaches the side.
Fixed Triggers
As our next improvement, we may want the enemy sprite to change direction at positions other than walls, and in some locations we may want it to jump; we may even want it to stop moving altogether (perhaps if it has reached its destination). To accomplish this, we will create a set of Sprite objects, one for each of the desired behavior changes; we will also set up corresponding events, so that when an enemy overlaps one of this sprites, it either changes its state or it simulates pressing Jump in the case of a jump action. In the example capx provided, these sprites are named TriggerLeft, TriggerRight, TriggerStop, and TriggerJump. There is a simple hand-drawn picture for each one, to make it easier to visualize the corresponding enemy state changes for the purpose of designing the level; when the desired configuration has been achieved, you will want to set the Visibility property of each of these to False, so that the player can't see how the enemies are being controlled.
The set of events for these triggers is as follows:
And a sample layout using the triggers is demonstrated below. This setup will cause the enemy to turn around when it reaches the end of the platform (so it doesn't fall off), and when it reaches the middle of the platform, it will jump.
Random Triggers
One of the cardinal rules for A.I. is that, in order to seem realistic, there must not be a predictable pattern of actions. To increase the variation of each enemy's actions, we create another set of Sprite objects such that, when touched, have a 50/50 chance of causing the enemy to change state or simulate a jump action. In the example capx file, these are named TriggerLeft50, TriggerRight50, TriggerJump50, and TriggerLeftRight50. The events for encountering the first three of these are similar to their fixed-trigger counterparts above, except that we add a random factor to the condition (System - Compare Two Values: random(0.0, 1.0) < 0.5 ), and we need to use an "On Collision With" condition rather than "Is Overlapping", because the enemy sprites should only have one chance to trigger the random condition per action. The TriggerLeftRight50 sprite adds even more unpredictability into the mix; when an enemy encounters this object, there is a 50 percent chance the enemy will change direction.
The events for these triggers are as below:
The image below shows one of the many possible setups with these random triggers; if you run the capx file, you will see the enemy sprite run and jump around the level in a sensible yet unpredictable way.
Responding to the Player: Line-Of-Sight
Another feature that may be worth adding is enemy interaction with the player. In particular, when an enemy sees a player, it is often the case that the enemy will then chase the player. Here, we will discuss how to accomplish this using the Line-Of-Sight behavior.
We have created a player sprite with 8-Direction behavior, controlled by the user.
We have also added the Line Of Sight behavior to the Enemy object. To make it more realistic, we will change some of the properties of this behavior.
First, by default the only objects that block enemy vision are those with the Solid behavior, but in our example, we would also like any objects with the Jump-Thru behavior to block enemy vision. To accomplish this, in the Enemy properties panel, underneath Line of Sight, change the Obstacles property from "Solids" to "Custom". Then, in the event sheet, we need to add an event that occurs On Start of Layout, the actions being Enemy - Line of Sight: Add Obstacle, and select the wall-like objects that are Solid; then add this action again, this time selecting the platform-like objects with the Jump-Thru behavior.
Second, we can (and should) change the Range property of the Line of Sight behavior to something smaller, keeping in proportion with the size of your layout; in this example, we've set it to 300.
Finally, we need to specify in the event sheet what happens when an enemy can see the player (which in our example means that the enemy is within 300 pixels of the player in any direction and there is no visual obstacle between the enemy and the player). For this example, we compare the X coordinate of the player to the X coordinate of the enemy to determine if the player is to the left or to the right of the enemy, and we set the enemy's state accordingly. In addition, if the enemy sees the player jumping, then after a short delay (using the System - Wait action) the enemy will also attempt to jump. You can download and run the capx example file and press the arrow keys to move around the player, to get a feel for how these events work, and to change the values of the parameters if desired.
Hopefully, this article has given you some ideas on how to implement A.I. in your own platform game. If you have any additional ideas, please feel free to share them in the comments section!