A procedurally generated city (2d)-Part 3 : Creating the game map


The plan

Now that we are able to get a squarish city-like thing, we need to differentiate on what every square means for the game.

We need to define all the types of tiles that we are going to be using. We will be using the following:

  • roads
  • blocks
  • 2 different kind of buildings
  • 2 different kind of roofs
public enum CellType{  
   Block,
   Road,
   Building1,
   Building2,
   Roof1,
   Roof2;

   public static EnumSet<CellType> Buildings = EnumSet.of(CellType.Building1, CellType.Building2);
   public static EnumSet<CellType> Roofs = EnumSet.of(CellType.Roof1, CellType.Roof2);
}

Let's also add a new step to our CityGenerator.create() method. Instead of returning a list of LNodes, let's return an actual GameMap.

public GameMap create(int iterations, int width, int height){  
   Array<CityNode> nodes = new Array<CityNode>();
   this.mapWidth = width;
   this.mapHeight = height;
   CityBlock seed = new CityBlock(0, 0, width, height, opt);

   nodes.add(seed);
   Array<CityNode> temp = new Array<CityNode>();
   for (int i = 0; i < iterations; i++){
       step(nodes, temp);
       nodes.clear();
       nodes.addAll(temp);
       temp.clear();
   }
   return createMap(nodes);
}

private GameMap createMap(Array<CityNode> nodes){  
   GameMap map = new GameMap();
   map.getProperties().put("width", this.mapWidth);
   map.getProperties().put("height", this.mapHeight);
   map.getProperties().put("tilewidth", Constants.TILE_SIZE);
   map.getProperties().put("tileheight", Constants.TILE_SIZE);

   TileMapLayer cellsLayer = createCityLayout(map, nodes);
   map.getLayers().add(cellsLayer);

   fillBlocks(map, nodes.select(new Predicate<CityNode>() {
      @Override
      public boolean evaluate(CityNode node) {
         return node.type == CellType.Block;
      }
   }));
   return map;
}

The custom map classes

Before explaining how the createCityLayout or fillBlocks methods work, you should know that I am using a set of custom classes which extend libgdx's classes for tiled maps:

public class GameMap extends TiledMap {

   public Array<Rectangle> roads = new Array<Rectangle>();
   public Array<Rectangle> buildings = new Array<Rectangle>();
   public Array<Vector2> initialSpawns = new Array<Vector2>();
   public Vector2 playerSpawn;

   public boolean isWalkable(int x, int y){
      TiledMapTileLayer floorLayer = (TiledMapTileLayer) getLayers().get("floor");

      TiledMapTileLayer.Cell cell = floorLayer.getCell(x, y);
      if (cell == null)
         return false;

      TiledMapTile tile = cell.getTile();
      return tile != null & tile.getProperties().get("walkable", false, Boolean.class);
   }

   public boolean isWalkable(Vector2 point){
      return isWalkable(point.x, point.y);
   }

   public boolean isWalkable(float x, float y){
      return isWalkable(MathUtils.floor(x), MathUtils.floor(y));
   }

   public int getWidth(){
      return getProperties().get("width", Integer.class);
   }

   public int getHeight(){
       return getProperties().get("height", Integer.class);
   }

   public Rectangle getRoadAt(Vector2 xy) {
       for(int i = 0; i < roads.size; i++){
           if (roads.get(i).contains(xy)){
               return roads.get(i);
           }
       }
       return null;
   }
}


public class TileMapLayer extends TiledMapTileLayer {

   public TileMapLayer(int width, int height, int tileWidth, int tileHeight) {
      super(width, height, tileWidth, tileHeight);
   }

   public void setCell(int x, int y, TileCell cell) {
      super.setCell(x, y, cell);
   }

   public TileCell getCell(int x, int y){
      return (TileCell)super.getCell(x, y);
   }
}


public class TileCell extends Cell {  
   private CellType type;

   public TileCell(){ }

   public TileCell(CellType type){
      this.type = type;
   }

   public void setType(CellType type){
      this.type = type;
   }

   public CellType getType(){
      return type;
   }    
}

Creating the city layout

This is by far the easiest step of the city generation process in my mind, just dump the city nodes generated in the previous tutorials to our TileMapLayer for the map to have them stored:

private TileMapLayer createCityLayout(TiledMap map, Array<CityNode> nodes){  
   int width = map.getProperties().get("width", Integer.class);
   int height= map.getProperties().get("height", Integer.class);
   int tileWidth = MathUtils.floor(map.getProperties().get("tilewidth", Float.class));
   int tileHeight = MathUtils.floor(map.getProperties().get("tileheight", Float.class));

   TileMapLayer bgLayer = new TileMapLayer(width, height, tileWidth, tileHeight);
   bgLayer.setName("floor");

   for(CityNode node : nodes)
      for (int x = (int)node.bounds.x; x < node.bounds.x + node.bounds.width; x++)
         for (int y = (int)node.bounds.y; y < node.bounds.y + node.bounds.height; y++){           
            if(bgLayer.getCell(x, y) != null)
               continue;

            TileCell bgCell = new TileCell(node.type);
            bgLayer.setCell(x, y, bgCell);
         }
   return bgLayer;
}

Filling the blocks

Now when we talk about filling the blocks, we mean to have a delimited block which contains one or more buildings in it. We will have a padding of 1 tile in each block, representing our sidewalk. Graphically we will have this:

City Layout

private void fillBlocks(GameMap map, Iterable<CityNode> blockNodes) {

   TileMapLayer bgLayer = (TileMapLayer)map.getLayers().get("floor");

   for(CityNode node : blockNodes) {
      if (node.bounds.width - 2 < (opt.minBldWidth * 3) || node.bounds.height - 2 < opt.minBldHeight)
         continue;

      int lastBldX = (int)node.bounds.x + 1;
      int bldCount = (int)(node.bounds.width - 2) / opt.minBldWidth; // the amount of buildings to place in this block
      CellType prevRoof = null;
      CellType prevBld = null;
      CellType roofType = null;
      CellType buildingType = null;

      for(int i = 0; i < bldCount; i++){
         while (buildingType == prevBld)
            buildingType = buildings[MathUtils.random(buildings.length - 1)];
         while(roofType == prevRoof)
            roofType = roofs[MathUtils.random(roofs.length - 1)];

         int roofH = MathUtils.random(2, 4);
         int roofY = (int)node.bounds.y + (int)node.bounds.height - 1 - roofH;
         int y1 = MathUtils.random((int)node.bounds.y + 1, (int)node.bounds.y + 2);
         int y2 = (int)node.bounds.y + (int)node.bounds.height - 1;
         int x2 =lastBldX + opt.minBldWidth;

         fill(buildingType, lastBldX, y1, x2, roofY, bgLayer);
         fill(roofType, lastBldX, roofY, x2, y2, bgLayer);

         lastBldX += opt.minBldWidth;  

    // if we can't fit the exact amount of buildings in this block, just fill up the rest of the block with the same building. Say minimum building width is 3 tiles, the block is 10 tiles wide, so we have 8 tiles for "buildings", this means we will have 2 buildings, one of 3 tiles, the other one will be 5 tiles
         if (i == bldCount - 1){
            x2 = (int)node.bounds.width + (int)node.bounds.x - 1;

            fill(buildingType, lastBldX, y1, x2, roofY, bgLayer);
            fill(roofType, lastBldX, roofY, x2, y2, bgLayer);
         }

         prevRoof = roofType;
         prevBld = buildingType;
      }

      map.buildings.add(buildingCollision);
   }
}

private void fill(CellType cellType, int x1, int y1, int x2, int y2, TileMapLayer bgLayer){  
   for (int x = x1; x < x2; x++){
      for (int y = y1; y < y2; y++){
         TileCell bgCell = bgLayer.getCell(x, y);
         if (bgCell == null)
            continue;
         bgCell.setType(cellType);
      }
   }
}
comments powered by Disqus