Some of you would have probably encountered something like this situation before. You’re trying to make a level loader that takes the number of the level and loads it. It’ll probably look something like this:
Code: Select all
class LevelLoader
{
public:
LevelLoader ( ) = default;
Level* LoadLevel ( const size_t i )
{
switch ( i )
{
case 1:
return new Level1;
case 2:
return new Level2;
case 3:
return new Level3;
case 4:
return new Level4;
case 5:
return new Level5;
case 6:
return new Level6;
case 7:
return new Level7;
case 8:
return new Level8;
case 9:
return new Level9;
}
}
}
1) Create the level.
2) Include it into the level loader.
3) Scroll down to an obvious spot on the switch and place it there.
Wouldn’t it be nice to skip steps 2 and 3? That way, to add another level to the level loader, you just had to make one. It would also be a lot easier if you can break up these switch cases and put them into their relevant files.
Maybe if we make the level loader a global, the levels can add themselves to the level loader.
Code: Select all
class LevelLoader
{
public:
static LevelLoader& GetInstance ( )
{
return instance;
}
void AddCreator ( const size_t i, Level::Creator*const creator )
{
if ( i >= level_creators_.size ( ) )
{
level_creators_.resize ( i + 1 );
}
level_creators_ [ i ] = creator;
}
Level* LoadLevel ( const size_t i )
{
return level_creators_ [ i ].Create ( );
}
private:
static LevelLoader instance;
LevelLoader ( ) = default;
~LevelLoader ( ) = default;
std::vector<Level::Creator*> level_creators_;
}
Despite this, main() isn’t necessarily the first function to be called in a program. C++ is standardised to initialise a program’s globals first afterall. If those globals were an object with a constructor or an int initialised with a function, they will be called first before main() does.
Code: Select all
static auto dummy = [ ]
{
std::cout << "This would be called first.\n";
return 0;
} ( );
class Global
{
public:
Global ( )
{
std::cout << "This would also be called first.\n";
}
};
static Global global;
int main ( )
{
std::cout << "This would be called afterwards.\n";
}
Code: Select all
class Level1 : public Level
{
class Creator : public Level::Creator
{
public:
Creator ( )
{
LevelLoader::GetInstance ( ).AddCreator ( 1, this );
}
Level1* Create ( ) override
{
return new Level1;
}
}
// Insert code here
}
static Level1::Creator creator;
Code: Select all
LevelLoader& GetInstance ( )
{
// Will be created the first time the function is called.
static LevelLoader instance;
return instance;
}
Code: Select all
// LevelLoader will always be initialised when GetInstance() is called!
LevelLoader::GetInstance ( ).AddCreator ( 1, this );
Not to mention, this encapsulates the different levels completely. You don't even need a header anymore. Just make a .cpp and isolate the level's definition completely from the rest of the program. Full encapsulation!
Plus, compile times would no longer outlast the heat death of the universe! Without having to include any headers at all(versus including one for every single level), the program can just compile the individual .cpp(s) dramatically decreasing complexity in compilation!
Finally, this technique makes it very easy to work in teams. Once you get the level loader and level interface done, the team can just individually work on the different levels independently without having to coordinate to get the levels into the level loader and such!
Really, the only downside to this technique is that it heavily relies on globals which some people would want to avoid like the plague(with perfectly valid reasons really). I myself are one of those people since you can pretty much work without globals just fine. In the same way, you can totally write a program by manually writing down the 1s and 0s into the executable yourself instead of C++! Writing it in C++ just makes things easier and better for your sanity. And really, that should be more than enough for me to use this technique on all of my projects.
As a demonstration, I created a repository to demonstrate this technique in action.