Author: Mark Alexander
- Create and use an mp_grid
- Create dynamic paths
- Create a basic AI
- Snap instances to a grid when placing them in a game
This short tutorial will show you how to create an use an mp_grid and a dynamic path resource to create some simple (but very effective!) artificial intelligence. We are going to make a very basic Tower Defense style demo, where you can place blocks anywhere in the room and the "enemy" instances will avoid them, all the while following a path to a specific goal point. Note that although this demo shows a Tower Defense game, the techniques applied here can be used in almost any top-down game where reactive AI is required.
NOTE: Since this tutorial uses dynamic resources that are added at run-time, you will need the Standard or Professional version of GameMaker: Studio.
Import the TD_AI.gmz into GameMaker: Studio (get it from the link at the top of the page) and run it once. As you can see, the game is not very inspired yet - all we have is an enemy that moves from one point to another in a straight line across the screen.
We need to spice things up a bit and have our enemy change direction and react dynamically to things that the player does, and in this case we are going to have it change direction and avoid blocks that the player adds into the room while playing. For this we are going to use a very powerful tool from the GameMaker: Studio arsenal: mp_grids.
An mp_grid is a "motion planning grid", and all it does is section up a room into individual grid "squares", and each of these squares can then be flagged as "occupied" or not. This grid is then used by another mp_* function to create a unique path resource that will go around those squares flagged as "occupied" and go through those ones that are not. you would then assign this path to an instance and (to the player) it will look like the instance is displaying "intelligence" as it neatly avoids obstacles while following the path.
Creating The Mp_grid
Let's get started then! Open up the object "obj_Control" and open the Create Event code block now. Here we are going to create our mp_grid and a single path too. Both the grid and the path will be created and have their ID stored in a global scope variable - we use global variables since we only need one single path and one grid for all instances to use. In general it's good practice when using mp_grids to only ever create one and have instances access that, since creating and using mp_grids is a very processor intensive task.
So, add the following into the Create Event now:
With this code, we are creating a grid that is 40 x 30 cells in size (we divide the room width and height by 16 to get the number of cells since our "base" block size in the game is 16) and we are assigning it's ID to a global variable. You should note here that since mp_grids are quite resource heavy, you should never make the grid smaller than is absolutely necessary - the smaller the cell size, the more processing it requires and the more possible it is that your game will lag later.
We also make another global variable and assign a new dynamic path ID to that. We call this a dynamic path, since it is one that will be created dynamically and change throughout the game (rather than a pre-defined path that is created in GameMaker: Studio Path Editor).
After that we then add the wall instances into the mp_grid. All this function does is loop through all instances of the object "obj_Wall" in the room, and then use their position to "flag" a cell in the mp_grid. These "flagged" cells will be the ones that we want the enemy instances to avoid.
Notice that we have a custom script in there at the end and it's giving us a syntax error. This is because we haven't defined it yet! This script is going to be used to calculate the global path every time we change anything in the room, and we call it in this event to set up the initial path for the AI instances to follow when they are created.
The Path Script
We now need to make our script for creating the path that the enemy instances are going to follow, so make a new script resource and call it "scr_Define_Path". Now copy the following code:
This code gets the start and finish coordinates for our path and then uses them in conjunction with the function mp_grid_path(). This function will calculate a path between the two given points and returns true if one is found (ie: no obstacles "flagged" in the grid block it) or it will return false if none is found. Note that if the function returns true, it also creates the path for you and assigns it to the path resource that you use as argument1 (in this case the global path we defined at the start).
So, our code first tries to create a path through the mp_grid, and if none is found, it tells us with a message (you would normally have some "failsafe" code in there to catch this and deal with it, but for our tutorial, a simple message is fine). However if one is found, it sets the path type (1 to make a "smooth" path), and sets the precision to 8 to make the path as smooth as possible. this step isn't really necessary, but I find that it gives the path a better "feel".
Our script returns true or false depending on the outcome of the path creation check, which we will use as an additional check later in our tutorial game
Debugging The AI
When working with mp_grids and paths, it is often important to be able to see exactly what cells have been flagged as occupied, as well as show the path that is being created. For that we have some special draw functions, which we are now going to add into our controller object to give a visual clue as to what exactly is going on. This code can be removed later (and should be, as it is one of the slowest functions in GameMaker: Studio, which is why it is only for debugging).
In the object "obj_Control" add a Draw Event now with the following code:
This code simply draws each cell of the mp_grid as either red (flagged as occupied) or green (flagged as open), then draws some lines to better define the grid, before finally drawing the path.
You can run the tutorial game again now, and while the behavior of the enemy instance hasn't changed, you can see the mp_grid cells that the walls fall into have been flagged as occupied (red) and you can also see the path drawn from the start to the finish.
There is one very important thing to do to now - tidy up our mp_grid and path! Whenever you create a dynamic resource like an mp_grid or a path, it takes a chunk of system memory to store it's information. If you do not delete these resources from your game, then they can quickly take over more and more memory which will eventually cause your game to lag and finally crash. To prevent that we need to add a Room End Event to our object "obj_Control" with the following:
This frees up the memory associated with these resources and you should always make sure that anything that you create dynamically in your games has the equivalent clean up code.
We have one final block of code to add into our controller, and that's the code to create the wall objects that our player can place into the room to change the path. For that we need to use the Global Right Mouse Button Pressed event, so add that now to the object.
The code we are going to add here will first get the "snapped" mouse coordinates, then check the position for an instance of the wall object. If one is found it destroys it, but if one isn't found it then goes ahead and creates it (in this way the RMB can be used to add and remove wall instances). After creating the wall, it is then added into the mp_grid, and the script we created earlier is used to create the path that the enemy instances will follow. At this point, if the path creation has succeeded, we send some information to the object "obj_pathfinder" (our "enemy" object), otherwise we destroy the wall instance we have just created because it is blocking the path and we want the enemy instances to always have a path to the goal. We will cover the "obj_pathfinder" instance in the next part of the tutorial, but for now simply copy the code into the Global RMB Pressed Event:
Our last task in this short tutorial is to have our enemy object "obj_pathfinder" actually follow the new path that will be created. So, open this object now and then open the Create Event and remove the current code block. Now add the following:
This code creates a variable to hold the current path position, as well as two more variables to hold the actual coordinates of that position. You see, what we are going to do here is not start our instance along the path using path_start() as you would expect. No instead we are going to have it go from point to point on the path without actually using the path itself. Let me explain...
When the mp_grid function creates the path, the path is comprised of a number of points, and each point has an x/y position in the room. Now, if we have the enemy instance follow the path exactly, when it changes (as the player adds wall objects) the enemy instances will "jump" around and look very bugged as they change position in the room to keep on the path. Obviously this is not a behavior that we want!
To overcome this, what we are going to do is use another of the more basic AI functions that GameMaker: Studio has to move from point to point on the path in a more autonomous way. So we need to know the current "go to" point and it's coordinates - which when the AI is created is point 1.
But the instance isn't actually moving yet? Let's add in our final block of code to deal with that... Create a Step Event and open a code block, then add this code:
Believe it or not, this short code block is probably the most important in the whole tutorial! This code will give our enemy instances a basic AI that actually avoids obstacles while constantly searching for the path to the finish object. How does it do this? Well, we are first of all checking to see if the instance has reached the assigned path position (we set the xx/yy variables in the create event, remember?) and if the position has been reached, we increment the position counter variable (pos) and then use that to determine what to do next.
We first check to see if pos is equal to the length of the path, because if we try to get a position that is not on the path, GameMaker: Studio will give an error, which we obviously don't want. If the last path point has been reached, but if it hasn't then the next path point position is used to set the xx/yy variables again.
The last two lines are where the magic happens! We use the function mp_potential_step() to move our instance towards the path point that is defined as the current position to go to. This function on it's own is a very basic AI as it tells an instance to move towards a point while avoiding obstacles. However it is not powerful enough to cross a whole room of obstacles, which is why we are using the mp_grid. But it is powerful enough to avoid one or two obstacles and get to the defined path point, which is close by and easy to reach.
It's this combination of the mp_grid and the motion planning function that make this simple AI surprisingly versatile and powerful, so keep in mind that you can mix and match simple functions like this to achieve sophisticated effects when making your games.
That's it for this tutorial. you can test your game now and use the RMB to place and remove wall objects. if you have done everything correctly, then you should see the path change when you create a wall (make one right on top of the path and see what happens), and the enemy instances will rush to change course and avoid it, all the while getting back to the path.
Now, this AI is not full-proof, but hopefully you have a good enough grasp of the necessary functions to improve upon it. For example, you can "trap" an enemy instance in a loop of movement if the path point they have to go to means they have to go backwards around a wall... can you fix this? (I'll give you a hint - you can use an alarm in the enemy and count a variable down.)
Have fun playing with these functions and this demo game!