Hello Guest, please login or register.
Did you miss your activation email?
Login with username, password and session length.

Pages: [1]   Go Down

Author Topic: [Tutorial] How to implement states in GML.  (Read 6752 times)

0 Members and 1 Guest are viewing this topic.
[Tutorial] How to implement states in GML.
« on: January 05, 2012, 06:37:28 pm »
  • *
  • Reputation: +9/-0
  • Offline Offline
  • Gender: Male
  • Posts: 3725
Finite State Machines are a common used principle in games and applications. And most of you have a basic grasp of them. However I have often seen them implemented incorrectly. For each programming language it is different and even within a programming language there are several methods of implementing decent to good FSMs. This tutorial will focus on GML for implementing FSMs.
 
First and foremost thing to know about states and finite state machines in any language is:
1) A state is always defined by 1 attribute (variable) and not more.
2) The values of attributes (member variables) do NOT define the state, but the state DEFINES the values of attributes.
3) State valid values for the attributes are set when entering the state, not during an update cycle.
This means that checking several numerical or boolean variables like "jumping", "falling", "isRunning", "facing" in several if statements is not using states.

There are several ways to implement states in GML. I will give three methods on how to use states. All three of them will have the object in question have an attribute called "state". This attribute will thus hold the value for the state and this is the only variable that will be checked to determine the code that needs to be executed during the event/cycle/frame.


Method 1: Numerical or String
Okay, this is my least favorite method and not one I would recommend for games with some complexity. The 'state' variable will hold a value that is an string or an integer. For examples look at the code below:
String:
Code: [Select]
state = "jumping";
Integer:
Code: [Select]
state = 1;
Note that when using integers as your state variable it is better to have some constants defined such as 'JUMPING'. This will make the code more understandable for yourself and others. I will be using constants in the rest of the examples.

During the events you then check the 'state' variable's value with a switch or if-else statements. This will allow the event to execute the right code for that condition.
Code: [Select]
switch( state )
{
case IDLE:
//do the idle <event> code
break;
case WALKING:
//do the walking <event> code
break;
case JUMPING:
//do the jumping <event> code
break;
default:
//you are in an error state
//do the error handling code
break;
}

//OR

if( state == IDLE )
{
//do the idle <event> code
}
else if( state == WALKING )
{
//do the walking <event> code
}
else if( state == JUMPING )
{
//do the jumping <event> code
}
else
{
//you are in an error state
//do the error handling code
}

It is also good practice to use one event to check if a transition to another state has to occur. A Step or Draw event is something that is done every cycle and often the transitions are an animation end, distance to another object or a timer timeout, these events are good choices. You can also implement the checks in different events, but it is harder to assess whether you have implemented it properly. Some might argue that putting the checks in the events, such as Draw, Key Release and Animation End, that associate with them is more sensible.

Whatever your choice, the checks are most often done in an if (- else if) statement and when true the 'state' attribute value is set. A common mistake however is that only the 'state' attribute is set. Remember, the state DEFINES the values of the attributes and NOT the value of the attributes defines the state. Thus when setting the state you should also set the values of all the attributes of the object to a for the state valid value. However some attributes do not matter or should not be changed at all like the position of the object. These do not have to be considered. Some attributes can have a valid range and thus the value needs to be checked if it is within the valid range.
Code: [Select]
if( key_pressed('A') )
{
state = JUMPING;
sprite_index = player_jump;
image_speed = 0.5;
attribute1 = 6;
if(attribute2 < 3)
{
attribute2 = 3;
}
else if(attribute2 > 9)
{
attribute2 = 9;
}
//else attribute2 keeps its value
}

It is important that the values of the attributes are set to state valid values when the state changes. Failing to do so might or even will cause errors when executing the state specific code. It is also propper to execute state specific code when exiting the state. This is cleaning up, because each state only knows what happens within himself and does not know what happens in other states. However this is not absolutely necessary as it can be safe to assume that the next state will set the right values for the variables. But when the state has created an extra object and the object should not have a lifecycle beyond the state it is propper to destroy the object when exiting.


Method 2: Scripts
The characteristic of the first method is that all the code for each state is listed in a event. When the game is simple and there are only a small number of states the listing won't get long and that method is good. However when the game gets more complex and the number of states grows, so does the code listing in the event. You need to scroll more and it becomes much harder to oversee what you have implemented. For others it also because a bit more difficult to read.

Game Maker offers a solution to this by separating code into Scripts. Instead of having the 'state' attribute hold a numerical or a string value the attribute holds a Script value. Even the object no longer needs to check the state it is in during the event code and with a simple command the state specific can be executed. And scripts are capable of accessing the attributes of the object it is called from like it is part of the event code. The following code shows how to initialize the state:
Code: Text
  1. state = scrIdle;  // scrIdle is the name of the script when the state is IDLE
  2.  

The disadvantage is that a script does not automatically know from which event it is called like the event code. Game Maker provides some functions to check which event in the gamecycle currently is active. However each script can have up to 16 arguments to provide some additional information. Thus it is a simple matter to provide an event identifier as the first argument. To make the code readable it is propper to use constants for event identifiers when possible. The code with the object is then as follows:
Code: Text
  1. if(state != 0 && script_exists(state))
  2. {
  3.         script_execute( state, <event>, ... ); //... are any additional arguments for this event, such as locally calculate values.
  4. }
  5. else
  6. {
  7.         //you are in an error state.
  8.         //do the error handling.
  9. }
  10.  
The "script_execute" command is also available in the Lite version of GameMaker and can have up to 16 arguments besides the script argument.
Code: Text
  1. switch(argument0)
  2. {
  3. case STEP:
  4.         //do step event code for the state;
  5.         break;
  6. case DRAW:
  7.         //do draw event code for the state;
  8.         break;
  9. case KEYPRESS:
  10.         //do keypress event code for the state;
  11.         break;
  12. case ENTER:
  13.         //In this case the state of the object has transitioned to this script.
  14.         //Set all the attributes to the correct values.
  15.         break;
  16. default:
  17.         //Error, an unknown event has been passed.
  18.         //handle the error or let it slide
  19.         break;
  20. }
  21.  
  22. //OR
  23.  
  24. if(argument0 == STEP)
  25. {
  26.         //do step event code for the state;
  27. }
  28. else if(argument0 == DRAW)
  29. {
  30.         //do draw event code for the state;
  31. }
  32. else if(argument0 == KEYPRESS)
  33. {
  34.         //do keypress event code for the state;
  35. }
  36. else if(argument0 == ENTER)
  37. {
  38.         //In this case the the state of the object has transitioned to this script.
  39.         //Set all the attributes to the correct values.
  40. }
  41. else
  42. {
  43.         //Error, an unknown event has been passed.
  44.         //handle the error or let it slide
  45. }
  46.  

Note that the example has a special event called "ENTER". This is a state machine specific event which does not occur in Game Maker. The purpose of the event is to set all the attributes of the object to state valid values, when the state of the object transitions to this particular script. A similar event is when the state transitions and the script has clean up for "EXIT"ing the state. The following example shows two methods for changing the state:
Code: Text
  1. if( key_pressed(&#39;A&#39;) )
  2. {
  3.         if(state != 0 && script_exists(state))
  4.         {
  5.                 script_execute(state, EXIT);
  6.         }
  7.         //else an error
  8.         state = scrJump;
  9.         if(state != 0 && script_exists(state))
  10.         {
  11.                 script_execute(state, ENTER);
  12.         }
  13.         //else an error
  14. }
  15.  
  16. //OR nr2
  17.  
  18. if( key_pressed(&#39;A&#39;) )
  19. {
  20.         if(state != 0 && script_exists(state))
  21.         {
  22.                 script_execute(state, EXIT);
  23.         }
  24.         //else an error
  25.         if(script_exists(scrJump))
  26.         {
  27.                 scrJump(ENTER);
  28.         }
  29.         //else an error
  30. }
  31.  
Code: Text
  1. ...
  2. case ENTER:
  3.         sprite_index = player_jump;
  4.         image_speed = 0.5;
  5.         attribute1 = 6;
  6.         if(attribute2 < 3)
  7.         {
  8.                 attribute2 = 3;
  9.         }
  10.         else if(attribute2 > 9)
  11.         {
  12.                 attribute2 = 9;
  13.         }
  14.         //else attribute2 keeps its value
  15.         break;
  16. case EXIT:
  17.         image_speed = 0;
  18.         attribute1 =0;
  19.         break;
  20. ...
  21.  
  22. //OR nr2
  23.  
  24. ...
  25. case ENTER:
  26.         state = scrJump;
  27.         sprite_index = player_jump;
  28.         image_speed = 0.5;
  29.         attribute1 = 6;
  30.         if(attribute2 < 3)
  31.         {
  32.                 attribute2 = 3;
  33.         }
  34.         else if(attribute2 > 9)
  35.         {
  36.                 attribute2 = 9;
  37.         }
  38.         //else attribute2 keeps its value
  39.         break;
  40. case EXIT:
  41.         state = 0; //I know it is an error state, but it forces the next script to set a propper state
  42.         image_speed = 0;
  43.         attribute1 =0;
  44.         break;
  45. ...
  46.  

Like with the first method of numerical/string values the attributes have to be set to values that are for the state valid. Just changing the script is not enough. And the best place is within the script that is also the state.


Method 3: Array of scripts
The 2nd method has the advantage over the 1st method that the code for each state is separated in their own listings and thus far more easy to debug then when everything is put in the event code. However the check for what state the object is in during the event code of the 1st method is swapped by the check for the event that the game is in during the script code of the 2nd method. This check can also be eliminated when the state uses an array of scripts.

Okay this goes a bit against the requirement that the state is only a single attribute, because an array is a container of the same type attributes. However it has more advantages. So let me explain. The 'state' attribute is container for several script values instead of just one. The first element of the container is still the state identifier, which will be referred to as ID (value = 0). The rest of the scripts can be accessed using the event identifier (starting with value 1 and going up). Thus the code in a single script only contains the code that is needed for that state during that event. Setting it up is done as the following example:
Code: Text
  1. state[ID] = scrIdle;
  2. state[STEP] = scrIdle_step;
  3. state[DRAW] = scrIdle_draw;
  4. state[KEYPRESS] = scrIdle_keypress;
  5. [/event]
  6.  
  7. Note that the code for entering and exiting the state is put in the script under ID. Executing the state specific code for an event is similar as in the 2nd method, but no event identifier needs to be provided.
  8. [code=event]
  9. if(state[<event>] == 0)
  10. {
  11.         //no code or a default needs to be executed.
  12. }
  13. else if(script_exists(state[<event>]))
  14. {
  15.         script_execute( state[<event>], ... ); //... are any additional arguments for this event, such as locally calculate values.
  16. }
  17. else
  18. {
  19.         //you are in an error state.
  20.         //do the error handling.
  21. }
  22.  
Code: Text
  1. //do all the script code.
  2.  

A state transitions is also similar to that of the 2nd method, however the initialization needs to do a bit more.
Code: Text
  1. if( key_pressed(&#39;A&#39;) )
  2. {
  3.         if(state[ID] != 0 && script_exists(state[ID]))
  4.         {
  5.                 script_execute(state[ID], EXIT);
  6.         }
  7.         //else an error
  8.         if(script_exists(scrJump))
  9.         {
  10.                 scrJump(ENTER);
  11.         }
  12.         //else an error
  13. }
  14.  
Code: Text
  1. if(argument0 == ENTER)
  2. {
  3.         state[ID] = scrJump;
  4.         state[STEP] = scrJump_step;
  5.         state[DRAW] = scrIdle_draw;
  6.         state[KEYPRESS] = scrJump_keypress;
  7.         sprite_index = player_jump;
  8.         image_speed = 0.5;
  9.         attribute1 = 6;
  10.         if(attribute2 < 3)
  11.         {
  12.                 attribute2 = 3;
  13.         }
  14.         else if(attribute2 > 9)
  15.         {
  16.                 attribute2 = 9;
  17.         }
  18.         //else attribute2 keeps its value
  19. }
  20. if(argument0 == EXIT)
  21. {
  22.         state[ID] = 0; //I know it is an error state, but it forces the next script to set a propper state
  23.         state[STEP] = 0;
  24.         state[DRAW] = 0;
  25.         state[KEYPRESS] = 0;
  26.         image_speed = 0;
  27.         attribute1 =0;
  28. }
  29.  
Note that during the "ENTER" the script that is set for the DRAW event is called scrIdle_draw. This is another advantage of this method as it allows states to reuse scripts when two states have exactly the same code for an event. However if there is in the code one token difference like a '+' changed into a '-' then a new script still has to be created. This method can rank up the number of scripts and if you do not organize them well it can be a confusing mess.
 
« Last Edit: January 07, 2012, 11:03:54 am by Niek »
Logged
Re: [Tutorial] How to implement states in GML.
« Reply #1 on: January 05, 2012, 06:39:17 pm »
  • *
  • Reputation: +9/-0
  • Offline Offline
  • Gender: Male
  • Posts: 3725
Well, it isn't my best work. However I hope it helps some people. Maybe I'll add some actual examples later. Now I'm tired.
« Last Edit: January 05, 2012, 06:40:48 pm by Niek »
Logged
Pages: [1]   Go Up

 


Contact Us | Legal | Advertise Here
2013 © ZFGC, All Rights Reserved



Page created in 0.065 seconds with 38 queries.

anything