Tuesday, January 18, 2011

Mini-Maps

For games with complex level design, mini-maps are essential. Especially in games like Super Metroid where the focus of the gameplay is more on backtracking and using your new abilities than pure exploration, the many map is very important tool.

For Platform Hack, I'm planning on two different "mini" maps. One map will be accessible from the pause menu, and will show the entire level, along with special icons for the player, doors, chests, etc. The other map will be a true mini-map, in the upper-right corner of the HUD, showing only a tiny portion of the current level.



Originally I intended to draw a pixel directly on the screen to correspond with each tile in the level, but that turned out to be too slow.

Instead, what we can do it create a Texture2D object on the fly and display it on the screen. If we need to crop (for the small mini-map in the corner), we can calculate coordinates to center on the player, and then specify the sourceRectange parameter of SpriteBatch.Draw.

To build a Texture2D in XNA, we first have to set the size. Then we load in an Color array to specify the individual color of the pixels:



Drawing the full map is pretty straightforward:

spriteBatch.Draw(MapTexture,TopLeftPixel, null, Color.White,0,new Vector2(0,0),this.scale,SpriteEffects.None,0);

If we want to have a cropped version of the mini-map, we can use a function to determine the cropping rectangle, centered on the player position:



Then we can draw with these params in the Draw function:

spriteBatch.Draw(MapTexture, TopLeftPixel, getPlayerCenteredMapRec(this.scale), Color.White, 0, new Vector2(0, 0), this.scale,SpriteEffects.None, 0);

One important feature of mini-maps is the Fog of War. We don't want to reveal the entire map to the player from the beginning. Instead, we want to hide parts of it and reveal more as the player explores. Uncovering new sections of the map is one of the fun parts of games with complex level design.

To implement Fog of War, we store an extra 2D character array that determines which tiles in the map have been revealed. As shown above in the GetColorArray() function, sections that are not yet revealed (char '?') are drawn Gray.

The Fog of War char[,] array starts out completely filled with "unknown" tiles - '?'. Each time we draw, we make a call to update the Fog Of War, determine what tiles the player can see, and switch those tiles over to '.'.

I'm using an open source algorithm called MRPAS to determine what the player can see from his position. Full source available here: http://www.umbrarumregnum.net/downloads/mrpas

The code itself is somewhat complex, but essentially it draws lines out from the player until it hits an obstacle. One nice feature of the algorithm is you can specify to "light walls", which means the wall tiles themselves will be revealed in the Fog Of War, and therefore show up in the minimap.



I was a little worried that the whole thing would run slow, since we're doing a dozen nested loops on nxn mazes every Draw cycle, as well as running the MRPAS algorithm. However, it runs without hiccup. If performance started to become an issue (potentially on massive maps), you could simply keep track of which tiles the player has occupied. If we've already been to this tile, don't run the Fog of War / MRPAS algorithm. Similarly, only recalculate the Color[] for the actual mini-map tile if the player changes tiles.

Next time I'm going to look into everything related to enemies - path finding, line of sight, rudimentary AI, attacking, etc.

Monday, January 10, 2011

Building Mazes

Happy New Year!

From NetHack to Civilization to Diablo to MineCraft, lots of games have used procedural algorithms to generate semi-randomized levels. For a one man shop like myself, its a way to generate lots of gameplay without a ton of gruntwork.

So how to go you about building mazes?

One site that really helped me out was PCG.Wikidog.com - Procedural Content Generation. It contains algorithms for some of the classic roguelikes, including Angband.

A maze itself can be generated with a pretty simple iterative or recursive algorithm. The psuedocode is as follows:


1. Start out with a 2D array of size nxn.

2. Fill each cell in the grid with character '?' (undefined)

3. Carve out a random cell.
   a. Define function Carve: make the input cell empty '.'
   b. Make the surrounding four cells "maybe" ','.
   c. Add these surrounding cells to a coordinate list called Frontier

4. Iterate or Recurse through the Frontier list until it is empty.
   a. pick a random cell from Frontier. You can use weighted algorithms to pick near the front or back of the list, which lead to different styles of maze (tight and claustrophic vs long empty passages)
   b. Run function Check on the random cell. Define check (Note, this is the most complex part of the algorithm, so you may need to follow along the actual code below)
     i. get the EdgeState of the cell - sum up the cells surroundind the current cell. Empty to the north = 1, south = 2, west = 4, east = 8.
     ii. If we're not up against the side of the map, set the diagonal spaces adjacent to the input space as empty, and return false. Otherwise return true. This basically makes the maze have corners.
   c. If Check was true, Cave out the cell. Otherwise Harden the cell - turn it into a wall '#'
   d. Remove this cell from the Frontier list.

5. Once the Frontier list is emtpy, go through the entire map and change "maybe" cells ',' into empty cells '.' . Turn unknown '?' cells into walls '#'


Full Code:

private char[,] maze; //field
private List frontier;

private int height;
private int width;

private void BuildMaze()
{
 maze = new char[height, width];

 //traverse the maze and fill in unknown blocks
 for (int i = 0; i < height; i++)
 {
  for (int j = 0; j < width; j++)
  {
   maze[i, j] = '?';
  }
 }
 //carve out a random spot
 Carve(r.Next(width-1),r.Next(height-1));

 while (frontier.Count > 0)
 {
  double pos = Math.Pow(r.NextDouble(), Math.Pow(Math.E, branchrate));
  int index = (int)(pos * frontier.Count);
  Coord c = frontier[index];
  if (Check(c.x, c.y))
   Carve(c.x, c.y);
  else
   Harden(c.x, c.y);
  frontier.Remove(c);
    
 }

 for (int i = 0; i < height; i++)
 {
  for (int j = 0; j  0)
 {
  if (maze[y - 1,x ] == '?')
  {
   maze[ y - 1,x] = ',';
   frontier.Add(new Coord(x, y - 1));
  }   
 }
 if (y < height - 1)
 {
  if (maze[y + 1,x] == '?')
  {
   maze[y+ 1,x ] = ',';
   frontier.Add(new Coord(x, y + 1));
  }
 }
 if (x > 0)
 {
  if (maze[y,x - 1] == '?')
  {
   maze[y,x - 1] = ',';
   frontier.Add(new Coord(x - 1, y));
  }
 }
 if (x < width - 1)
 {
  if (maze[y,x + 1] == '?')
  {
   maze[y,x + 1] = ',';
   frontier.Add(new Coord(x + 1, y));
  }
 }
 //shuffle frontier?
 
}

private void Harden(int x, int y)
{
 maze[y,x] = '#';

}

private bool Check(int x, int y)
{
 int edgestate = 0;
 if (y > 0)
 {
  if (maze[y - 1,x ] == '.')
   edgestate += 1;
 }
 if (y < height - 1)
 {
  if (maze[ y + 1,x] == '.')
   edgestate += 2;
 }
 if (x > 0)
 {
  if (maze[y,x - 1] == '.')
   edgestate += 4;
 }
 if (x < width - 1)
 {
  if (maze[y,x + 1] == '.')
   edgestate += 8;
 }

 if (this.noDiagonals)
 {
  if (edgestate == 1)
  {
   if (y < height - 1)
   {
    if (x > 0)
    {
     if (maze[ y + 1,x - 1] == '.')
      return false;
    }
    if (x < width - 1)
    {
     if (maze[y + 1,x + 1 ] == '.')
      return false;
    }

   }
   return true;
  }
  else if (edgestate == 2)
  {
   if (y > 0)
   {
    if (x > 0)
    {
     if (maze[y - 1,x - 1 ] == '.')
      return false;
    }
    if (x < width - 1)
    {
     if (maze[ y - 1,x + 1] == '.')
      return false;
    }
   }
   return true;
  }
  else if (edgestate == 4)
  {
   if (x < width - 1)
   {
    if (y > 0)
    {
     if (maze[y - 1,x + 1 ] == '.')
      return false;
    }
    if (y < height - 1)
    {
     if (maze[ y + 1,x + 1] == '.')
      return false;
    }
   }
   return true;
  }
  else if (edgestate == 8)
  {
   if (x > 0)
   {
    if (y > 0)
    {
     if (maze[y - 1,x - 1 ] == '.')
      return false;
    }
    if (y < height - 1)
    {
     if (maze[y + 1,x - 1 ] == '.')
      return false;
    }
   }
   return true;

  }
  return false;
 }
 else
 {
  if (edgestate == 1 || edgestate == 2 || edgestate == 4 || edgestate == 8)
   return true;
  else
   return false;
 }

}
And that's it. You end up with something like this:
.#.##.#.#.#....#.#.#
....#.#.#.#.##.#....
###.#...#...####.###
#...###...#..#.#.#..
..#.#.##.#####.....#
###.#.#........###..
.#..#.##.#####...###
.#.##..#.....#.####.
....#.##.#######..#.
#.###.#...#...###...
The trouble is that the passageways are only a single tile wide or tall, which would make it very hard for our hero to navigate the game world, since he's two tiles tall! So we can scale the maze, simply by take a tile and copying it to 3 tiles to the east, south and south east. This doubles the size of the board. We can continue to double until the maze feels right - for my game it's a factor of 2, which is 4 times the size. You end up with something like this:
....########....####........####....####........########....####....########....
....########....####........####....####........########....####....########....
....########....####........####....####........########....####....########....
....########....####........####....####........########....####....########....
........................########....########........####....####........####....
........................########....########........####....####........####....
........................########....########........####....####........####....
........................########....########........####....####........####....
####################....................########............########....####....
####################....................########............########....####....
####################....................########............########....####....
####################....................########............########....####....
........................################################....########............
........................################################....########............
........................################################....########............
........................################################....########............
################....########....####....####....####............####....########
################....########....####....####....####............####....########
################....########....####....####....####............####....########
################....########....####....####....####............####....########
The world is still pretty boring, so one thing we can do is generate "rooms". These are large areas of empty space that we can later populate with platforms, elevators, traps and enemies. Generating rooms is pretty easy. Prior to building the map, select a few cells and expand them by random dimensions, changing them to type the "maybe" type ',' We don't want to completely empty them out, so the maze algorithm will still work. So now we have something like this. Better, but still not a game world:
....................########............########....####....####....####....####
########....####................############........................####........
########....####................############........................####........
########....####................############........................####........
########....####................############........................####........
....####....####....####....########............################............####
....####....####....####....########............################............####
....####....####....####....########............################............####
....####....####....####....########............################............####
....####################....################................####....####........
....####################....################................####....####........
....####################....################................####....####........
....####################....################................####....####........
....########............................####....................................
....########............................####....................................
....########............................####....................................
....########............................####....................................
........########....########....############....................................
........########....########....############....................................
........########....########....############....................................
........########....########....############....................................
The next step is to place special characters. These are tiles that will represent platforms, traps, enemies, elevators, items, powerups, etc. Once we have our fully scaled map, we can traverse through and check for certain patterns in which to place our special tile. For example, to place a platform '-', we want to be in a large empty room. So before we place a platform, check to see that the 5 spaces above and below are empty:
........................####............
........................####............
........................####............
........................####.......A....
....####................####....########
....####..............-.####....########
....####................####....########
....####................####....########
############............####............
############........-...####............
############...-........####............
############............####............
......................-.........####....
................................####..-.
................................####....
..A......A...-..................####....
############....-...........########....
############................########....
############................########....
############................########....
Lastly, we want to add some challenge and progression to our maps. We can do this with locked doors and keys. The easiest way is to generate lots of smaller maps, place a random key 'K' inside, enclose the entire thing with a wall and a single locked door as the exit 'D'. Connect all the smaller maps together, and we have a maze that requires some thinking to complete!
############################################################
#..................#............####...#............####...#
#....K.............#.....K......####...#.....K......####...#
#.A........A.......#..A.........####...#..A.........####...#
################...#########...........#########...........#
################.-.#########...........#########...........#
################...#########.-.........#########.-.........#
################...#########...........#########...........#
#..................D............####...D............####...#
#..................#............####...#............####...#
#..................#............####...#............####...#
#.A.............A..#...A........####...#...A........####...#
########....################....################....########
########....################....################....########
########....################.-..################.-..########
########....################....################....########
#..................#...................#...................#
#..................#...................#...................#
#..................#...................#...................#
############################################################
The fun thing about maze generation is that the possibilities are endless. You can instantly generate a new world to explore and jump around in.



Next time I'm going to look into minimaps, and "fog of war". We don't want to be able to spoil the entire maze in one glance!