Welcome to another one of our Coffee-break Tutorials! We hope you have 10 minutes of your time to dedicate to reading this, as today we'll be discussing a very important subject that is applicable to just about every game in some shape or form... Finite State Machines.
NOTE: If you prefer to use GML instead of Drag and Drop, then you should go and read the companion tutorial here instead of continuing.
If you've been programming or hanging around in-game dev circles for even a small amount of time then you'll have heard of this term, and it simply refers to a method of programming that uses sequential "states" to perform tasks in a logical form. A real-life example of a state machine would be making some pasta, for example:
For a more precise explanation of state machines that doesn't make you hungry, you might want to read this article, but hopefully, you get the idea!
State machines are incredibly useful for many things. most notably AI in games, but that's a very complex subject and since this is only a quick tutorial to get you introduced to the idea, we'll be using a finite state machine to make modular buttons for a menu. For this, you'll need to get a copy of the Brick Breaker demo as well as some extra assets that we'll be adding into it. You can get both of these from the links below:
Once you've downloaded the files linked above, import the YYZ into GameMaker Studio 2 and then unzip the assets to a folder somewhere safe. If you run the Brick Breaker demo as it is, you'll see that we are presented with a black screen and some text asking you to press fire to play the game. This isn't very user friendly or pleasant to look at, so let's add in some buttons to Play the game or to Quit the game.
to start with, you need to add the button sprites that we'll be using, and you can do this by simply dragging them from the assets folder and dropping them on the IDE. They have been saved with the "_stripXX" suffix to their name, so they should be split into the correct sub-images automatically:
Make sure to set the origin for the sprite to the "middle-center".
While we're at it, let's just add the sounds we'll be using too. These can also be dragged from the asset folder and dropped on the IDE, and will create the required sound resources for you:
Finally, we are going to make three objects. The first will be a parent object for all buttons in the game, and the second and third will be the Play and Quit buttons respectively. So go ahead and make a new object called
oButton_Parentand then create two more called
oButton_Quit. You should make the Play and Quit buttons children of the parent object too:
This is important, as we want to make everything as modular as possible, so all our state code will go into the parent object and we will only need to add one or two extra lines in each button to modify the behaviour according to the type of button that it is.
Now we have everything set up, we need to edit the current object that we have for the title -
oTitle - so that it will create the buttons and also stop drawing text. We'll also use this object to create
macros to hold the different states (for information on macros, please see here. We could use strings or even integers, but I find macros (or even
enums) make the code a lot clearer and easier to edit if required later.
So, open up
oTitle and the first thing we'll do is remove the Global Left Released event, as we won't be needing that anymore (use the Right Mouse button on the event and select delete). Now open the Draw event and remove all the actions except
Set Draw Colour and
Draw Rectangle. Again, we only need this object to clear the screen, but we don't want it to draw any text.
With that done, open up the Create event. Here we'll be adding in the following actions after the ones that are already there:
This code is creating the different macro states that the buttons can be in as well as creating the buttons themselves. You can close this object now, as most of the rest of the code we need to add will be done in the object
oButton_Parentwhich you should open.
In this object we need to add a Create Event and give it the following set of actions:
This sets the initial state for all the buttons as well as a variable that we'll use to control what happens when a button is pressed.
With that done, we need to add a Step Event. This event will handle the different button states, and we'll be doing that using a
Switch statement, where each
Case will be a different state. We can prepare this now by adding the following DnD™:
These are all the different states that a button can have and we'll be adding actions into each of them to deal with effects and user input. Each of the sections below outlines the actions required for each state.
In the Create Event of the parent object we set the image X and Y scales to 0, as we want the button to "grow" to full size when it is created. For that we'll use the following set of actions in the state
All this does is increment the size of the button until it is 1, at which point it changes state to the "normal" one.
The "normal" state is where the button just sits and waits for the user to do something. The following actions for this state go in
Here we are checking the size of the button and if it's greater than 1 then we reduce it (this is because in the "over" state we'll make the button grow a little), and we're also checking to see if the mouse goes over the button, in which case we'll switch states again to the "over" state and set the
When the mouse goes over the button the state changes to the overstate, so we need to add this code into
The start of this code first checks to see if the mouse is no longer over the instance, in which case it sets the state back to the "normal" state again and also sets the
image_index back to 0. If the mouse is still over the instance, then we scale it up a bit for effect and also check to see if the player presses the mouse button. If the button is pressed we change state again, this time to the "pressed" state and also set the
The penultimate state is a bit more interesting than the previous ones as it's here that we'll be doing a specific task based on the button that is actually being pressed. Let's look at the actions we'll need and see how we'll do that... add this into
Again, we're messing a little with the image scale here just to make the buttons a bit more dynamic, but you'll notice that we're also calling
event_user(0). This is the first of the available User Defined Events that are available from the Other object event category. Now, we could use a script here instead, but I find the user events, in this case, to be very useful as they can be added to each object individually meaning that you never have to go looking for what something does. In this case, the Play and Quit objects will both get User Events to define how they will react when they are pressed.
This state is for when the button object is being destroyed and will be set when a button is pressed. The following actions should go in the
So, here we make the button scale down to 0 so it "disappears" and then we call another user event, this time User Event 0. This event will be used to do something when the button is destroyed, for example, say the button is for the Game Options, then in this event of the button object we would add in code to create the Game Options objects. In the case of our small demo project, we'll be using the event to either start the game or quit the game, depending on which button has been pressed.
Our state machine is set up for the button objects, so we now only have to add in the User Events to each specific button to ensure that everything works as it should. To start with we'll deal with the Play button, so open
oButton_Play now and add a User Event 0 to it (from Events > Other). In this event we will add the following DnD™:
We are flagging the button as being pressed, then playing a sound unique to this button object, before using the
Apply To...action to tell ALL button objects to change to the "destroy" state. We are doing this because we don't need the buttons anymore as we are going to change rooms and start playing the game, but you can do all sorts of other things in this event. For example, this could be a "Sound" button, and in this event, you could be toggling a global controller variable to switch sound on or off in your game (in which case you wouldn't need to set the destroy state), or it could be a fullscreen/windowed toggle or anything really!
Now add a User Event 1 event. This is the event that is called from the "destroy" state, and in it add this:
Here we check the
pressed variable and then change rooms if this button has been pressed (the variable is
true). We do this because the destroy state can be called for multiple buttons at once, and we need to ensure that only the code for the button that has been pressed is run.
Now open the object
oButton_Quitand add a User Event 0 and a User event 1 with the following actions in the appropriate events:
User Event 0
User Event 1
As you can see, the code follows the same pattern as before and it should be obvious to you by now what it does.
With that done, all that's left is to run the game! We don't need to add anything to any rooms as we already set up the
oTitle controller to create the buttons at the start of this tech blog, so when you run the game you should be presented with two buttons to Play or Quit the game, and they should respond to being mouse over by scaling up, and they should react correctly to being pressed, either by starting the game or by quitting it.
This tutorial is a little longer than others, but we hope it's been worth your while going through it and that you now have a good idea of how Finite State Machines work. The important points to take away from this are the use of
enums to name your "states" (to make it easier to identify them), and the use of a parent object - where possible - for the "state machine", only adding code specific to the child objects in the child objects using scripts or user events.
Note that for this example we've made menu buttons, but as mentioned at the start of this blog, you can use state machines for a multitude of tasks, and there's really no limit to the number of states that one can have. You just have to ensure that there is a coherent and logical "path" through the states when you are planning it. You might want to even start with a pen and paper and design a basic flow-chart to get the state progression worked out before starting in on the programming!
For those of you that just want to see what all of the above does without adding it all into a project, you can get a version of the Ball Breaker demo with the Finite State Machine added from the link below:
That's it for another Coffee-break Tutorial! Happy GameMaking!