Creating a City Building Game with SFML Part 10: Putting it All Together

13 Daniel Mansfield Aug 12, 2014

Get started with this tutorial series here!

Now we'll look at the (rather long) update function. We'll split it into several parts.

void City::update(float dt)
{
    double popTotal = 0;
    double commercialRevenue = 0;
    double industrialRevenue = 0;

    /* Update the game time */
    this->currentTime += dt;
    if(this->currentTime < this->timePerDay) return;
    ++day;
    this->currentTime = 0.0;
    if(day % 30 == 0)
    {
        this->funds += this->earnings;
        this->earnings = 0;
    }
    /* Run first pass of tile updates. Mostly handles pool distribution. */
    for(int i = 0; i < this->map.tiles.size(); ++i)
    {
        Tile& tile = this->map.tiles[this->shuffledTiles[i]];

        if(tile.tileType == TileType::RESIDENTIAL)
        {
            /* Redistribute the pool and increase the population total by the tile's population */
            this->distributePool(this->populationPool, tile, this->birthRate - this->deathRate);

            popTotal += tile.population;
        }
        else if(tile.tileType == TileType::COMMERCIAL)
        {
            /* Hire people. */
            if(rand() % 100 < 15 * (1.0-this->commercialTax))
                this->distributePool(this->employmentPool, tile, 0.00);
        }
        else if(tile.tileType == TileType::INDUSTRIAL)
        {
            /* Extract resources from the ground. */
            if(this->map.resources[i] > 0 && rand() % 100 < this->population)
            {
                ++tile.production;
                --this->map.resources[i];
            }
            /* Hire people. */
            if(rand() % 100 < 15 * (1.0-this->industrialTax))
                this->distributePool(this->employmentPool, tile, 0.0);
        }

        tile.update();
    }

Initially we move to the next day if enough time has passed, much like how AnimationHandler::update works. If a month has passed, then the earnings are added to funds and the earnings reset to 0. The bulk of the update function is a series of loops that iterate over every tile in the map (using shuffledTiles of course). These loops must be separate as the order in which various things happen is important.

First, people attempt to move from the populationPool into the residential zones, and the population of each zone is adjusted according to the net birth rate. The commercial zones then attempt to hire people, where they will hire more people the lower the commercialTax is. Finally the industrial zones will attempt to hire people too, but will also extract resources from the ground if any are left. (Remember the resources variable in Map?) We then call update on the tile to change its tileVariant if necessary (i.e. the population is full).

    /* Run second pass. Mostly handles goods manufacture */
    for(int i = 0; i < this->map.tiles.size(); ++i)
    {
        Tile& tile = this->map.tiles[this->shuffledTiles[i]];

        if(tile.tileType == TileType::INDUSTRIAL)
        {
            int receivedResources = 0;
            /* Receive resources from smaller and connected zones */
            for(auto& tile2 : this->map.tiles)
            {
                if(tile2.regions[0] == tile.regions[0] && tile2.tileType == TileType::INDUSTRIAL)
                {
                    if(tile2.production > 0)
                    {
                        ++receivedResources;
                        --tile2.production;
                    }
                    if(receivedResources >= tile.tileVariant+1) break;
                }
            }
            /* Turn resources into goods */
            tile.storedGoods += (receivedResources+tile.production)*(tile.tileVariant+1);
        }
    }

In the second pass, the industrial zones attempt to take any resources from smaller zones that they are connected to via roads or zones (those in the same region) before turning those resources into storedGoods. Each zone can only receive one resource from every other tile, and can only receive one more than its tileVariant in total resources. The larger the tileVariant (and so the larger and more advanced the industrial zone) the more goods it can produce per resource and the more resources it can receive.

    /* Run third pass. Mostly handles goods distribution. */
    for(int i = 0; i < this->map.tiles.size(); ++i)
    {
        Tile& tile = this->map.tiles[this->shuffledTiles[i]];

        if(tile.tileType == TileType::COMMERCIAL)
        {
            int receivedGoods = 0;
            double maxCustomers = 0.0;
            for(auto& tile2 : this->map.tiles)
            {
                if(tile2.regions[0] == tile.regions[0] &&
                    tile2.tileType == TileType::INDUSTRIAL &&
                    tile2.storedGoods > 0)
                {
                    while(tile2.storedGoods > 0 && receivedGoods != tile.tileVariant+1)
                    {
                        --tile2.storedGoods;
                        ++receivedGoods;
                        industrialRevenue += 100 * (1.0-industrialTax);
                    }
                }
                else if(tile2.regions[0] == tile.regions[0] &&
                    tile2.tileType == TileType::RESIDENTIAL)
                {
                    maxCustomers += tile2.population;
                }
                if(receivedGoods == tile.tileVariant+1) break;
            }
            /* Calculate the overall revenue for the tile. */
            tile.production = (receivedGoods*100.0 + rand() % 20) * (1.0-this->commercialTax);

            double revenue = tile.production * maxCustomers * tile.population / 100.0;
            commercialRevenue += revenue;
        }
    }

In the third and final pass, the goods produced by the industrial zones are distributed amongst the commercial zones which then sell the goods. First, any goods in connected industrial zones are moved into the commercial zone producing taxable income for the industrial zones. Any connected residential regions increase the maximum number of customers the commercial zone can receive. We then calculate the revenue the commercial zones generate from selling the goods.

    /* Adjust population pool for births and deaths. */
    this->populationPool = this->adjustPopulation(this->populationPool, this->birthRate - this->deathRate);
    popTotal += this->populationPool;

    /* Adjust the employment pool for the changing population. */
    float newWorkers = (popTotal - this->population) * this->propCanWork;
    newWorkers *= newWorkers < 0 ? -1 : 1;
    this->employmentPool += newWorkers;
    this->employable += newWorkers;
    if(this->employmentPool < 0) this->employmentPool = 0;
    if(this->employable < 0) this->employable = 0;

    /* Update the city population. */
    this->population = popTotal;

    /* Calculate city income from tax. */
    this->earnings = (this->population - this->populationPool) * 15 * this->residentialTax;
    this->earnings += commercialRevenue * this->commercialTax;
    this->earnings += industrialRevenue * this->industrialTax;

    return;
}

In the last part of update we adjust the populationPool based on the net birth rate, and we add new people to the employmentPool if the total population is different from the population on the previous day. This simulates citizens entering the job market, although of course if the population decreases then the number of employable people will drop too. Finally, we tax all the income (as well as the residential zones) and increase earnings by the total amount.

And with that the final class is completed! Now all that's left is to incorporate this into GameStateEditor and put a Gui in that class too. Firstly, we'll replace the map variable in GameStateEditor with a City, city instead. So now instead of this->map.tileSize calls (for example) we will have this->city.map.tileSize calls. (Beware the mapPixelToCoords function if you find and replace map with city.map!) We'll also need to add a guiSystem variable like we did in GameStateStart. Oh, and remember to include <map>, <string, and "gui.hpp"!

Inside the constructor we should of course replace map.load with the City equivalent,

    this->city = City("city", this->game->tileSize, this->game->tileAtlas);
    this->city.shuffleTiles();

And we'll also need to add the Gui system. This one will be considerably longer than in GameStateStart!

/* Create gui elements. */
this->guiSystem.emplace("rightClickMenu", Gui(sf::Vector2f(196, 16), 2, false, this->game->stylesheets.at("button"),
    {
        std::make_pair("Flatten $"          + this->game->tileAtlas["grass"].getCost(),         "grass"),
        std::make_pair("Forest $"           + this->game->tileAtlas["forest"].getCost(),        "forest" ),
        std::make_pair("Residential Zone $" + this->game->tileAtlas["residential"].getCost(),   "residential"),
        std::make_pair("Commercial Zone $"  + this->game->tileAtlas["commercial"].getCost(),    "commercial"),
        std::make_pair("Industrial Zone $"  + this->game->tileAtlas["industrial"].getCost(),    "industrial"),
        std::make_pair("Road $"             + this->game->tileAtlas["road"].getCost(),          "road")
    }));

this->guiSystem.emplace("selectionCostText", Gui(sf::Vector2f(196, 16), 0, false, this->game->stylesheets.at("text"),
    { std::make_pair("", "") }));

this->guiSystem.emplace("infoBar", Gui(sf::Vector2f(this->game->window.getSize().x / 5 , 16), 2, true, this->game->stylesheets.at("button"),
    {
        std::make_pair("time",          "time"),
        std::make_pair("funds",         "funds"),
        std::make_pair("population",    "population"),
        std::make_pair("employment",    "employment"),
        std::make_pair("current tile",  "tile")
    }));
this->guiSystem.at("infoBar").setPosition(sf::Vector2f(0, this->game->window.getSize().y - 16));
this->guiSystem.at("infoBar").show();

The "rightClickMenu" will (as its name implies) be shown when the player presses the right mouse button. It will list all of the possible tiles that they can place along with their prices, and when a tile is chosen currentTile will be set to that tile. Any time the player selects tiles, from then on, will cause the selected tiles to be replaced with currentTile. "selectionCostText" will be displayed when the player is selecting tiles, and will tell them how much the tiles they are placing will cost. It will go red if the player does not have enough funds to place the tiles.

Lastly, "infoBar" will sit and span the bottom of the screen, displaying the game day and other useful information for the player. If we want it to display information we'll have to update that information all the time, so we can put that code inside of update

void GameStateEditor::update(const float dt)
{
    this->city.update(dt);

    /* Update the info bar at the bottom of the screen */
    this->guiSystem.at("infoBar").setEntryText(0, "Day: " + std::to_string(this->city.day));
    this->guiSystem.at("infoBar").setEntryText(1, "$" + std::to_string(long(this->city.funds)));
    this->guiSystem.at("infoBar").setEntryText(2, std::to_string(long(this->city.population)) + " (" + std::to_string(long(this->city.getHomeless())) + ")");
    this->guiSystem.at("infoBar").setEntryText(3, std::to_string(long(this->city.employable)) + " (" + std::to_string(long(this->city.getUnemployed())) + ")");
    this->guiSystem.at("infoBar").setEntryText(4, tileTypeToStr(currentTile->tileType));

    /* Highlight entries of the right click context menu */
    this->guiSystem.at("rightClickMenu").highlight(this->guiSystem.at("rightClickMenu").getEntry(this->game->window.mapPixelToCoords(sf::Mouse::getPosition(this->game->window), this->guiView)));

    return;
}

The first entry of the "infobar" will be the game day, the second the city's funds, the third the population (with the number of homeless in parentheses), the fourth the number of employable people (with the number left unemployed in parentheses), and the fifth the name of the currentTile. We have to first typecast to a long so that we don't get floating point populations appearing (we used a double for ease of calculation but we truncate it here to print the 'real' value). With update done let's make sure that the Gui is actually drawn to the screen.

void GameStateEditor::draw(const float dt)
{
    this->game->window.clear(sf::Color::Black);

    this->game->window.setView(this->guiView);
    this->game->window.draw(this->game->background);

    this->game->window.setView(this->gameView);
    this->city.map.draw(this->game->window, dt);

    this->game->window.setView(this->guiView);
    for(auto gui : this->guiSystem) this->game->window.draw(gui.second);

    return;
}

First, we switch to the guiView and then we draw each Gui in turn like in GameStateStart. The Gui is of course drawn after the city, otherwise the player wouldn't be able to see it! Finally we can add the Gui code to handleInput. Before we get to the switch statement it would be sensible to save ourselves some writing and create gamePos and guiPos variables that record the position of the mouse in gameView and guiView coordinates.

void GameStateEditor::handleInput()
{
    sf::Event event;

    sf::Vector2f guiPos = this->game->window.mapPixelToCoords(sf::Mouse::getPosition(this->game->window), this->guiView);
    sf::Vector2f gamePos = this->game->window.mapPixelToCoords(sf::Mouse::getPosition(this->game->window), this->gameView);

    while(this->game->window.pollEvent(event))

Inside of the MouseMoved event we'll add the code to display the "selectionCostText" Gui.

    else
    {
        this->city.map.select(selectionStart, selectionEnd,
            {
                this->currentTile->tileType,    TileType::FOREST,
                TileType::WATER,                TileType::ROAD,
                TileType::RESIDENTIAL,          TileType::COMMERCIAL,
                TileType::INDUSTRIAL
            });
    }

    this->guiSystem.at("selectionCostText").setEntryText(0, "$" + std::to_string(this->currentTile->cost * this->city.map.numSelected));
    if(this->city.funds <= this->city.map.numSelected * this->currentTile->cost)
        this->guiSystem.at("selectionCostText").highlight(0);
    else
        this->guiSystem.at("selectionCostText").highlight(-1);
    this->guiSystem.at("selectionCostText").setPosition(guiPos + sf::Vector2f(16, -16));
    this->guiSystem.at("selectionCostText").show();
}
/* Highlight entries of the right click context menu */
this->guiSystem.at("rightClickMenu").highlight(this->guiSystem.at("rightClickMenu").getEntry(guiPos));
break;

}

The total cost is of course the cost per tile multiplied by the number of selected tiles. If the city does not has enough funds we highlight the text, making it red. We then position the text to the bottom right of the cursors and show it. We also highlight the entry of the "rightClickMenu" that the player is hovering over. For the MouseButtonPressed event, we hide the Gui when the middle mouse button is pressed.

case sf::Event::MouseButtonPressed:
{
    /* Start panning */
    if(event.mouseButton.button == sf::Mouse::Middle)
    {
        this->guiSystem.at("rightClickMenu").hide();
        this->guiSystem.at("selectionCostText").hide();

When the left mouse button is pressed we select a tile from the "rightClickMenu" if it is visible, or we start selecting tiles if it isn't.

else if(event.mouseButton.button == sf::Mouse::Left)
{
    /* Select a context menu entry. */
    if(this->guiSystem.at("rightClickMenu").visible == true)
    {
        std::string msg = this->guiSystem.at("rightClickMenu").activate(guiPos);
        if(msg != "null") this->currentTile = &this->game->tileAtlas.at(msg);

        this->guiSystem.at("rightClickMenu").hide();
    }
    /* Select map tile. */
    else
    {
        /* Select map tile. */
        if(this->actionState != ActionState::SELECTING)
        {
            this->actionState = ActionState::SELECTING;
            selectionStart.x = gamePos.y / (this->city.map.tileSize) + gamePos.x / (2*this->city.map.tileSize) - this->city.map.width * 0.5 - 0.5;
            selectionStart.y = gamePos.y / (this->city.map.tileSize) - gamePos.x / (2*this->city.map.tileSize) + this->city.map.width * 0.5 + 0.5;
        }
    }
}

When we created the "rightClickMenu" we set the messages to be equal to the name of the tile in the tileAtlas, and so we can easily set currentTile to the one clicked. When an entry is selected we hide the menu. Finally, in the MouseButtonPressed event we handle what happens when the right mouse button is pressed.

else if(event.mouseButton.button == sf::Mouse::Right)
{
    /* Stop selecting. */
    if(this->actionState == ActionState::SELECTING)
    {
        this->actionState = ActionState::NONE;
        this->guiSystem.at("selectionCostText").hide();
        this->city.map.clearSelected();
    }
    else
    {
        /* Open the tile select menu. */
        sf::Vector2f pos = guiPos;

        if(pos.x > this->game->window.getSize().x - this->guiSystem.at("rightClickMenu").getSize().x)
        {
            pos -= sf::Vector2f(this->guiSystem.at("rightClickMenu").getSize().x, 0);
        }
        if(pos.y > this->game->window.getSize().y - this->guiSystem.at("rightClickMenu").getSize().y)
        {
            pos -= sf::Vector2f(0, this->guiSystem.at("rightClickMenu").getSize().y);
        }
        this->guiSystem.at("rightClickMenu").setPosition(pos);
        this->guiSystem.at("rightClickMenu").show();
    }
}
break;

As before, we stop selecting if the right mouse button is pressed, but we also make sure that we hide the "selectionCostText". If the player isn't selecting then we open the "rightClickMenu". The if statements make sure that the Gui is not opened in a position so as to go off the edge of the screen; it will always open with a corner on the mouse cursor, but which corner depends on where the mouse is when the button is pressed. We then set the position of the Gui before showing it.

Next, we'll add the code to change the selected tiles. When the left mouse button is released.

/* Stop selecting. */
else if(event.mouseButton.button == sf::Mouse::Left)
{
    if(this->actionState == ActionState::SELECTING)
    {
        /* Replace tiles if enough funds and a tile is selected */
        if(this->currentTile != nullptr)
        {
            unsigned int cost = this->currentTile->cost * this->city.map.numSelected;
            if(this->city.funds >= cost)
            {
                this->city.bulldoze(*this->currentTile);
                this->city.funds -= this->currentTile->cost * this->city.map.numSelected;
                this->city.tileChanged();
            }
        }
        this->guiSystem.at("selectionCostText").hide();
        this->actionState = ActionState::NONE;
        this->city.map.clearSelected();
    }
}

Now, as well as clearing the selection, we hide the "selectionCostText" and replace the selected tiles if the city has enough funds to do so. We also call tileChanged to update the regions and roads.

Lastly, we need to readjust the dimensions and position of the "infobar" when the screen is resized.

/* Resize the window. */
case sf::Event::Resized:
{
    gameView.setSize(event.size.width, event.size.height);
    gameView.zoom(zoomLevel);
    guiView.setSize(event.size.width, event.size.height);
    this->guiSystem.at("infoBar").setDimensions(sf::Vector2f(event.size.width / this->guiSystem.at("infoBar").entries.size(), 16));
    this->guiSystem.at("infoBar").setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, event.size.height - 16), this->guiView));
    this->guiSystem.at("infoBar").show();
    this->game->background.setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, 0), this->guiView));
    this->game->background.setScale(
        float(event.size.width) / float(this->game->background.getTexture()->getSize().x), 
        float(event.size.height) / float(this->game->background.getTexture()->getSize().y));
    break;
}

This is just the same code as we had in the constructor, but placed in the resize function instead.

Phew, that was a lot of code! Try compiling and playing the game, hopefully it does everything it's supposed to... I hope you enjoyed this adventure into SFML, there's still a lot you can do with this game! Perhaps add some new power station, pylon tiles and create an electricity system (add another region to help distribute the electricity) or make the zones demand water. You could also add some nice background music (use the sf::Music class) or add the option to create a new game instead of just continuing from an existing one. Above all, enjoy yourself whilst you do it, and happy programming!

Source code for this section

13 comments


Or enter your name and Email
  • CS Claudio Silvestri 7 months ago
    Someone can add the program that works? I followed every step of the tutorial but my code doesn't work. Thanks
  • O Ollie 7 months ago
    So i have the base game running but I have a problem with my animations. the water tiles are working but none of the others (residential,commercial,industrial) are updating correctly. Any help or advice would be greatly appreciated!
  • DC Daniel Cooke 1 year ago
    If you are getting a vector subscript out of range: you need to change the FOR loops in city.cpp :: update from: for (int i = 0; i < this->map.tiles.size() ; ++i) to: for (int i = 0; i < this->map.tiles.size() -1; ++i). If you are not seeing a map, you need city_cfg.dat, you can create this yourself in the constructor or grab it from the git https://github.com/Piepenguin1995/citybuilder/tree/master/Part%2010%20-%20Putting%20it%20All%20Together
  • H HoodedSpectre 2 years ago
    I figured out why It wasn't creating the map if anyone is still having issues. You need to create the city_cfg.dat in order to get it to properly load.
    • H HoodedSpectre 2 years ago
      width=64 height=64 day=0 populationPool=50 employmentPool=25 population=50 employable=25 birthRate=0.00055 deathRate=0.00023 residentialTax=0.05 commercialTax=0.05 industrialTax=0.05 funds=30000 earnings=0
  • NL N3RD L1F3 2 years ago
    So i worked through this great tutorial....but now at the end I am having a problem getting it to run. Once i start the game by clicking the button, I get the background and the gui but no Map. If i right click, the gui menu appears but if i left click i get a vector subscript error. I am new to C++ so my understanding is pretty basic. I was able to debug to the point of where it crashes, which is in map.cpp. line: for (int x = start.x; x <= end.x; ++x) { /* Check if the tile type is in the blacklist. If it is, mark it as * invalid, otherwise select it */ this->selected[y*this->width + x] = 1; ++this->numSelected; if I understand correctling by left clicking it is trying to select a tile, but since there is no map, it can not select a tile and crashes..... So my question, why does it not create a map when i start the game? Any help would be appriciated.
    • H HoodedSpectre 2 years ago
      I'm not sure if you found a solution or not but I posted one.
  • JB Josh Braun 2 years ago
    Just wandering how to run/compile the game??
  • E Edg 2 years ago
    I got the vector subscript out of range error on VS2013 as well. It turns out that the shuffledTiles should range from 0 to map.tiles.size()-1, instead of 1 to map.tiles.size(). I changed the 3rd parameter in the call to std::iota to 0 from 1 to fix this. The statement is now: std::iota(shuffledTiles.begin(), shuffledTiles.end(), 0);
  • DW Don Wilson 2 years ago
    Just an FYI, there's likely an issue with the city earnings from taxation calculations. At the very bottom of update() when calculating the city earnings we're multiplying the commercial and industrial tax rates against the post-taxed commercial/residential revenues, not the pre-taxed revenues.
    • DW Don Wilson 2 years ago
      Not to mention there's no method City::adjustPopulation(), that line should be: this->populationPool += this->populationPool * (this->birthRate - this->deathRate);
  • H Harry 3 years ago
    I get exactly the same problem as Tom. Think I've narrowed it down to the: this->selected[y*this->width + x] = 1; within Map::select(....) line 263 on Map.cpp on the GitHub source code (https://github.com/Piepenguin1995/citybuilder/blob/master/map.cpp#L263) But no idea how to fix it. Great tutorial project tho!
  • T Tom 3 years ago
    Hey, not sure if you keep up with this tutorial,but I've found a bug in the code where if I compile this in VS2013 I receive a "vector out of subscript range" error once I make the first for loop pass in City::update. Not sure if you ran into this error, but I've followed this tutorial closely so I'm not entirely sure where my code might differ.