1
\$\begingroup\$

I have been learning opengl and I have made good progress over past few months. However I still struggle to understand game logic in C++, I am new to C++ too.

Say I have this program.

#include <headers>

int main()
{
    #Step 1
    VAO_VBO_EBO_inits();

    while(!glfwWindowShouldClose(window))
    {
        glClearColor(.2f, .2f, .3f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT);

        #Step 2
        DrawCalss();

        glfwPollEvents();
        glfwSwapBuffers(window);
    };
    return 0;
};

For OpenGL programmers this should be clear. With this structure, I have to load all levels and then draw part of them like so.

Step 1 feeds the gpu the vertex data and its attributes.

Step 2 binds shader programs and draws the vertices

Say I have A, B, C, D levels, within VAO_VBO_EBO_inits(); function I have to load all A, B, C, D levels then within DrawCalss(); function will draw only related levels.

DrawCalss(); function is dynamic but VAO_VBO_EBO_inits(); invoked only once.

What I want to do is create separate game loops for each levels and be able to switch between levels, and as I switch levels preceding level should be unloaded and next level reloaded.

Since I am new to C++ I don't know what way would be the most efficient, that's why I have not tried anything. How do I achieve it? what is the best practice for such scenarios?

\$\endgroup\$
3
  • \$\begingroup\$ I'm most familiar with seeing this done in a data-driven way. Rather than having a different game loop for each level, you have one game loop, that updates and draws objects that have been loaded into a data structure like a scene graph. Loading a level just means emptying the contents of that data structure and replacing it with a new batch of content. \$\endgroup\$ – DMGregory Mar 19 '20 at 18:36
  • \$\begingroup\$ @DMGregory but what about memory allocation, what if Level B is heavier than Level A? as far as I know we don't have a way of resizing pre-allocated memory in C++? \$\endgroup\$ – user133668 Mar 20 '20 at 8:50
  • \$\begingroup\$ Often games pre-allocate for the worst case, then fill as much as they need and tolerate some "wasted" empty space much of the time, because it's faster than allocating and de-allocating the exact right size all the time, and saves fragmentation. You can ask a question about this specifically and someone with deeper knowledge of game engine memory management in C++ can answer in more detail. \$\endgroup\$ – DMGregory Mar 20 '20 at 8:57
1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy