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.
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 methodsDinosaur.java
: This file contains the dinosaur's attributes, states, and methodsMessageType.java
: This file contains all the possible messages between two agents in the gameArtificialIntelligenceSample.java
: This file makes use of the former files leaving the dinosaur and the caveman interact within the render loopNotice that the first three files are located within the com.cookbook.ai
package.
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.
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:
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)
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 programmerexit(…)
: 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 responseThe sequential call order for an Agent
to change from state A to state B would be:
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 }
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.
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
.
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.
After some tedious work of designing your finite state machines, it is time to make use of them. Perform the following steps:
public class ArtificialIntelligenceSample extends GdxSample { ... private Agent caveman, dinosaur;
public void create () { super.create(); ... caveman = new Caveman(); dinosaur = new Dinosaur((Caveman) caveman); }
public void render () { ... float delta = Gdx.graphics.getDeltaTime(); caveman.update(delta); dinosaur.update(delta); }
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).
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/.
18.223.170.63