Designing a simple AI with a Finite State Machine

4 minute read

The preamble goes; we’re making a game where you fly planes and shoot down other planes. The project, for which we have no name, is made in Unity, and is a modern take on old games like “Sopwith” or “Triplane Turmoil”. A new take in that we have 3D models in a 2D perspective, or 2.5D if you will.

As we decided to scrap online multiplayer, we lost a very important aspect of our dev test loops: dogfighting! We used to connect locally or from each others homes and fight it out in our ugly planes. Without that capability, testing fell short of anything but basic controls and weapon systems. And so we sat down to assemble a simple AI (artificial intelligence). We are all of us intrigued to test something out with behavior trees, but for the first iteration an AI based on a Finite State Machine would do the trick quite nicely. To the uninitiated, an arbitrarily tailored excerpt from Wikipedia defines a Finite State Machine as:

{…} is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some external inputs; the change from one state to another is called a transition

We had little experience of state machines but some of simple game AI, although only considered intelligence by a generous definition. If abstractions and excerpts such as the one above bore you to death, there is a fun video at the end of the article!

Identifying the states

Starting of, we decided what the AI must be able to do, in a limited sense of gameplay, and agreed on the following features:

  • Follow a unit
  • Pursuit an enemy
  • Avoid ground
  • Patrol an area

By the states defined we have an enemy with enough features to keep us entertained during testing, and allow us to start working on some actual game modes. Even though the states sound simple, it tells nothing of the logic implied. In this system, the AI can fly by himself, or be part of a squadron. The squadron does not affect the states much (except for “Follow a unit” which was created for that purpose alone) but is ever present in the implementation.

To identify your states, you should try to conceptualize your features as behaviors that can be pursued with variable effort, and how to start and stop (or pause and resume) them.

Defining the transitions

Further, we figured the most important things to consider about our state machine was how to enter and how to exit a state. Before any logic was applied to any state, we wrote the transitions from and to the state to make sure we would not get stuck and that the state machine is finite. This is not difficult, but we recommend to do this with someone else as it may help to cover all the angles if you work it out together. The transitions will be very much dependent on your type of game, your logic, and your states defined. For the sake of it, let us look at Pursuing an enemy in isolation. Note that this diagram does not model the full state machine, for the sake of simplicity.

Foo
State flow from pursuit state perspective.

The state is triggered when the player (you, soon) enters the enemy units, lets call him Mark, “sphere of awareness”. The state can also be triggered by Mark being hit by a bullet. It then magically assigns the player as target and the chase begins. Mark chases the player by constantly trying to diminish the delta angle of himself and the player, and fire its weapon if he is close enough.

The exit transition is triggered by the following events:

  • Avoid ground. Or as we call it in code, Darwinism. Mark detects that he is on collision course, drops everything and steer the shortest path to safety. Since Mark is not a complete tool, he resumes his paused state as soon as he is out of harms way.
  • If Mark is part of a “squadron”, and the other plane is closer by much, the other plane takes over and Mark is made follower.
  • Death. Yeah.

Considering flow

By the means mentioned above, entering and exiting states should be easy enough. It is important however to pay attention to the flow as not to get stuck in any miserable endless circular states. In our case, a switch loop in Marks update loop handles all the states, and would look something along the lines of the following:

void Update(){
  switch(state){
    case State.PURSUIT:
      CheckForDanger();
      Chase(target, true);
      break;

    case State.DARWINISM:
      var angle = GetDeltaAngle(target);
      SteerClear(angle);
      if(!IsOnCollisionCourse) state = pausedState;
      break;

    case State.FOLLOW:
      CheckForDanger();
      Chase(squadron.leader);
      EvaluateSquadron();
      break;

    case State.PATROL:
      CheckForDanger();
      FlyTo(route.goingTo);
      PatrolManagement(20);
      break;
  }

Note how “Darwinism” (avoid ground) is prioritized above any other state, and cannot be exited without being out of harms way! This is our means of managing the flow and making sure the AI is being reasonably capable and yet simple.

As you might have seen and may agree with us on, state machines are reasonable models for this purpose. However, with more states to your system, a state machine, as any model, can become cumbersome. Further, keeping few states with a lot of intricate logic can also make a cumbersome model that will make it difficult to debug. We learned during this build that state machines makes it brilliantly simple to think about your AI in terms of “boxes”, and transfer these boxes to a switch and apply logic! For a next iteration of game AI however, we are eager to try out a behavioral tree.

We are by no means experienced in this field, and we might have missed something obvious, but this structure works very well for the intended purpose.

You can check out some of it in this video:

Leave a Comment