The game i was imagining lended itself very well to use of grids for its levels. You move from one block to the next, its easy to code solid logic that is straightforward both to me and the player.

When making the game first big decision was how big should i make the tiles. If they were too small it would be very difficult to make any moves and register your input on a phone screen and if they were too big there would not be enough space to make interesting level design. So i landed on a happy medium tile size of 72px, which makes 15×19 tiles playspace on the biggest levels. Very neat and easily divisible numbers.

 

Initializing the Grid


One advantage of using grids is the opportunity to reduce the use of objects, mainly not using an object instance for every wall block in the level. This way i can use objects to create and design levels in the room editor and at the room start i call a script that goes through the level, checks every block and records it in the grid as a number.

This code is run at a level controller objects create event:

///@param grid
var grid = argument0;

//Cover grid with rock(6)
ds_grid_clear(grid,6); 
if (instance_exists(oEnd)) oEnd.locks = 0;

//Go though every line
for (var i = 0; i < global.hCount; i++)
{
    //Go though every collumn
    for (var k = 0; k < global.wCount; k++)
    {
        var xx = (global.startX+global.tileW/2) + k*global.tileW;
        var yy = (global.startY+global.tileW/2) + i*global.tileW;
        
        //If tile is a Wall, 1              
        var wall = instance_position(xx,yy,oWall);
        if (wall != noone) 
        {
            ds_grid_set(grid,k,i,1);
            instance_destroy(wall);
        }
        //If tile is a player, 2
        if (position_meeting(xx,yy,oHead)) 
        {
            ds_grid_set(grid,k,i,2);
        }

        .
        .
        .

        //If tile is a Illusory wall, 17
        if (position_meeting(xx,yy,oIllusionBlock)) 
        {
            ds_grid_set(grid,k,i,17);
        }  
        //If tile is a Tunnel, 19
        if (position_meeting(xx,yy,oTunnel))
        {
            ds_grid_set(grid,k,i,19);
        }      
    }
}

In this script it takes global values like global.hCount and global.wCount which are the current levels height and width in blocks. Using nested for loops you can cover the entire playspace and check for presence of instances of certain objects and if  position_meeting() returns true for said object destroy it(or sometimes not) and write its value in the grid. This works for any level size.

 

Interacting with the Grid


You might ask if i delete a lot of the objects how do they interact? The answer is that they interact trough the grid values. For example, the movement is done with a state machine, so if move=0, it means player/enemy is idle and checks if drawn path ds_list is not 0 (there is pending path) so it changes move=1 which is a digging/moving state. Now before any movement actually happens you can check what grid value will be on the tile you are about to move into – if its a gem, you can send the gem flying to be collected, if its a key you can unlock the end gate, etc.

This code is run in Player step event:

//Movement if drawn a path
if (ds_list_size(path) > 0 && move == 0)
{
    destPos = path[| 0]; 
    xAxis = (destPos[0]-gridPosX);
    yAxis = (destPos[1]-gridPosY);
    //If move in a direction
    if (xAxis != 0 || yAxis != 0)
    {
        //Destination tiles value
        var val = ds_grid_get(global.playGrid,destPos[0],destPos[1]);
        //************************************ PICKUPS *******************************************
        //If dug a GEM        
        if (val == 4) 
        {            
            oBattle.gemsCollected += 100;
            //Pick up the Gem
            var gem = instance_place(x+xAxis*(global.tileW/2),y+yAxis*(global.tileW/2),oGem);
            gem.fly = true;
            //Mark spot as empty
            ds_grid_set(global.playGrid,destPos[0],destPos[1],0);
        }
        //If dug a GHOST GEM
        if (val == 9) 
        {
            var ghostGem = instance_place(x+xAxis*(global.tileW/2),y+yAxis*(global.tileW/2),oGhostGem);
            //Pick up the Ghost Gem
            if (!ghostGem.fly) 
            {
                oBattle.ghostGemsCollected += 1;
                ghostGem.fly = true;
                //Mark spot as empty
                ds_grid_set(global.playGrid,destPos[0],destPos[1],0);
            }           
        }
        //If dug a Key     
        if (val == 10) 
        {            
            var key = instance_place(x+xAxis*(global.tileW/2),y+yAxis*(global.tileW/2),oKey);
            //Pick up the key
            if (!key.fly) 
            {
                key.fly = true;                
                //Mark spot as empty
                ds_grid_set(global.playGrid,destPos[0],destPos[1],0);
            }  
        }
        //If Hit a Consumable
        if (val == 12) 
        {
            var cons = instance_place(x+xAxis*(global.tileW/2),y+yAxis*(global.tileW/2),oConsumable);
            //Pick up the consumable
            if (!cons.fly) 
            {
                cons.fly = true;
                //Mark spot as empty
                ds_grid_set(global.playGrid,destPos[0],destPos[1],0);
            }                                    
        }

        .
        .
        .

xAxis and yAxis is just movement direction that is calculated from destination position(array with x and y pos) and current position, so value of 1 is right for xAxis and down for yAxis and -1 the other way around and 0 is no movement.  val = ds_grid_get(global.playGrid,destPos[0],destPos[1]) gets the grid value to check and the rest is just decision making depending on that.

Drawing form the Grid


One other consequence of destroying a lot of objects at the level start is that they still have to be drawn on screen somehow and the only way is using the information in the grid. But a big negative for dealing with grids every frame is performance takes a huge hit. Maybe if this was a PC game it wouldn’t be that noticeable, but for a mobile game its a killer. Solution is to only access the grid if something actually changes and the rest of the time just draw it once on a surface and keep drawing that surface.

This code is run at a level controller objects draw event:

//Only draw new surface if something changes
if (drawUpdate)
{
    surface_set_target(digsiteSurf);
    draw_clear_alpha(0,0);
    
    //Draw Walls,Rocks,Gems,Barriers,keys and selection
    for (var i = 0; i < global.hCount; i++)
    {
        //Go though every collumn
        for (var k = 0; k < global.wCount; k++)
        {
            val = ds_grid_get(global.playGrid,k,i);
            sel = ds_grid_get(global.selectionGrid,k,i);
            var xx = (global.startX) + k*global.tileW;
            var yy = (global.startY) + i*global.tileW;
            
            if (val == 1) 
            {
                var img = scAutoTile(k,i,1,global.playGrid,global.wCount,global.hCount);
                switch (global.currentDigsite)
                {
                    case "Tutorial": case "Graveyard":  draw_sprite(spGraveyardWall,img,xx,yy); break;
                    case "Library":  draw_sprite(spLibraryWall,img,xx,yy); break;
                    case "Asylum":  draw_sprite(spAsylumWall,img,xx,yy); break;
                    case "Castle":  draw_sprite(spCastleWall,img,xx,yy); break;
                    case "Core":  draw_sprite(spCoreWall,img,xx,yy); break;
                    default: draw_sprite(spGraveyardWall,img,xx,yy);
                }
            }
            if (val == 4 || val == 9 || val == 10 || val == 12 || val == 14) draw_sprite(spRock,0,xx,yy);
            if (val == 6) 
            {
                var img = scAutoTile(k,i,6,global.playGrid,global.wCount,global.hCount);
                draw_sprite(spRock,img,xx,yy);
            }
            if (val == 8)
            {
                var img = scAutoTile(k,i,8,global.playGrid,global.wCount,global.hCount);
                draw_sprite(spBarrier,img,xx,yy);
            }
            if (val == 15 || val == 9 || val == 12) draw_sprite(spTreasureRoom,0,xx,yy);
        }
    }

    surface_reset_target();    
    
    drawUpdate = false;
}

So now only when the drawUpdate is turned on the digsiteSurf surface gets cleared and the new scene is drawn to it. Not every single thing is drawn this way, only the most numerous objects in the levels like the walls and rocks. Objects that have animations have to be actual object instances.

Now that i have all this level info in a single grid i can use grid functions like ds_grid_value_exists() to check if a certain value is in a vicinity or check if a move is available depending on adjacent grid values.

 

Hope this helps an if you think this is not the best approach, please leave a comment because i freely admit that i’m a noob in programming and am always willing to learn.