VBA4Play: Making a Maze (Part 1: Your First Maze)

This post has been written by guest author Jordan Goldmeier of Option Explicit VBA

Disclaimer: The following tutorial requires Conditional Formatting which is a feature of Excel 2010 and greater. If you are using an older version of Excel, please note that formatting will not work correctly for you.

This article is part of a series called “VBA4Play”, written in conjunction with Excel game developer, Cary Walkin, for his blog. Many thanks to Cary for inviting me to write a few tutorials for his series.

This post is a continuation of my first post for Cary Walkin’s VBA4Play series, Development Principles for Excel Games and Applications, and Cary’s first post, VBA4Play Part 1: Movement and Collision Detection. If you have not read either of these posts, I strongly suggest you read them before moving forward.

Today, we’ll be focusing on how you can turn regions of your spreadsheet into a map for a game. This will be similar to the illuminated pathway featured in the bottom-right of the spreadsheet maze shown below.

Maze

Tile sets, tiles, and squares

Let’s get started. The first thing you’ll need to do is define a tile set. The tile set is the exhaustive set of all possible tiles to be displayed on your map. A tile is a region, usually larger than a square, that contains specific information about the the terrain encoded as a number. This may sound complicated but our example really only includes two key pieces of information: if a square contains a 1, then the square represents a wall; if it contains a 0, it represents an open pathway.

Well, that sounds pretty easy, right? If each cell is a square, we can combine several squares to make a tile. Below, I’ve made a tile that defines a pathway that leads east from the center using. I’ve used each cell as the square; a three-by-three region of cells then becomes a tile.

image

Rather than have Excel look for ones and zeros and used VBA to color cell-by-cell, we’ll skip a step and just use Conditional Formatting to define a cell’s background. In the Conditional Format rules, I’ve set the cell background to be black when its value is equal to 1. I then delete the zeros because Excel treats an empty cell as a zero. This let’s us avoid the needless step of applying additional conditional formatting. If you’d like to follow along, open a new spreadsheet and try to recreate the tile below.

image

You’ll now have to define all the tiles in your tile set. I say “now,” but in my original formulation, I only started with eight. As I began developing the map, I realized I needed more. So try your best to define all the necessary tiles before moving forward but always remember you can add more later if need be. In all, I’ve defined 15 tiles (see left, sidebar below).

image

The numbers to the left of each tile are really important. You can’t really see them in the pic to the left because I’ve zoomed out to capture the tile set, but they start from 1 and count down to 15. They act as a key, or a unique identifier, for each tile. If you’ve done database work before, the concept should be pretty familiar to you. If you’re on the accounting side of experience, think of each number as an identifier to a particular account.

So now comes the incredibly monotonous task of naming each of these tiles. Start with the first tile, select the tile region and give it a name like “Tile1″.

image

Now keep doing this until you have no more tiles left to name. If you misspell a name or give it the wrong key, you can fix or delete the name by using the Name Manager on the Format tab.

Displaying your tiles

Giving each tile a number works to our advantage because now we need only to use the CHOOSE function to return the tile we’re interested in (see image below). If you’re new to the CHOOSE function, just remember to count by the number in the first parameter. Below, CHOOSE will return the named range, Tile1. If I want it to return Tile5, I’ll put a 5 in the first parameter. Make sense? Just imagine how many nested IFs this would have required! CHOOSE is so great.

image

Ok, so we’ve typed up the formula above into a cell and it’s returning a 1. Remember that each tile is comprised of a range of cells. So drag the formula to the right and down (Copy + Paste) until you have a 3-by-3 grid. Now click into the formula bar and then Ctrl+Shit+End to let Excel know you want to return a range and not a single cell.

Ok, that’s just one tile, what about the rest of them? Good question! Let’s move away from the array formula for a moment. If we wanted to return only one cell at a time, we’d have to pull them out of the tile one square at a time. So make a table that looks like this:

image

Inside the table, we’ll have to use a formula that can pull each individual cell from a chosen tile. For that, we’ll need to use the INDEX formula. The INDEX formula takes a range or an array in its first parameter, and a row and column indices in its second and third. Use the INDEX formula as I have below.

image

Your addresses may be different from my above (W40,X39), so take care to ensure your formula works. Note, I’ve done absolute addressing on the row and column arguments so that I can drag across and down without problem.

Now that you’ve dragged the region across and down, select column headers (including 1,2,3) and the entire tile as I have below. Copy it, and then paste another to the right of it.

image

Keep pasting until you have 5 sets of tiles. Now let’s select the entire row. This time, select the row headers but not the column headers. Copy that and paste another below, as I’ve done in this graphic:

image

Keep doing that until you have five rows.

OK, so now you should have a 5 by 5 tile grid all with the same tiles.

image

Mapping your tiles

What we need now is a way to easily map a specific tile to a specific area on the grid. So, in a space next to your tile grid, create a new table that looks like this:

image

Note the five-by-five grid above is similar to the five-by-five set of tiles in the larger grid. For now, just put a series of 1’s into the grid as I’ve done below. Then select the data region and give it a name like “TileMap”.

image

So here’s how this is going to work. The key you enter into the table above defines what tile will appear in the larger map. The problem is that we must keep track of two indices. We need to keep track of the the rows and columns inside of each tile and we must also keep track of the overall location of the tile. What we can do is a provide another series of indices around the larger grid as I’ve done below.

image

For example, the region that intersects with both series of 1s in the rows and columns of this square grid corresponds to the first row and first column of the smaller tile map we created a moment earlier.

We’ll use INDEX one more time. We’ll use the new row and column to help us map the current tile location back to its definition on the smaller grid. The number that’s returned will inform our CHOOSE formula.

image

Try your hand at the formula above. Now drag down and across to copy and paste the formula to the rest of the large grid. Afterward, pick a few cells on your larger map at random and press F2. Make sure the correct references are being pulled. If you’re satisfied, go to your tile map and start changing numbers. Notice how they are update immediately on the larger map. Remember you can consult your tile set as legend to find the right number for the desired tile.

maze auto change

When you’re done with your map, select the entire data region of your square map and give it a name like “SquareMap.”

Time to get fancy.

Getting Fancy #1: Creating a Player and Dealing with Collisions

If you want to work with the same layout I’m using, use this sequence in your tile map.

image

Now go start a new tab and title it something like “Calculations”. Near the top of the new workbook, create placeholders for the variables Top and Left as I’ve done below. Then name the corresponding cells Calculations.Top and Calculations.Left.

image

Create a larger table a few cells away from the placeholder. First, create the column header by initiating a consecutive series from 1 to 15. Then do the same for row headers. (See the table below as a guide.) Now use INDEX like we did before to pull out the corresponding cells from the larger map. Pay attention to setting the correct row and column references.

image

Use conditional formatting like we did earlier in the tutorial to turn the 1s black. For now, leave the zeros as they are. Note that an entry point to our map appears at the intersection of the second row and the first column. So enter the number 2 in the space next to the Top label. Enter a 1 next to the Left label.

We want our player to appear at the intersection of the coordinates given by Top and Left. There are a couple of ways we can do that. The easiest way is to employ an IF formula. But for a much larger map, the IF formula is going to slow us down. So we’re going to prefer a more complicated method even if the performance improvement in this example is trivial. In the upper left of your new table, retype the formula as I’ve done below:

image

Remember to be mindful of the references if your spreadsheet is laid our differently. Now drag right and down ensuring the correct references are being used.

Let’s take a moment and think about what’s going on. The AND() function will return a TRUE when all conditions inside its parenthesis evaluate to TRUE. It will return a FALSE in all other cases. In Excel, mathematical operations on Boolean conditions treat TRUE as a 1 and FALSE as a zero. So when the AND evaluates to true, it will be subtracted from either one or zero. If our player is correctly on a path in our map, it will show up as a –1.

Now select the entire data region. Click on the small arrow in the Numbers group from on the Format tab.

image

Select Custom for the category, then paste in the following code and press OK.

[=-1]":)";""

Custom Formats are beyond the scope of this article, but you can read more about them here. What this will do is if the value is -1 then the cell will have a smiley face visible without changing the data or formula.

Now we need to code some movement and controls. Open the Visual Basic Editor and double-click the sheet object (in the Project Explorer in the upper left) corresponding to the sheet were were just looking at. When that opens, we’ll insert our first sub:

1: Public Sub Restart()
2: [Calculations.Top] = 2
3: [Calculations.Left] = 1
4: End Sub

This code will simply restart the player to the beginning of the map. In an ideal world, you would not use explicit numbers in your code. Instead, you’d have constants either residing in your code or on the spreadsheet.

Next, we’ll add the following. The code is relatively similar to that of Cary’s in his Movement and Collision Detection tutorial.

1: Public Sub MoveLeft()
2: [Calculations.Left] = [Calculations.Left] – 1
3: End Sub

4: Public Sub MoveRight()
5: [Calculations.Left] = [Calculations.Left] + 1
6: End Sub

7: Public Sub MoveUp()
8: [Calculations.Top] = [Calculations.Top] – 1
9: End Sub

10: Public Sub MoveDown()
11: [Calculations.Top] = [Calculations.Top] + 1
12: End Sub

Going back to the spreadsheet, draw five shapes as I’ve done below. You can find these shapes on the Insert tab.

image

Now right-click each shape and assign it to its corresponding macro. Check each button to make sure the macro works. If everything is working, it’s now time to do some collision detection. What we need to do is test if the intended direction will make us hit a wall. To do this, we need to simply modify the code:

1: Public Sub MoveLeft()
2: If [SquareMap].Cells([Calculations.Top], [Calculations.Left] – 1) = 0 Then
3: [Calculations.Left] = [Calculations.Left] – 1
4: End If
5: End Sub

6: Public Sub MoveRight()
7: If [SquareMap].Cells([Calculations.Top], [Calculations.Left] + 1) = 0 Then
8: [Calculations.Left] = [Calculations.Left] + 1
9: End If
10: End Sub

11: Public Sub MoveUp()
12: If [SquareMap].Cells([Calculations.Top] – 1, [Calculations.Left]) = 0 Then
13: [Calculations.Top] = [Calculations.Top] – 1
14: End If
15: End Sub

16: Public Sub MoveDown()
17: If [SquareMap].Cells([Calculations.Top] + 1, [Calculations.Left]) = 0 Then
18: [Calculations.Top] = [Calculations.Top] + 1
19: End If
20: End Sub

Notice how the If command always evaluates the intended direction. One extra benefit to this method is that it also handles the boundaries of the map. Try to move the player off the map.

Getting Fancy #2: Clipping and Viewports

viewport is a view that only shows a subset of a larger image. Clipping describes how you trim the view for your viewport. Showing the entire map was a good start, but we don’t need to show the user everything. That’s too easy for them. Instead, we’ll create a viewport to show the user only a small amount of the map at a time. Below, I’ve trimmed the entire map to be only 7-by-7 tiles:

image

Starting with the 2 in the column header, I’m going to select it and change its formula to look like this:

image

Now I’m going to drag right until the rest of the cells after the second have the same formula. I’ll do the same thing for the 2 in the row header and then drag down.

Finally, I’ll click the 1 in the in the column header row and set it equal to Calculations.Left (see below). Likewise, I’ll click on the 1 in the header row and set it equal to Calculations.Top.

image

Now if we move the player around, we see that the viewport moves along with him. He’s always in the upper-left, minding his own business. Looks good, but what if we want him in the middle instead. Well, if we look at the at our row and column headers, there are seven numbers for each. The middle then is three numbers in. So if we want to push the player three spaces in, we’ll subtract the first cell of our row and column headers by three. Below, I’ve captured this for the column header.

image

Make sure to do the same for the row header. There’s no need to drag down or across after performing these calculations.

travers maze

Those hashmarks you see are actually errors. Having too many unhandled spreadsheet errors can be a cause for some major slowdown. If you like how they look, you can use IFERROR to place them there without error as I’ve done below.

image

I generally don’t like those hashmarks so I’ve placed a 1 in that final parameter instead. That makes them black due to the conditional formatting we set up earlier.

Getting Fancy #3: Illuminated Pathways

To begin, copy the viewport and paste it a few rows down, as I’ve done below. Once pasted, click on a few cells in the bottom viewport to ensure the formulas are pulling from the correct references. For the bottom viewport, the formulas should pull from the bottom header row and not from the top header row.

image

In the top viewport, delete everything in the content area and then recreate the static row and column headers (1 to 7) like we had before as shown below. You’ll need to select the content area and change it from the Custom type we set in Getting Fancy #1 back to General. There might also still be conditional formatting in the table so make sure to clear that out as well. In addition, I’ve enclosed a border around the map area to make it a little easier to read. We will not use the numbers in the header and column rows; they’re just there to guide us.

image

Now to create the illuminated pathway, we need to something that can measure the distance from the player to everything else. For that, we can use the distance formula. Now this next part is more art than science. I’ve found that the illumination works after you play around; that is, I haven’t found a fool proof method to always make it look how I want. You just gotta try a few things out.

So the goal here is to essentially make two different stratifications of numbers. The walls and objects farther away should be darker, so we want them to be a higher number. Closer objects and even paths that are farther away we want to be lighter, so we need to them a comparatively smaller than the former. So, in the first cell of the top viewport…

image

…I used this formula.

=IF(SQRT(($E14-Calculations.Top)^2+(F$13-Calculations.Left)^2)<1,SQRT(($E14-Calculations.Top)^2+(F$13-Calculations.Left)^2)*(F14+1),SQRT(($E14-Calculations.Top)^2+(F$13-Calculations.Left)^2)*(F14+1)*4)

Here’s how it works. The distance formula is everything inside the SQRT function. The IF function tests if the distance is really close, for instance, if the distance that results is less than one away. The larger your viewport, the greater this number can be. For instance, the illuminated pathway in the example file tests for values 10 away or less. Like I said, this is more art than science.

(I know I’m using an IF. You could do this without an IF, but that formula is beyond the scope of this article.)

If it’s less than 1 away, then we’ll multiply it by 1 plus the value in the same spot on the normal tile viewport. Since the walls are actually a 1 or hallways a 0 in the bottom viewport, multiplying the distance formula by the corresponding value plus 1 will ensure we’ll get a number back (so we’re not multiplying by zero) and that the walls will always produce greater numbers than hallways. Finally, if the distance is more than one away, we want to make the value sufficiently greater. Notice the last part of the IF statement is pretty similar to the previous argument except that we multiply everything by 4. Drag the formula down and across the entire viewport.

Because we used cells from the bottom viewport, the cells in the top viewport might borrow the bottom’s custom number format a conditional format.

For now, select the content area of the top viewport range and go into the Conditional Formats dialog box. Delete any rules that might exist. Once complete, click New Rule…

Now, change your settings to match mine below.

image

Make sure the tones are white and black. Don’t worry about that weird fade to red in the middle. So why the 65th percentile? No reason in particular, I just think it looked the best. In the example maze I think I used 90th percentile. Alternatively, you can make this a 3-Color Scale and try to get better control over the middle tones. I’ll leave that to you. Press OK when complete. You should now see faded colors blooming from the center of your map.

Finally, because our custom type was set to show the smiley player when it found a –1, we’ll need to change to show when it finds a 0, the center point of the map. (The distance formula always returns a 0 in the center.) So select the content area of the viewport and assign this custom format:

[=0]":)";""

That’s it! If the map looks a little blocky, try zooming out, like this:

image

Download everything in Part 1: VBA4Play Map Part 1 Examples.xlsm

Download the complete 3D maze: Maze Example.xlsm

Homework

Interested in pursuing these topics further? Here’s your chance.

1. If we turned the map above into a maze, create a mechanism to let the user know he or she has reached the end of the maze.

2. The tiles in this map always have the middle portion open to represent a hallway. Create a few more tiles that allow for a more “open concept” map. Share it in the comments, if you think it’s super awesome. Try making an even larger map.

3. If you have several pre-made maps already created (think levels), how can you replace the current map in view quickly?

4. Come up with a better way to fade the colors in the illuminated pathway.

5. Make a new subroutine that will randomize the tiles so that every time the subroutine is run it creates a new map. Note that you will want a restriction so that all tiles can be accessed by the player.

I hope you’ve found this tutorial useful. The next tutorial will make this maze three dimensional! Depending on how much time I have, I may also do an answer session for the homework. I’ll leave you with one thought: look how much you accomplished without tons of code!

Jordan

    • Jordan G
    • September 17th, 2013

    Reblogged this on Option Explicit VBA.

    • Darin Myers
    • September 18th, 2013

    Very impressive model. I really enjoyed the in-depth explanation you have provided so far. I took up your challenge of extending the model a bit more and made two modifications:

    1. I was a bit confused by the navigation at first until I realized that it was based on the direction that the player was facing. To visualize this, I changed the player to an arrowhead instead of an X using symbols. Now it is trivial to navigate and keep your bearings.

    2. As an Excel user, I am a big keyboard advocate. As there is no native keydown event in Excel VBA, I took an alternative approach that I have seen in other Excel games for keyboard navigation; using the selection chage event. I used this event to see if the direction changed with respect to a cell I defined as Center. After the move, I returned the selection to the center. Using this approach, I could hook in and run your direction change methods. As I was limited to up, down, left, or right, I called the TurnAround method when the user pressed down, which felt fairly natural to me when navigating.

    If you care to take a look at my modification, I have it saved in the public folder of my skydrive (link below, same name as your model with a suffix of “- Darin Mods”). I would appreciate and welcome your comments, and thanks again for this wonderful demonstration.

    https://skydrive.live.com/redir?resid=1B9FE8A711D3EB57!622&authkey=!AHuGsK1YWRyheTU

    • Hi Darin,

      Thanks for taking an interest. Unfortunately I can’t seem to access your file. “Features are unsupported in the browser” and then when I try to save it locally it only saves what could be supported in the browser…

      Also, I completely agree with your points, an arrow is better than the X so you can get a visual sense of orientation rather than relying on the compass at the top.

      Also, for keyboard controls. You could also do what I did for Arena.Xlsm. In Workbook_Activate bind the keys to the subroutines and then unbind them in Workbook_Deactivate and Workbook_BeforeClose so the binding looked like this:
      Application.OnKey “w”, “Move.MoveUp”
      Application.OnKey “s”, “Move.MoveDown”
      Application.OnKey “a”, “Move.MoveLeft”
      Application.OnKey “d”, “Move.MoveRight”
      And to unbind:
      Application.OnKey “w”
      Application.OnKey “s”
      Application.OnKey “a”
      Application.OnKey “d”

      If you have multiple sheets you can always check ActiveSheet.Name before binding/unbinding.

      That way you don’t need to mess around with your current selection.

      Cheers,
      Cary Walkin

        • Darin Myers
        • September 20th, 2013

        I tried following the link myself on another computer and I see what you describe. I was able to download it by clicking the drop-down of Edit Workbook, and then Edit in Excel. There may be one more box that comes up where you need to click download after that. Let me know if that doesn’t work and I will find a different place to upload it.

      • Jordan G
      • September 18th, 2013

      Darin,

      Thanks for the feedback! You and I think alike. In my original implementation, I had keyboard controls implemented with the same way with the selection change method. I decided in the end to forgo that portion because I thought using the arrows as control was easier to explain.

      I also really like Cary’s idea which I hadn’t thought of. Good thinking!

  1. Wow, this is amazing. Thank you so much for sharing this highly detailed and very smart implementation. Looking forward to rest of the episodes.

  2. Darin Myers :

    Let me know if that doesn’t work and I will find a different place to upload it.

    Sorry but it still does not work. I get an error that says: “An error occurred while attempting to save your workbook. As a result, the workbook was not saved.”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 42 other followers

%d bloggers like this: