Developer Console

Simple AI

Written in April 2018 by Nathan Ranney, the founder of game development studio Gutter Arcade.

In this post, I will show you some simple AI concepts and how to set up different behaviors for your games.

We are going to work with a couple of simple behaviors, which should give you a decent idea about how to structure more complex behaviors and enemies. The enemy we will be working with will have three different states which will determine its actions. First, the idle state. The enemy will be sitting still, doing nothing, until one of the other states are triggered. The second state, chase, is pretty much what it sounds like. The enemy will chase our player around for a short time before transitioning to the last state, the shoot state, where the enemy will stop moving and fire a shot at the players current position. Start by creating a new script and name it enum_init.

enum_init script

Copied to clipboard.

enum states {
    idle,
    chase,
    shoot
}

If you are unfamiliar with enums, I highly recommend checking out my blog post on state machines.

Create a new enemy object, throw a sprite on there, and add the create, step, and draw event. Add the following code.

oEnemy create event

Copied to clipboard.

state = states.idle;
actionDur = 0;
speed = 0.5
distanceTrigger = 80;

The state variable is used to manage the current state of the enemy, and the behavior based on the state. actionDur is used within a given state to determine how long that state will last, or to trigger something within the state. Speed is the movement speed of the enemy. Last but not least, distanceTrigger is used to set the enemy to the chase state based on how close it is to the player object.

oEnemy draw event

Copied to clipboard.

draw_self();
draw_circle(x,y,distanceTrigger,1);

This event is somewhat optional. Really all we are doing here is drawing the distanceTrigger around the enemy object. This is essentially debug information so you can see the radius around the enemy where it is checking for the player object.

States and Behaviors

In this section, we will define the behaviors for our enemy based on the state it is in. By default our enemy starts in the idle state, so let's set that up first. Open the step event and add the following.

oEnemey step event

Copied to clipboard.

switch (state) {
    case states.idle:
        if(distance_to_object(oPlayer) <= distanceTrigger){
            state = states.chase;
        }

        speed = 0;
        actionDur = 0;
    break;
}

The idle state is very simple. The enemy checks around itself for the oPlayer object, and once the player object is within range, it switches to the chase state. To achieve this we are using the distance_to_object function that is built-in to GameMaker. actionDur is set to zero in this event to make sure it is always zero when entering different states. Now we need to add the chase state. Add the following switch case below the idle case.

oEnemy step event

Copied to clipboard.

case states.chase:
    if(distance_to_object(oPlayer) > distanceTrigger + 10){
        state = states.idle;
    }

    speed = 1;
    direction = point_direction(x,y,oPlayer.x,oPlayer.y);

    actionDur ++;
    if(actionDur >= 120){
        state = states.shoot;
        actionDur = 0;
    }
break;

Slightly more complex than idle, but still pretty simple. First, we set a check to stop chasing the player if the player object gets too far away from the enemy. This is done by checking to see if the player object is farther away than the distanceTrigger plus 10, so a slightly larger radius than the basic distanceTrigger.

The speed is set to one, and the direction (also a built-in GameMaker variable) is set to wherever the player object is at the time. Speed and direction work together by default. If speed is not zero, and direction is set, the object will move in the direction it is pointed at the rate of the speed variable.

Finally we use actionDur to switch to the shoot state. actionDur counts up every frame until it reaches 120, at which point the state changes to shoot and actionDur is reset back to zero. In my game, which runs at 60 fps, the enemy shoots after chasing the player for two seconds. For the shoot state, add this final switch case.

oEnemy step event

Copied to clipboard.

case states.shoot:
    if(actionDur == 0){
        bullet = instance_create(x,y,oBullet);
        bullet.direction = direction;
    }

    speed = 0;

    actionDur ++;
    if(actionDur >= 60){
        state = states.idle;
        actionDur = 0;
    }
break;

This assumes you already have a oBullet object created. If you don't have that, create one and add speed = 2 to the create event.

To ensure the enemy only creates one bullet, we check to see if actionDur is set to zero, meaning it hasn't counted up yet, and create a bullet. The bullet direction is set to the same direction of the enemy that is creating it. We zero out the enemy speed so that it stops moving to shoot. This is mostly for debug purposes so we can tell for sure that the enemy has entered this state. Finally we count up the actionDur just as we did in the chase event. Once actionDur reaches 60 (one second if your game runs at 60 fps) the enemy resets to the idle state.

Resources