Creating a City Building Game with SFML Part 4: Tiles

Get started with this tutorial series here!

Up until now we haven’t really discussed how the game will work, but it’s time to fix that! The city will be constructed from a 2D grid of tiles (drawn isometrically) that will evolve over time. The player will be able to (using their city’s funds) bulldoze areas and place new tiles, then watch as their population grows over time. We will only have a few tiles; grass, forest, water, roads, and three zones; residential, commercial, and industrial. We won’t go any further in this tutorial series, but it should be easy to add whatever kind of tile you like!

Residential zones will house the city’s population, commercial zones will sell goods and employ people, and industrial zones will supply those goods to the commercial zones, as well as employ people themselves. The goods will be transported via roads, and currently the other tiles will be purely aesthetic, other than imposing restrictions on what can be placed where. You won’t be able to place a commercial zone over a river, for example. Let’s start by examining the Tile class (in tile.hpp)

  1. #ifndef TILE_HPP
  2. #define TILE_HPP
  4. #include <SFML/Graphics.hpp>
  5. #include <vector>
  7. #include "animation_handler.hpp"
  11. std::string tileTypeToStr(TileType type);
  13. class Tile
  14. {
  15.     public:
  17.     AnimationHandler animHandler;
  18.     sf::Sprite sprite;
  20.     TileType tileType;
  22.     /* Tile variant, allowing for different looking versions of the
  23.      * same tile */
  24.     int tileVariant;
  26.     /* Region IDs of the tile, tiles in the same region are connected.
  27.      * First is for transport */
  28.     unsigned int regions[1];
  30.     /* Placement cost of the tile */
  31.     unsigned int cost;
  33.     /* Current residents / employees */
  34.     double population;
  35.     /* Maximum population per growth stage / tile variant */
  36.     unsigned int maxPopPerLevel;
  37.     /* Maximum number of building levels */
  38.     unsigned int maxLevels;
  39.     /* Production output per customer/worker per day, either monetary or goods */
  40.     float production;
  41.     /* Goods stored */
  42.     float storedGoods;
  44.     /* Constructor */
  45.     Tile() { }
  46.     Tile(const unsigned int tileSize, const unsigned int height, sf::Texture& texture,
  47.         const std::vector<Animation>& animations,
  48.         const TileType tileType, const unsigned int cost, const unsigned int maxPopPerLevel,
  49.         const unsigned int maxLevels)
  50.     {
  51.         this->tileType = tileType;
  52.         this->tileVariant = 0;
  53.         this->regions[0] = 0;
  55.         this->cost = cost;
  56.         this->population = 0;
  57.         this->maxPopPerLevel = maxPopPerLevel;
  58.         this->maxLevels = maxLevels;
  59.         this->production = 0;
  60.         this->storedGoods = 0;
  62.         this->sprite.setOrigin(sf::Vector2f(0.0f, tileSize*(height-1)));
  63.         this->sprite.setTexture(texture);
  64.         this->animHandler.frameSize = sf::IntRect(0, 0, tileSize*2, tileSize*height);
  65.         for(auto animation : animations)
  66.         {
  67.             this->animHandler.addAnim(animation);
  68.         }
  69.         this->animHandler.update(0.0f);
  70.     }
  72.     void draw(sf::RenderWindow& window, float dt);
  74.     void update();
  76.     /* Return a string containing the display cost of the tile */
  77.     std::string getCost()
  78.     {
  79.         return std::to_string(this->cost);
  80.     }
  81. };
  83. #endif /* TILE_HPP */

There’s quite a lot here but not much that is complicated! The first thing that you might not have seen before is the enum class line. A c++11 feature, it’s a standard enum but you access it like a static class variable. So instead of just writing GRASS you have to write TileType::GRASS. This is makes it almost equivalent to including the enum in its own namespace, but without the ability to add a using namespace line.

We then declare the tileTypeToStr function which converts the specified enum entry to a string. Whilst it would be nicer to include both of these in some kind of class or namespace, I feel it makes the rest of the code too complicated and ugly to bother! Up until the constructor we have some standard variable declarations, whose comments describe. Two things should be mentioned: firstly, our tiles will use the tileVariant variable in order to change appearance but provide the same function. For zones the population (employees) that they can support is proportional to their variant. So a tileVariant = 0 could support maxPopPerLevel (say 50) and may look like some small shops, whereas a tileVariant = 4 could support 5*maxPopPerLevel and may look like a large shopping center.

For other tiles the variant is purely cosmetic, and for roads will be used to store their orientation information so they point the right way. Secondly, why have we used an array for the regionsvariable? That’s purely some cheeky foresight, in the future we’ll want to know not only which tiles are connected together by roads, but we may also want to know which tiles are connected electrically and which are in the same watered region.

The constructor itself is a little more complicated, taking quite a few arguments. tileSize and height are the half width of the tile’s sprite, measured in pixels, and the height of the tile, measured in tiles (or multiples of the half width). This will be either 1, for roads and small buildings, or 2 for larger buildings. We use this to set the origin of the sprite and to calcuate the correct frame size for the animation. Using the setOrigin function we change what is regarded as (0,0) on the sprite; here we set it to (0,tileSize*(height-1)). The sprite is drawn starting from the origin position, so this ensures that the tiles will always be drawn in the correct place, regardless of their size.


After setting the frameSize to the correct dimensions we pass each of the specified animations to the animation handler and then update it once in order to initialise everything correctly. Unlike with our GameState class update does not have a timestep parameter. This is because it should be called every time a new game day occurs, which will be the same for all tiles. It is therefore a waste to keep track of the time since each tile’s update. Also unlike our GameState class, draw takes an additional sf::RenderWindow parameter. This is so the tiles do not have any knowledge of the Game class, which they do not need.

Moving on to tile.cpp,

  1. #include <SFML/Graphics.hpp>
  3. #include "animation_handler.hpp"
  4. #include "tile.hpp"
  6. void Tile::draw(sf::RenderWindow& window, float dt)
  7. {
  8.     /* Change the sprite to reflect the tile variant */
  9.     this->animHandler.changeAnim(this->tileVariant);
  11.     /* Update the animation */
  12.     this->animHandler.update(dt);
  14.     /* Update the sprite */
  15.     this->sprite.setTextureRect(this->animHandler.bounds);
  17.     /* Draw the tile */
  18.     window.draw(this->sprite);
  20.     return;
  21. }
  23. void Tile::update()
  24. {
  25.     /* If the population is at the maximum value for the tile,
  26.      * there is a small chance that the tile will increase its
  27.      * building stage */
  28.     if((this->tileType == TileType::RESIDENTIAL ||
  29.         this->tileType == TileType::COMMERCIAL ||
  30.         this->tileType == TileType::INDUSTRIAL) &&
  31.         this->population == this->maxPopPerLevel * (this->tileVariant+1) &&
  32.         this->tileVariant < this->maxLevels)
  33.     {
  34.         if(rand() % int(1e4) < 1e2 / (this->tileVariant+1)) ++this->tileVariant;
  35.     }
  37.     return;
  38. }
  40. std::string tileTypeToStr(TileType type)
  41. {
  42.     switch(type)
  43.     {
  44.         default:
  45.         case TileType::VOID:            return "Void";
  46.         case TileType::GRASS:           return "Flatten";
  47.         case TileType::FOREST:          return "Forest";
  48.         case TileType::WATER:           return "Water";
  49.         case TileType::RESIDENTIAL:     return "Residential Zone";
  50.         case TileType::COMMERCIAL:      return "Commercial Zone";
  51.         case TileType::INDUSTRIAL:      return "Industrial Zone";
  52.     }
  53. }

Covering draw first, we change the animation to whatever tileVariant is. This is handy as it means that we can place all of the sprites for each tile in a single file, with the animation frames extending to the right and the tile variants extending downwards as separate animations. We then update the animation and use the created bounds variable (the section of the texture that the frame is in) to tell the sprite which area to display. Finally we draw the tile to the screen. In the update function we check to see if we are dealing with a zone, and if we are we give the zone a chance to advance to the next tile variant if the population is outgrowing the current one.

What’s with the 1e4 and 1e2 though? As you probably know, rand generates a number between 0 and RAND_MAX, which on my system is equal to 2147483647 (but is guaranteed to be at least 32767). Ideally we’d use rand() < 0.1 / (this->tileVariant+1) where rand generates a number between 0 and 1, and so we to take rand() % 10000 to get a number from 0 to 10000 and then multiply the other side by 10000 to get an equivalent expression using the actual version of rand. If we make the value we mod by larger than RAND_MAX we run into problems, so 10000 is really the largest we can go safely. Essentially the chance is 10% for tileVariant = 05% for tileVariant = 13.33% for tileVariant = 2, and so on.

Lastly we have our non-member function that converts the TileType to a string, using nothing but a simple switch statement. We put default first with no break so that if an unlisted TileTypeoccurs (say we add a new one but forget to update the function) the function will just return "void". Since we’re using returns we don’t need breaks in any of the other cases either!

With the Tile class done (for now), we can actually create the tiles we’ll be using. We’ll do this using what I like to call a “tile atlas,” which will be an std::map from a string to a Tile. Whenever we want a new tile, we copy an existing one from the tile atlas. Using an std::map instead of an array just makes it easier for us; it’s much easier to remember that "forest" is the forest tile than it is to remember that 4 is the index for a forest (fourest?). We’ll create this atlas, tileAtlas, inside of the Game class.

  1. private:
  3. void loadTextures();
  4. void loadTiles();
  6. public:
  8. const static int tileSize = 8;
  10. std::stack<GameState*> states;
  12. sf::RenderWindow window;
  13. TextureManager texmgr;
  14. sf::Sprite background;
  16. std::map<std::string, Tile> tileAtlas;
  18. void pushState(GameState* state);

Don’t forget to include the necessary headers as well! (<map><string>, and "tile.hpp") Note the new tileSize variable. This is equal to half width the width of each tile (in pixels), as we mentioned earlier. It’s the same for all tiles and remains constant, so we’ve made it const static. We’ve also declared a loadTextures function that will populate the atlas, and that we will now define in game.cpp

  1. void Game::loadTiles()
  2. {
  3.     Animation staticAnim(0, 0, 1.0f);
  4.     this->tileAtlas["grass"] =
  5.         Tile(this->tileSize, 1, texmgr.getRef("grass"),
  6.             { staticAnim },
  7.             TileType::GRASS, 50, 0, 1);
  8.     tileAtlas["forest"] =
  9.         Tile(this->tileSize, 1, texmgr.getRef("forest"),
  10.             { staticAnim },
  11.             TileType::FOREST, 100, 0, 1);  
  12.     tileAtlas["water"] =
  13.         Tile(this->tileSize, 1, texmgr.getRef("water"),
  14.             { Animation(0, 3, 0.5f),
  15.             Animation(0, 3, 0.5f),
  16.             Animation(0, 3, 0.5f) },
  17.             TileType::WATER, 0, 0, 1);
  18.     tileAtlas["residential"] =
  19.         Tile(this->tileSize, 2, texmgr.getRef("residential"),
  20.             { staticAnim, staticAnim, staticAnim,
  21.             staticAnim, staticAnim, staticAnim },
  22.             TileType::RESIDENTIAL, 300, 50, 6);
  23.     tileAtlas["commercial"] =
  24.         Tile(this->tileSize, 2, texmgr.getRef("commercial"),
  25.             { staticAnim, staticAnim, staticAnim, staticAnim},
  26.             TileType::COMMERCIAL, 300, 50, 4);
  27.     tileAtlas["industrial"] =
  28.         Tile(this->tileSize, 2, texmgr.getRef("industrial"),
  29.             { staticAnim, staticAnim, staticAnim,
  30.             staticAnim },
  31.             TileType::INDUSTRIAL, 300, 50, 4);
  32.     tileAtlas["road"] =
  33.         Tile(this->tileSize, 1, texmgr.getRef("road"),
  34.             { staticAnim, staticAnim, staticAnim,
  35.             staticAnim, staticAnim, staticAnim,
  36.             staticAnim, staticAnim, staticAnim,
  37.             staticAnim, staticAnim },
  38.             TileType::ROAD, 100, 0, 1);
  40.     return;
  41. }

Include the "animation_handler.hpp" header this time! First we’ve created a new Animationin order to save ourselves some writing. It’s named staticAnim as we will use it if the tile only has one frame of animation (regardless of how many different animations it has). Thus we give it a start and end frame of 0 and a duration of 1 second, although it doesn’t really matter what we set for that. Now we actually create the tiles and add them to the atlas. You should be able to understand most of it by looking at the Tile constructor, but we’ll step through the first to save you checking back.

We first add an entry with the key "grass" to the std::map and assign it a new Tile with a tileSize equal to the const static one inside the Game class, a height equal to 1, the texture named "grass", a single animation (which in this case is the static one), the tileTypeTileType::GRASS, a construction cost of 50, a population per tile variant of 0 and a maximum number of tile variants of 1. If you’ve never seen the { staticAnim, Animation(0, 3, 0.5f) } code before, this is a new c++11 feature that gives us an easier way of passing std::vectors (and others) to functions without creating them in advance. Personally it’s my favorite feature, as not only is it clear (you can initialize an array using that syntax already!) it’s a big time saver.

We’ll also need to load the textures that all these tiles will need, since as you can see we require them to create the tiles! Our updated loadTextures function will look like

  1. void Game::loadTextures()
  2. {
  3.     texmgr.loadTexture("grass",         "media/grass.png");
  4.     texmgr.loadTexture("forest",        "media/forest.png");
  5.     texmgr.loadTexture("water",         "media/water.png");
  6.     texmgr.loadTexture("residential",   "media/residential.png");
  7.     texmgr.loadTexture("commercial",    "media/commercial.png");
  8.     texmgr.loadTexture("industrial",    "media/industrial.png");
  9.     texmgr.loadTexture("road",          "media/road.png");
  11.     texmgr.loadTexture("background",    "media/background.png");
  12. }

Finally, make sure that you call the loadTiles function inside of the constructor

  1. Game::Game()
  2. {
  3.     this->loadTextures();
  4.     this->loadTiles();
  6.     this->window.create(sf::VideoMode(800, 600), "City Builder");
  7.     this->window.setFramerateLimit(60);
  9.     this->background.setTexture(this->texmgr.getRef("background"));
  10. }

That’s the end for the tiles at the moment, let’s move on to creating a game map!

Source code for this section

Author: Daniel Mansfield