HowTo: Break up a table(switch) into multiple files

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
User avatar
cyboryxmen
Posts: 190
Joined: November 14th, 2014, 2:03 am

HowTo: Break up a table(switch) into multiple files

Post by cyboryxmen » January 18th, 2018, 3:59 am

I will assume that most of you have already learnt about polymorphism. Otherwise, this tutorial will be lost on you.

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;
		}
	}
}
This structure does not scale well when adding more levels to it. For one, you’ll need to keep including more and more headers into it every time you add another level. Every single one of those includes are going to significantly ramp up your compilation time and then some depending on how many times the level loader header itself is included. Also, the switch statement is going to be nearly unmanageable as the number of test cases sprawls downwards with the length of nearly 10 times the height of your computer screen! The process of adding a level would look something like this.

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_;
}
Course, this will not fix the issue by itself. Like all other C++ functions, somebody still has to call the AddCreator() function to add the level in; whether that be the level loader or some other master class. The only exception to this rule is main(). Being the entrypoint to all standard C++ programs, main() would simply be called automatically at the start of the program.

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";
}
Taking advantage of this, we can make the level load itself into the level loader at the start of the program automatically without a master class ordering it to do so.

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;
There is a problem with using globals that depend on each other in C++ though. Generally, you cannot depend on globals to be initialised in a specific order. This problem becomes relevant when you realise that the level might add itself to the level loader before the level loader is even created! This can however be easily fixed using globals local to a function.

Code: Select all

LevelLoader& GetInstance ( )
{
	// Will be created the first time the function is called.
	static LevelLoader instance;
	
	return instance;
}
As you might know, globals localised in a function are standardised to always be initialised upon the first time the function is called. This way, we can make sure that LevelLoader is always initialised the when we call GetInstance()!

Code: Select all

// LevelLoader will always be initialised when GetInstance() is called!
LevelLoader::GetInstance ( ).AddCreator ( 1, this );
With this technique, no longer must we deal with incredibly long and hard to maintain tables(switch statements)! To extend the table, simply add another .cpp to the project that'll add a new entry to the table. To remove it from the table, simply remove it from the project entirely! It's that easy to manage!

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.
Zekilk

User avatar
chili
Site Admin
Posts: 3948
Joined: December 31st, 2011, 4:53 pm
Location: Japan
Contact:

Re: HowTo: Break up a table(switch) into multiple files

Post by chili » January 19th, 2018, 1:35 am

Nice post!

A similar idea can also be found in the scenes in 3D Fundamentals, in HUGS, and coming up in the future in Project Twin.
Chili

Post Reply