In our previous post covered generating the streets. Today, we are going to add the buildings. As always, the full script is available in our GitHub repository.
There are three types of images that we will use:
- The basic building foundation. In our example above, we used ten different types of buildings.
- A random number of additional building floors
- Different roof types
Note that for the additional building floors, we need to match the color of the foundation. Therefore, the list needs to be in the same order as the building foundation.
Our approach will be to:
- Load the images .
- Walk through the map and add a building at each blank spot.
- For each building, we can have 1 or more levels and 0 or more roof types.
Loading the Images
Let’s start by loading the images. The Kenney assets that we are using include a spritesheet and an XML file that maps out the sprite sheet. We are going to pull the image assets we need into a list. We will then randomly select based on the size of the list. This will also allow us to align the additional levels with the type of building.
We need to:
- Identify which buildings we want
- Iterate through the XML file to get the spritesheet coordinates
- Load the relevant region into an object
- Append the object to the list
Parsing the XML
Python offers a few different options for parsing XML. We will use the ElementTree API.
import xml.etree.ElementTree as ET
We start by getting the root:
tree = ET.parse(xmlPath)
root = tree.getroot()
We then iterate through the XML sheet. For each node in the sheet, we convert the node name to a number and then check whether that number is in our building list, roof list, or level list. Note that the conversion of the name into a number is hardcoded to Kenney assets. In future iterations, we need to generalize this.
We create a new tile object and store information about the tile (e.g., location and size). We then load the image and store it in the tile.
We add the building and roof tiles to their respective lists. We add the level tiles to a dictionary. This allows us to look up the specific tile that we want.
for child in root:
currentNumber = int(child.attrib['name'][14:][:3])
if currentNumber in buildings:
index = buildings.index(currentNumber)
tile = TileData(name = currentNumber)
tile.setLocation(int(child.attrib['x']),int(child.attrib['y']))
tile.setSize(int(child.attrib['width']),int(child.attrib['height']))
tile.loadImage(spriteSheet)
tile.levelRef = levels[index]
self.buildingList.append(tile)
if currentNumber in levels:
tile = TileData(name = currentNumber)
tile.setLocation(int(child.attrib['x']),int(child.attrib['y']))
tile.setSize(int(child.attrib['width']),int(child.attrib['height']))
tile.loadImage(spriteSheet)
self.levelDict[str(currentNumber)] = tile
if currentNumber in rooves:
tile = TileData(name = currentNumber)
tile.setLocation(int(child.attrib['x']),int(child.attrib['y']))
tile.setSize(int(child.attrib['width']),int(child.attrib['height']))
tile.loadImage(spriteSheet)
self.roofList.append(tile)
Generate the City
We made a slight change to the city generation script. We added a parameter with the number of buildings. We then randomly generate an integer within that range and store the integer in the map.
# Walk through and place buildings
for y in range(0,height):
for x in range(0,width):
if self.cityMap[y][x] in [None,'b']:
self.cityMap[y][x] = random.randint(0,numBuildings-1)
Place the Tiles
Placing the tiles required a little math. Our math that we used in the previous post did not work since the building tiles are taller than the road tiles. However, I realized that the foundation of both the building and the road tiles are 32 pixels, which means we simply need to align from the bottom.
To help with the alignment, I added an adjustment factor that is set to 0 by default. Therefore, it doesn’t change the position of the roads. However, it is set to the height – 101 for the building tiles, which aligns the tiles perfectly.
Adding Building Floors and Roofs
Once we place the building, we next determine whether the building has additional levels and whether to place a roof.
First, we decide how many levels to draw. We use the following statement to generate a random number:
numLevels = random.randint(0,5) * random.randint(0,1)
The randint(0,5) has a uniform distribution. However, we want a slightly higher percentage of no additional levels, so we multiply by a random number that is 0 or 1. This gives slightly higher than 50% chance of a single level building (58%). If there is more than 1 level, we set hasLevels to true, which we will use when we draw the tiles. We also set the tileLevel variable to the corresponding level to the foundation level (obtained through the dictionary).
numLevels = random.randint(0,5) * random.randint(0,1)
if numLevels > 0:
hasLevels = True
levelTile = self.levelDict[str(currentTile.levelRef)]
We also randomly assign a roof using a method similar to the levels. Note that randint generates random numbers that could include both boundaries. For example, randint(0,5) could generate (0,1,2,3,4,5). Therefore, when using with len(someList) ensure you use either randint(0,len(someList)-1) or you decrement on the access someList[randint(0,len(someList))-1]. I use the latter simply because I want 0 to represent no roof anyway.
roofType = random.randint(0,len(self.roofList))
if roofType > 1:
hasRoof = True
roofTile = self.roofList[roofType-1]
Conclusion
This generates a pretty good city. Depending on the game you are making, you may want more or less roads or to tweak the building generation portion to change the probabilities of different buildings (or to ensure that buildings cannot be next to each other). I would also like to add randomized objects (e.g., crosswalks, benches, and street lights) to give the city a more realistic feel.
If you found this post useful, we would love to get your feedback. Please feel free to leave a like or a comment.