Get started with this tutorial series here!
With the Tile
class now in place we can put them to use by combining them into a Map
. The Map
class will contain a 2D array of Tile
s as well as a bunch of helper variables and functions for altering the array. This will be our largest class yet (definition-wise, there aren’t too many declarations) as some of the functions are complicated. But anyway, let’s examine map.hpp
#ifndef MAP_HPP
#define MAP_HPP
#include <SFML/Graphics.hpp>
#include <string>
#include <map>
#include <vector>
#include "tile.hpp"
class Map
{
private:
void depthfirstsearch(std::vector<TileType>& whitelist,
sf::Vector2i pos, int label, int type);
public:
unsigned int width;
unsigned int height;
std::vector<Tile> tiles;
/* Resource map */
std::vector<int> resources;
unsigned int tileSize;
unsigned int numSelected;
unsigned int numRegions[1];
/* Load map from disk */
void load(const std::string& filename, unsigned int width, unsigned int height,
std::map<std::string, Tile>& tileAtlas);
/* Save map to disk */
void save(const std::string& filename);
/* Draw the map */
void draw(sf::RenderWindow& window, float dt);
/* Checks if one position in the map is connected to another by
* only traversing tiles in the whitelist */
void findConnectedRegions(std::vector<TileType> whitelist, int type);
/* Update the direction of directional tiles so that they face the correct
* way. Used to orient roads, pylons, rivers etc */
void updateDirection(TileType tileType);
/* Blank map constructor */
Map()
{
this->tileSize = 8;
this->width = 0;
this->height = 0;
this->numRegions[0] = 1;
}
/* Load map from file constructor */
Map(const std::string& filename, unsigned int width, unsigned int height,
std::map<std::string, Tile>& tileAtlas)
{
this->tileSize = 8;
load(filename, width, height, tileAtlas);
}
};
#endif /* MAP_HPP */
The width
and height
values are just the dimensions of the map, which we have to remember because we aren’t using a 2D array to contain the tiles (I’m sorry, I lied); we’re using a single std::vector
and just pretending that it’s two dimensional. This makes it much simpler (and more efficient) to iterate through when the position of the tile is not needed, and it isn’t very complicated when the position is needed either.
The resources
std::vector
will be used later by the industrial zone tiles in order to produce their goods. Each tile will have a limited amount of resources that the zone can extract, which is what that array manages. Zones will also buy goods from other (smaller) zones that do have resources, and so industrial zones will not simply accelerate their production and then suddenly become useless. But that’s all for another tutorial! Right now we have a few simple variables, and then finally some functions. We will use a custom binary format to load and save the maps, which we’ll examine when we define the functions. Then there’s the now familiar draw
function, and some rather simple constructors.
There’s three functions that we haven’t discussed; findConnectingRegions
, depthfirstsearch
, and updateDirection
. updateDirection
is simple (if tedious to implement) and iterates over each Tile
in the Map
, changing their tileVariant
so as to change their animation and orient (if they have a correctly created texture) them in the correct direction. The other two functions will be used to split the Map
into different regions, labeling them according to what region they fall in. The whitelist
argument is simply a std::vector
containing all the different TileType
s that can make up the region. So if you wanted a “greenery” region you might pass TileType::GRASS
and TileType::FOREST
as arguments. type
is the index of the regions
array in the Tile
class that the region information should be stored in. Currently we only have one possible region, but as mentioned above more will be added later.
Now let’s begin creating map.cpp
#include <SFML/Graphics.hpp>
#include <string>
#include <map>
#include <vector>
#include <fstream>
#include "map.hpp"
#include "tile.hpp"
/* Load map from disk */
void Map::load(const std::string& filename, unsigned int width, unsigned int height,
std::map<std::string, Tile>& tileAtlas)
{
std::ifstream inputFile;
inputFile.open(filename, std::ios::in | std::ios::binary);
this->width = width;
this->height = height;
for(int pos = 0; pos < this->width * this->height; ++pos)
{
this->resources.push_back(255);
TileType tileType;
inputFile.read((char*)&tileType, sizeof(int));
switch(tileType)
{
default:
case TileType::VOID:
case TileType::GRASS:
this->tiles.push_back(tileAtlas.at("grass"));
break;
case TileType::FOREST:
this->tiles.push_back(tileAtlas.at("forest"));
break;
case TileType::WATER:
this->tiles.push_back(tileAtlas.at("water"));
break;
case TileType::RESIDENTIAL:
this->tiles.push_back(tileAtlas.at("residential"));
break;
case TileType::COMMERCIAL:
this->tiles.push_back(tileAtlas.at("commercial"));
break;
case TileType::INDUSTRIAL:
this->tiles.push_back(tileAtlas.at("industrial"));
break;
case TileType::ROAD:
this->tiles.push_back(tileAtlas.at("road"));
break;
}
Tile& tile = this->tiles.back();
inputFile.read((char*)&tile.tileVariant, sizeof(int));
inputFile.read((char*)&tile.regions, sizeof(int)*1);
inputFile.read((char*)&tile.population, sizeof(double));
inputFile.read((char*)&tile.storedGoods, sizeof(float));
}
inputFile.close();
return;
}
First we open a binary std::ifstream
for the file specified. If you are not familiar with std::ifstream
, it is the C++ way of reading from files. It inherits from the same class std::cin
does (both are input streams) and so has a very similar interface, although we won’t be using that here as we are dealing with binary files, not text files. Instead we will use the read
member function. Once we’ve opened the stream we set width
and height
and then we loop enough times to read every Tile
from the stream, read
ing the data upon each loop. read
requires a char*
as the first argument, which can be interpreted as a pointer to the start of an array of individual bytes (a char
is the size of a byte).
The second argument is the number of bytes to read. If we pass a pointer to the variable we want to read and pretend it’s a char*
pointer by using a cast, read
will fill the variable with the data we want. We repeat this for all the variables we require. Note that in our first read
call we are also pretending that tileType
is of type int
and not an enum
; this is valid, as enum
s are (kind of) the same as int
s, just referred to differently.
void Map::save(const std::string& filename)
{
std::ofstream outputFile;
outputFile.open(filename, std::ios::out | std::ios::binary);
for(auto tile : this->tiles)
{
outputFile.write((char*)&tile.tileType, sizeof(int));
outputFile.write((char*)&tile.tileVariant, sizeof(int));
outputFile.write((char*)&tile.regions, sizeof(int)*1);
outputFile.write((char*)&tile.population, sizeof(double));
outputFile.write((char*)&tile.storedGoods, sizeof(float));
}
outputFile.close();
return;
}
As you might expect, save
is load
in reverse. Well almost, we’re still processing the file in the same direction, so mostly we just replace the read
s with write
s! And we replace the std::ifstream
with an std::ofstream
of course. We then have a for
loop, although in a form you won’t be familiar with unless you’ve seen some c++11
; it says “for each tile
in this->tiles
, do the following” The auto
keyword is another c++11
feature, and can be used instead of the variable’s type if the type is obvious. ‘auto’ is far easier to write than std::vector<Tile>::iterator
, and without c++11
the loop would look far worse.
void Map::draw(sf::RenderWindow& window, float dt)
{
for(int y = 0; y < this->height; ++y)
{
for(int x = 0; x < this->width; ++x)
{
/* Set the position of the tile in the 2d world */
sf::Vector2f pos;
pos.x = (x - y) * this->tileSize + this->width * this->tileSize;
pos.y = (x + y) * this->tileSize * 0.5;
this->tiles[y*this->width+x].sprite.setPosition(pos);
/* Draw the tile */
this->tiles[y*this->width+x].draw(window, dt);
}
}
return;
}
With the saving and loading functions done, we can write the code to draw the Map
. For this we need to know the coordinates of the Tile
, and so we use some normal nested for
loops (y
before x
so we iterate horizontally first, and then vertically). We then have some formulae, seemingly conjured out of thin air, that convert the Map
coordinates (x,y)
to world coordinates that we can use to draw each Tile
to the screen. They aren’t conjured from thin air however, and are created with just a small amount of reasoning!

Consider an isometric grid, where the very top point has coordinates (0, 0)
, the x
axis extends along the right edge and the y
axis extends along the left edge. If we increase x
by 1, then pos.x
will increase by tileSize
(remember tileSize
is half the width, or the height, of each sprite) and pos.y
will increase by tileSize * 0.5
.
pos.x = x * this->tileSize;
pos.y = x * this->tileSize * 0.5;
If we increase y
by 1, then pos.x
will decrease by tileSize
(follow along the grid lines) and pos.y
will increase by tileSize * 0.5
.
pos.x = x * this->tileSize - y * this->tileSize;
pos.y = x * this->tileSize * 0.5 + y * this->tileSize * 0.5;
With a tiny bit of algebra we get
pos.x = (x - y) * this->tileSize;
pos.y = (x + y) * this->tileSize * 0.5;
The way we’ve set up our coordinates means that all sprites to the left will have negative world coordinates. We don’t want that, so we shift the world coordinates to the right by half the width of the Map
(in pixels). Once we’ve converted to the world coordinates, we set the position of the Tile
‘s sprite and then draw it. Because we’re using a 1D std::vector
instead of a 2D array, we have to convert our (x,y)
coordinates to a single value by using index = y * this->width + x
.
That’s all for now, in the next tutorial we will complete the definitions of the Map
member functions!
Author: Daniel Mansfield