Finite state machine and messaging

A finite state machine is a computation model consisting in a finite set of states, from which can only be in one at a time. Transitions between them are produced under certain conditions.

Game characters' behavior is usually modelled by a series of states and transitions. A clear example is a spy game where enemies patrol all over the map under apparent calm. Suddenly, one of them hears a noise and turns into track state. Seconds later, the alarm is raised because the spy has been spotted, so all nearby enemies enter into alert state. In conclusion, they are no more than a finite state machine capable of communicating with other agents.

This recipe illustrates a new episode of the caveman and the dinosaur whose interaction will be textually outputted to the console.

Getting ready

As a Libgdx extension, the Artificial Intelligence library must be added as a dependency to the build.gradle file. Go back to Chapter 1, Diving into Libgdx, if you need to learn how to carry out it.

The code resides in several files:

  • Caveman.java: This file contains the caveman's attributes, states, and methods
  • Dinosaur.java: This file contains the dinosaur's attributes, states, and methods
  • MessageType.java: This file contains all the possible messages between two agents in the game
  • ArtificialIntelligenceSample.java: This file makes use of the former files leaving the dinosaur and the caveman interact within the render loop

Notice that the first three files are located within the com.cookbook.ai package.

How to do it…

State machines provide a high degree of freedom in order to create an unreadable monster, so if you plan to have a lot of transitions between states, you should consider not using this approach (an alternative is provided in the There's more... section of this recipe).

As it might be tedious and repetitive for you, only a few states from the caveman will be shown. You can find the rest in the source files. The real goal is to understand the whole set of tools provided by the Libgdx AI extension.

The hardest part in this recipe is to create the agents that will interact between them because once this is done, their use is pretty straightforward.

Creating the agents

Every entity in your game that is capable of sending/receiving a message to/from another entity must implement the Agent interface, so we already have where to begin with. Perform the following steps:

  1. Define the Caveman class by implementing the Agent interface and make it contain a finite state machine and several properties:
    public class Caveman implements Agent {
    
    private StateMachine<Caveman> fsm;
       private float hungry; // [0-100]
       private float energy; // [0-100]
       private boolean threatened;
       ...

    The StateMachine interface offers a complete API to navigate through all the states at any order. The method names are self-explanatory:

    • void changeState(State<E> newState)
    • void revertToPreviousState()
    • void setInitialState(State<E> state)
    • State<E> getCurrentState()
    • State<E> getPreviousState()
    • boolean isInState(State<E> state)
  2. Create an enum to define some states that the caveman can come into. Every state will implement the State interface, and consequently, it must override four methods:
    • enter(…): This is implicitly called when the agent enters into State
    • update(…): This will contain the logic of State and will be called explicitly by the programmer
    • exit(…): This is implicitly called when the agent leaves State
    • onMessage(…): This is called to deal with messages while running State, so you can give a specific response

    The sequential call order for an Agent to change from state A to state B would be:

    Creating the agents

    The resulting code would be something like this:

    public enum CavemanState implements State<Caveman> {
    SLEEP() {
    public void enter(Caveman caveman) {
          caveman.say("Good night!");
    }
    
    public void update(Caveman caveman) {
    caveman.increaseEnergy(.08f);
    caveman.increaseHungry(.01f);
                
    if(caveman.energy==100) {
    caveman.say("(yaaaaawn) I'm a new man");
    caveman.getFSM().changeState(IDLE);
          }
    }
    
    public void exit(Caveman caveman) {}
    
    public boolean onMessage(Telegram telegram) {
       return false;
    }
    },
    ... //Next state
    }
  3. The StateMachine interface can also have a global state that will be executed every time update is called:
    GLOBAL_STATE() {
       public void enter(Caveman caveman) {}
    
    public void update(Caveman caveman) {
    // 1 in 1000
    if (MathUtils.randomBoolean(0.001f) && 
       !caveman.getFSM().isInState(PAINTING)) {
    caveman.say("OH! Gotta pee");
    caveman.getFSM().changeState(PAINTING);
    }
       }
    
    public void exit(Caveman caveman) {}
    
    public boolean onMessage(Telegram telegram) {
    return false;
    }
    },
    ... // other states

    In addition, StateMachine can handle messages in a general way through its boolean handleMessage(Telegram telegram) method. False is returned if the telegram has not been successfully handled, so it is routed to the current state handler and if the same happens, it arrives to the global state handler.

  4. Once all the states are defined, just focus on Caveman itself by writing your custom methods. A possible constructor might be:
    public Caveman() {
    fsm = new DefaultStateMachine<Caveman> (this, CavemanState.IDLE);
       
    hungry = MathUtils.random(0, 100);
    energy = MathUtils.random(0, 100);
    threatened = false;
       
    fsm.setGlobalState(CavemanState.GLOBAL_STATE);
    }

    As you can observe, IDLE would be the initial CavemanState.

  5. The caveman likes eating a lot so he needs to go hunting very often. However, an unfriendly dinosaur might stand in his way and growl loudly. Consequently, we should prepare our caveman to act fast and run home:
    HUNTING() {
    ...
    @Override
    public boolean onMessage(Telegram telegram) {
    if (telegram.message == MessageType.GRRRRRRRR) {
          Caveman caveman = (Caveman)(telegram.receiver);
    caveman.threatened = true;
          caveman.getFSM().changeState(RUN_TO_HOME);
    
          return true;
    }
    return false;
       }
    }

    Notice that we have introduced a new element, MessageType.GRRRRRRRR. It is one of the possible messages defined in the MessageType.java file, which would look as follows:

    public class MessageType {
    public static final int GRRRRRRRR = 0;
    }

    Then, the dinosaur would just have to call this code:

    MessageDispatcher.getInstance().dispatchMessage ( 
    0.0f, // no delay
    dinosaur, dinosaur.caveman, MessageType.GRRRRRRRR, null);

    The MessageDispatcher class is just a singleton class in charge of sending telegrams from an Agent to another in a specific delay time. The last parameter is expected to be an Object and allows some extra information to be included.

Usage example

After some tedious work of designing your finite state machines, it is time to make use of them. Perform the following steps:

  1. First of all, declare the dinosaur and the caveman as agents:
    public class ArtificialIntelligenceSample extends GdxSample {
       ...
    private Agent caveman, dinosaur;
  2. Initialize them:
    public void create () {
    super.create();
    ...
       caveman = new Caveman();
    dinosaur = new Dinosaur((Caveman) caveman);
    }
  3. Update the agents and, therefore, both the state machines:
    public void render () {
       ...
       float delta = Gdx.graphics.getDeltaTime();
    caveman.update(delta);
    dinosaur.update(delta);
    }

How it works…

Although for the sake of simplicity, the whole example has not been encompassed, the next diagram illustrates all the possible transitions between states except for the aforementioned case where the GRRRRRRRR message goes into action. It is only sent if the caveman is in a HUNTING state while the dinosaur is in a GO_FOR_A_WALK state and also a luck component (only happens 1/1000 times).

How it works…

There's more…

It is not necessary to be a genius to realize the potential mess of state machines as they proliferate. Adding the concept of substate would be a step forward in this aspect, so we get to the commonly known as hierarchical finite state machines. However, this might not be enough and changing the approach to behavior trees would fit our needs.

This is a more restrictive idea but, at the same time, more structured as it includes control nodes all along its directed acyclic graph organization keeping the natural hierarchical order from trees. This contributes to reusability and good memory management.

Explaining behavior trees is not the goal of this recipe but if it piqued your curiosity, do not hesitate to search for it on the Internet because there are a lot of good resources such as this series of posts by Bjoern Knafla at http://www.altdev.co/2011/02/24/introduction-to-behavior-trees/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.170.63