The Game Executable
Prerequisites
- The UObject Base Class
- Transforms in 3D Space
- ECS Design and a Game World
- The Camera Component
- Loading Files and Resources
- Dates and Times - UTimeSystem
- Creating a Window
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:
- Create and initialize our systems
- Create a game window
- Set up some default search paths for resources
- Initialize our objects - for now, start with a camera
- Create a timer for our game loop
- Enter game loop - while the application is running...
- Process messages
- Calculate the time between frames
- Call the Update function on our Systems
- Swap the window buffer
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.