Mini Map with Fog of War in Gamemaker Studio

I was showing off the newest game, that we at Elektri are working on, and one of my Instagram followers asked me how I did the mini map with fog of war / map discovery mechanic inside of that game. In the 20 seconds of googling the issue I see results on Reddit and the Gamemaker forums explaining the high level concept of what I am doing, but not really getting into the nitty-gritty, so I thought I would go really in-depth about what is going on in my fog of war system.

I recommended that you follow the tutorial that Heartbeast did on building a random walk algorithm before you follow this tutorial. First, because this mini map is built on top of the random level generation script, and second it will give you a rudimentary crash course on what a DS_GRID is. If you just want to follow along with making the mini map, you can download a completed version of the random level generation tutorial here.

The mini map is made up 2 key pieces of data, tiles that the player is able to walk on, and the player’s current location. Because we only need to draw these 2 things to the user interface, we only need to make 2 sprites. These sprites do not need to be very big, the ones that I use are 3 pixels by 3 pixels. The sprite representing the player’s location is white, so RGB 255, 255 and 255 and the one representing the floor is RGB 0, 0, 0 with an alpha of 128 to make it translucent.

All of the work for the mini map is going to be handled by 1 game object. In most games you can put it in your game manager but for the purpose of this tutorial we can make a dedicated mini map object. This new mini map object is only going to have 2 events, the create event and the draw_gui event. The create event is going to be used to initialize some data and most of the heavy lifting is done in the draw_gui event.

If you followed heartbeast’s tutorial, you would already know that the level is being stored inside of a DS_GRID and perhaps all we need to do is draw the existing DS_GRID to the GUI layer and we would have a functional mini map. Hey readers, that is a good idea that you just came up with! Lets go ahead and do that.

We can resize the GUI by running a line of code in the create event. The project has some values that I do not particularly like. The camera is set to 320 * 180 with the view port at 1280 * 720. I left the camera alone and changed the view port to 640 * 360 and changed the GUI to 640 * 360.


display_set_gui_size(640, 360);

Now we need to loop through the DS_GRID, stored in o_level, and draw the floor tiles to the screen. We can store the width and height of the grid, as local variables, with the ds_grid_width() and ds_grid_height() methods and use this data in our nested for loop. Inside the for loop we check current the grid position and draw it to the GUI if it is a floor tile.

Finally we check the player’s x and y location in the room, divide those values by the width and height of the cell to get the cell that our player is standing on, and draw that location to the GUI (also don’t forget to put an instance of our mini map object into the game room).


// Get width and height
var width = ds_grid_width(o_level.grid_);
var height = ds_grid_height(o_level.grid_);

// Spacing for map tiles
var space = sprite_get_width(s_map_path);

// Draw the map
for (var yy = 0; yy < height; yy++)
{
for (var xx = 0; xx < width; xx++)
{
if (o_level.grid_[# xx, yy] == FLOOR)
draw_sprite(s_map_path, 0, xx * space, yy * space);
}
}


// Draw the player
draw_sprite(s_map_player, 0, (o_player.x div CELL_WIDTH) * space, (o_player.y div CELL_HEIGHT) * space);

What we have working now is indeed a functional mini map with player location but you might be asking yourself, hey wait a minute, this tutorial is supposed to have fog of war right? Well, yes, but it was your idea to just draw the existing DS_GRID to the GUI and I just followed along with your idea.

To be able to make a working fog of war we will need access to data that the existing DS_GRID doesn’t have, and to do that we need to make a second DS_GRID to store that data. Our new DS_GRID is going to have the exact same width and height as the DS_GRID stored in o_level and instead of storing if a tile is a floor or wall we are going to store if a floor tile is void or has been explored. If the tile is void we will not draw it to the screen and if it is explored we can then draw it to the screen.

To make programming this a little bit easier we can open up the create_macros script and add an explored macro. You can add our new explored macro just underneath the existing floor, wall and void macros, and give our new macro a value of -8.


#macro EXPLORED -8

With the new macro created, we can create our new DS_GRID in the create event of the mini map object. We can give it the same dimensions as the o_level DS_GRID using the ds_grid_width and ds_grid_height functions again. After the DS_GRID is created the entire data set needs to be filled with void.


// Set the grid width and height
var width = ds_grid_width(o_level.grid_);
var height = ds_grid_height(o_level.grid_);

// Create the minimap grid
explored = ds_grid_create(width, height);


// Fill the grid with void
ds_grid_set_region(explored, 0, 0, width, height, VOID);

In the draw_gui event, we need to switch the DS_GRID that we are drawing to the GUI from the level grid to our own grid, and add an if statement to only draw a tile if it is flagged as explored, instead of if it is a floor.


if (explored[# xx, yy] == EXPLORED)

Finally, we need some sort of method to change the value inside of the DS_GRID from void to explored. A really easy way is to simply change the value of the tile that the player is standing on. If we are standing on a tile, then we know it’s a floor and we can mark that tile as being explored. We also already know what tile the player is at because we are drawing it to the screen, so we can flag that tile as explored inside of our new DS_GRID.


// Flag player's location as explored
explored[# o_player.x div CELL_WIDTH, o_player.y div CELL_WIDTH] = EXPLORED;

Now we have a working fog of war, but not enough of the map is discovered as we walk through the room, the player needs to be standing on a tile for it to be flagged as discovered. We need a way to discover more of the map, ideally whatever is on the screen should be flagged as explored. What I did is counted how many tiles are on the screen at a time, this game’s camera size is 320 * 180, and the tiles are 32 * 32. So there are 10 tiles that can be on the X axis at any given time and 5 and a half tiles on the Y axis.

What I do will not discover 100% of the tiles on-screen but it will be close enough. We are already flagging the tile that the player is on, and we can flag 4 tiles to the left and right and 2 tiles up and down giving us 9 tiles of visibility on the X axis and 5 on the Y axis. This means that the tiles on the very edge of the camera will not be discovered on the mini map until you move closer to them.

To do this, inside of the draw_gui event we are going to have another nested for loop where we check 4 tiles left and right of the player and 2 tiles up and down from the player, and if those are floor tiles in the level DS_GRID then we will mark them as explored in our DS_GRID.


// Flag the rest of the tiles on-screen as explored.
for (var yy = 2; yy >=-2; yy--)
{
for (var xx = 4; xx >=-4; xx--)
{
// Get cell to check
var xexplore = (o_player.x div CELL_WIDTH)+xx;
var yexplore = (o_player.y div CELL_HEIGHT)+yy;

// Don't go outside DS_GRID
xexplore = clamp(xexplore, 1, width-2);
yexplore = clamp(yexplore, 1, height-2);


// Check the cell and flag as explored
if (o_level.grid_[# xexplore, yexplore] == FLOOR)
explored[# xexplore, yexplore] = EXPLORED;
}
}

And there you have it, a working mini map with a fog of war that will only draw what is visible on the user’s screen. Hopefully somebody, somewhere, finds this post useful at some point in time.

Until Next Time
– Steven