Build a Game Engine

The Game Executable

Prerequisites

Article

With our UObject base class, the ability to place objects in the world, a camera component, a windowing system, logging and event management, a date and time system, and a system to load resources, we are ready to build the skeleton of a game loop. From here on out, we'll start adding systems and components in other modules: an input system to handle IO, a rendering system with mesh and light components to draw things on the screen, a scripting system, and so on. While they will all rely on the Core module, the Core module doesn't need to know about any of their implementations.

We will now turn our attention to creating our game executable. Instead of compiling a new game application that we from our editor (which would require us creating or leveraging a compiler), we can create a generic game executable that is data-driven. Every game created from our engine will have the same executable, but will load different models, scripts, shaders, sound effects, and configuration settings to create unique games.

Before we look at the game executable, let's review the UApplication class that we covered in our ECS Design article, which will allow us to create and find systems with a tick rate:

class UApplication : public UObject {
public: 
    UApplication();
    ~UApplication() = default;

    template<class T> T* CreateSystem(float tickRate = 0) {
        T* sys = new T();
        sys->Initialize();

        RegisteredSystem* rs = new RegisteredSystem();
        rs->system = sys;
        rs->tickRate = (tickRate == 0) ? 0 : (1.0f / tickRate);
        m_systems.push_back(rs);
        return(sys);
    }

    template<class T> T* GetSystem() {
        auto it = m_systems.begin();
        for (; it != m_systems.end(); it++) {
            T* sys = dynamic_cast<T*>((*it)->system);
            if (sys) return(sys);
        }

        return(0);
    }

protected:
    struct RegisteredSystem {
        USystem* system;
        float tickRate;
        float ticker;
    };

public:
    /**
     * Update all systems
     */
    void Update(float delta);

    static UApplication* Instance();

protected:
    // Singleton instance
    static UApplication* m_instance;

    // Registered systems
    std::vector<RegisteredSystem*> m_systems;
};

This class helps us tie together all of the System functionality we've been building so far. As each system is created via the CreateSystem function, its Initiailize function is also called. In this case, that means that the order of creation matters. For example, any systems registering event handlers will require that the EventSystem is already created. In addition, we have an Update helper function that we covered briefly in the Dates and Times article that will iterate over all of our systems and call their Update functions depending on the tick rate:

void UApplication::Update(float delta) {
    // Loop over all systems
    auto it = m_systems.begin();
    for (; it != m_systems.end(); it++) {
        // If tickRate == 0, then update every frame
        if ((*it)->tickRate == 0) {
            (*it)->system->Update(delta);
            continue;
        }

        // Otherwise, increment the time since last update
        (*it)->ticker += delta;

        // If we're over our tick rate, then tick
        while ((*it)->ticker > (*it)->tickRate) {
            // Pass the specified tick rate as the delta
            (*it)->system->Update((*it)->tickRate);

            // Decrement the counter by the tick rate, loop again if needed
            (*it)->ticker -= (*it)->tickRate;
        }
    }
}

Game Executable

Let's start by taking a look at the steps we need to take based on what we have so far:

That's it! As we add things like serialization (which we'll talk about in the next section), then we can start to load some of that data to populate our world from files: entities, their components, scripts, application settings, and more.

Let's look at the main() function of our game executable:

int main(int argc, char** argv) {
    // Initialize application
    UApplication* app = UApplication::Instance();
    UWorld* world = UWorld::Instance();

    // Create systems
    UTimeSystem* timeSystem = app->CreateSystem<UTimeSystem>();
    UEventSystem* eventSystem = app->CreateSystem<UEventSystem>();
    UErrorSystem* errorSystem = app->CreateSystem<UErrorSystem>();
    UResourceSystem* resourceSystem = app->CreateSystem<UResourceSystem>();

    // Add resource paths
    resourceSystem->AddSearchPath("Resources/Shaders");
    resourceSystem->AddSearchPath("Resources/Textures");
    resourceSystem->AddSearchPath("Resources/Models");
    resourceSystem->AddSearchPath("Resources/Scripts");
    resourceSystem->AddSearchPath("Resources/Scenes");

    // Create window
    UWindow* window = app->WindowHandle();
    window->Create("Game Window", 800, 600, false);

    // Get the framebuffer width and height for the camera
    int framebufferWidth, framebufferHeight;
    window->FramebufferSize(framebufferWidth, framebufferHeight);

    // Set up our camera
    UEntity* camera = world->CreateEntity("Main Camera");
    UCameraComponent* camerac = camera->CreateComponent<UCameraComponent>();
    camerac->aspect = (float)framebufferWidth / (float)framebufferHeight;
    camerac->transform.Position(vector3(0, 1, 0));

    // Create a timer for our game loop
    UTimer* timer = new UTimer();
    timer->Start();

    // Enter main game loop
    while (window->IsClosing() == false) {
        // Process any messages
        window->ProcessMessages();

        // Calculate delta seconds between frames
        float delta = timer->Elapsed();
        timer->Reset();

        // Update systems
        app->Update(delta);

        // Swap window buffer
        window->SwapBuffer();

        // Don't take 100% CPU
        timeSystem->Sleep(1);
    }

    return(0);
}

This gives you a pretty good idea of what the main game loop is going to look like. We'll really just be adding more systems at the top and loading more data around where we create our camera. We'll also remove some of the hard coded values like game window title and size and read those from configuration files. Everything else will run locally in the modules we build from here on out.