I'm glad you're really appreciating the suggestions. A lot of people don't care about the code design at all (mostly to their own disadvantage, at least in the long term). I can tell you a lot about how I implemented my tasks and what I consider practical, however that's not the only solution (as usual, considering design choices). There might be different circumstances in your case, or you might doubt a concrete approach. You have to know, I am not happy with all of my decisions, but sometimes you have just to try out something, or it is never going to be implemented.
I hope you can nevertheless apply some ideas to your project. Just ask if something is not clear.
I'm now working on a graphic manager class, any extra tips on the interface for this would be great.
Okay. First, I wrote for every game element a class. In my jump'n'run, there were enemies, the player, projectiles, and so on. These classes inherit from a common base class which stores things like position and velocity. Then, there is the main logic-handling class called Game. It stores the game elements (each type in a separate STL container), and inside a member function Game::Output() – not mandatory the best name – I passed the container elements to GraphicManager.
The GraphicManager class provides several overloaded Draw() functions, each taking a const reference to the desired logic object.
void Draw(const Player& player);
void Draw(const Enemy& enemy);
void Draw(const Projectile& projectile);
// ...
Inside, the functions create local sprites, set them up according to the objects' states (by invoking getter methods, for example Player::GetPosition()) and call MyRenderWindow.Draw(sprite); where MyRenderWindow is a member of GraphicManager (to be exact, only a reference to an sf::RenderWindow, but that depends on the implementation). Creating every frame new sprites might be a performance issue, if you have got really really many of them. In my case, this was never a problem, so I implement it the easy way. Separating graphics from logics becomes more difficult otherwise, a std::map is one of the alternatives.
How would you (or anyone) go about drawing the map? I have read many tutorials on tile based games, but none seem to cover the basic question of how to actually build the basic tile engine.
Sadly, there aren't really good tutorials regarding such stuff. I have learned only very few things from tutorials. Anyway, you gain the most experience by developping own projects and experimenting with new things. A very important thing is in my opinion the knowledge about C++ itsself. I don't only mean the raw language features, although they can be very complex and yet powerful (for example templates), but also common techniques like RAII and modern design patterns. There are a lot of good books (Meyers, Sutter, Alexandrescu) which can teach you quite a lot about effective and modern programming.
How would you (or anyone) go about drawing the map? [...] Currently I have a nested for loop to work through an array (that will be filled from a file later), I imagine there isn't many choices for this part,
You're right, a 2-dimensional array (or better container) is the usual way. Note that you only draw the tiles that are really required (calculate the screen bounds, divide by tile size and you get the tile indices).
but then I have an ugly series of if statements to work out which tile has what attributes and which sprite to use there. Should I make a tile class (probably yes, make a class for everything :wink:) and what sort of functions would it need? It probably needs a general map class to tell it what to do as well?
A class is certainly no bad idea here. However, you don't need to put everything in a class, we're not in Java. For example, you should generally rather use global functions when appropriate (i.e. when they don't need access to private member data).
But for tiles, a class is helpful, especially if they become more complicated. In my project, the tile class just stores some state about its representation. I had a public enum in the class to name the distinct tile types and a private member variable to store the type. Additionally, there were related getter and setter functions. Depending on your specific needs, you can establish further methods to form the interface.
class Tile
{
public:
enum TileType
{
Empty,
Grass,
Rock,
// ...
};
public:
void SetType(TileType Type);
TileType GetType() const;
private:
TileType MyType;
// other tile-related logic data
};
My design decision was not to store the tile position in the tiles themselves. Their position is given by the index in the nested container, and duplicating information is always bad. Not only because it consumes unnecessarily memory, you also have to make sure both informations are always consistent. Besides (at least in my case), a tile doesn't care about where it stands, so why should it know about the position?
Now comes the interesting part, namely the integration of tiles into game logics and graphics. It's standing to reason that we can store the tiles together with other logic elements, in a new nested container inside the Game class. In my project, it first looked like this:
std::vector<std::vector<Tile> > MyTiles;
But now I'm using a TileMap class. This is just for convenience, because I need some special operations (for example resizing the map at the edges, and so on). You actually don't need an own class for this. Maybe, a typedef helps you not to write std::vector<std::vector<Tile> > or whatever all the time (and to be able to change the container type).
The tiles are looped through as usual and each of them is passed to GraphicManager. Here, the overload has more parameters: The tile indices. This is required because as mentioned, the tiles don't store their position, and the GraphicManager has to know where to draw them.
void GraphicManager::Draw(const Tile& tile, unsigned int x, unsigned int y);
Invoking the function is straightforward:
void Game::OutputTiles()
{
// calculate bounds
for (unsigned int x = BeginX; x < EndX; ++x)
{
for (unsigned int y = BeginY; y < EndY; ++y)
{
GraphicMgr.Draw(Tiles[x][y], x, y);
}
}
}
GraphicMgr is here not a member of Game, but member of a superior, singleton-like class holding all the managers. The identifier GraphicMgr is a macro for convenience (actually, a reference would be prettier, but I came into trouble because at definition time, the target wasn't constructed yet). Just if you wonder...