What I would probably do is using the inheritance mechanism of C++ and have a level superclass which level subclasses inherits from. The superclass contains virtual functions for creating, destroying, updating and rendering a level.
You can then have a pointer to the current level so that only a single level is updated and rendered at a time.
If each level should be created and handled differently, the inheritance mechanism will work good but if they work exactly the same and every level is created/handled the same, you might not need to use inheritance and instead just have a level class that is instantiated multiple times, one for each level in your game.
Below I've added a simple code example showing how such classes can be constructed/structured and a simple state machine to switch between game states (menu, game, end credits etc).
First we have the level superclass:
class ILevel
{
public:
ILevel (void) {}
virtual ~ILevel (void) {}
virtual void Create (void) = 0;
virtual void Destroy (void) = 0;
virtual void Update (const float DeltaTime) = 0;
virtual void Render (void) = 0;
public:
ILevel* GetPreviousLevel (void) const {return m_pPreviousLevel;}
void SetPreviousLevel (ILevel* pPreviousLevel) {m_pPreviousLevel = pPreviousLevel;}
ILevel* GetNextLevel (void) const {return m_pNextLevel;}
void SetNextLevel (ILevel* pNextLevel) {m_pNextLevel = pNextLevel;}
unsigned int GetID (void) const {return m_ID;}
void SetID (const unsigned int ID) {m_ID = ID;}
std::string& GetName (void) const {return (std::string&)m_Name;}
void SetName (const std::string& rName) {m_Name = rName;}
bool GetIsFinished (void) const {return m_IsFinished;}
protected:
// A pointer to the previous level - might wanna be able to return to the previous level etc
ILevel* m_pPreviousLevel;
// A pointer to the next level for easy transition between the current level and the next
ILevel* m_pNextLevel;
unsigned int m_VBO;
unsigned int m_IBO;
unsigned int m_VAO;
// A numerical ID of the level (1 for level 1, 2 for level 2 etc) might be needed
unsigned int m_ID;
// A string for naming the level might also be needed
std::string m_Name;
bool m_IsFinished;
};
Then we have the class(es) that inherits from this superclass:
class CLevel1 : public ILevel
{
public:
CLevel1 (void);
virtual ~CLevel1 (void);
virtual void Create (void) override;
virtual void Destroy (void) override;
virtual void Update (const float DeltaTime) override;
virtual void Render (void) override;
};
class CLevel2 : public ILevel
{
public:
CLevel2 (void);
virtual ~CLevel2 (void);
virtual void Create (void) override;
virtual void Destroy (void) override;
virtual void Update (const float DeltaTime) override;
virtual void Render (void) override;
};
As you can see above, they looks identical, but you might wanna handle them in different ways and have unique data in each.
Then we have the actual entry point and its mainloop:
enum EGameState
{
MENU = 0,
GAME,
END_CREDITS,
};
ILevel* pCurrentLevel = nullptr;
EGameState GameState;
void Update(void)
{
if(GameState == EGameState::MENU)
{
UpdateMenu();
}
else if(GameState == EGameState::GAME)
{
pCurrentLevel->Update(Time.DeltaTime);
if(pCurrentLevel->GetIsFinished())
{
ILevel* pNextLevel = pCurrentLevel->GetNextLevel();
// Destroy the current level
pCurrentLevel->Destroy();
if(pNextLevel)
{
// Set the new current level (i.e, the next level)
pCurrentLevel = pNextLevel;
// And create it
pCurrentLevel->Create();
}
// If pNextLevel is not valid (i.e it's a nullptr), the current level is the last level
else
GameState = EGameState::END_CREDITS;
}
}
else if(GameState == EGameState::END_CREDITS)
{
UpdateEndCredits();
}
glfwPollEvents();
}
void Render(void)
{
glClearColor(0.2f, 0.2f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if(GameState == EGameState::MENU)
{
RenderMenu();
}
else if(GameState == EGameState::GAME)
{
pCurrentLevel->Render();
}
else if(GameState == EGameState::END_CREDITS)
{
RenderEndCredits();
}
glfwSwapBuffers(window);
}
int main(int argc, char* argv[])
{
InitializeGLFW();
GameState = EGameState::GAME;
ILevel* pLevel1 = new ILevel;
ILevel* pLevel2 = new ILevel;
// Create the actual level, create the VBO, IBO, VAO etc
pLevel1->Create();
pLevel1->SetPreviousLevel(nullptr);
pLevel1->SetNextLevel(pLevel2);
pLevel1->SetID(1);
pLevel1->SetName("Level 1");
pLevel2->SetPreviousLevel(pLevel1);
pLevel2->SetNextLevel(nullptr);
pLevel2->SetID(2);
pLevel2->SetName("Level 2");
// Set the current level
pCurrentLevel = pLevel1;
while(!glfwWindowShouldClose(window))
{
Update();
Render();
};
return 0;
}
This is just an example on how the game can be constructed/structured and you might need to adapt it to work for you and what's working for your specific game.
One think you can also ask yourself is whether you want to have all the levels loaded in memory or if you only want to have the current level in memory. In the end, it depends on your game and if it's a small one, with only a few levels, it might be okay to have them all in memory.