Engine Loop
Main Game Loop
The engine loop is the heartbeat of the game, executing once per frame and coordinating all systems.
Basic Loop Structure
int main() {
Engine* engine = Engine::Create();
engine->init(800, 600, "My Game");
MyScene* scene = engine->createScene<MyScene>();
engine->loadScene(scene);
// Main game loop
while (engine->running()) {
engine->updateScene(); // Update scene logic
engine->update(); // Update physics, input, timers, audio
engine->render(); // Render frame with Vulkan
}
engine->cleanup();
Engine::Destroy(engine);
return 0;
}
Loop Phases
1. UpdateScene (Game Logic)
engine->updateScene();
Calls the active scene’s UpdateScene() method:
void Scene::UpdateScene(Engine* engine) {
// Per-frame game logic
// Update HUD, check level completion, manage enemies, etc.
}
Responsibilities:
- Update scene-specific logic
- Manage level progression
- Control ambient effects
- Handle scene transitions
2. Update (Engine Systems)
engine->update();
Coordinates all engine subsystems in order:
2.1 Input Polling:
glfwPollEvents()- Get keyboard, mouse, window events- Gamepad state updated
- Cursor position recorded
- Mouse scroll delta processed
2.2 Timing:
- Calculate elapsed time since last frame
- Update delta time
- Used by CharacterController, physics, and user code
2.3 Game Object Updates:
forEach (GameObject in active scene) {
gameObject->Update(engine); // User-defined per-object logic
}
Each GameObject’s Update() is called:
- Input handling
- Local state updates
- Sound playback
- Movement logic
2.4 Physics Step:
physicsScene->simulate(deltaTime); // PhysX simulation
physicsScene->fetchResults(); // Get collision results
Simulation:
- Apply gravity
- Update velocities
- Detect collisions
- Apply collision responses
- Update rigid body positions
2.5 Collision Processing:
- Process collision callbacks (onCollision)
- Process trigger callbacks (onTriggerEnter/onTriggerExit)
- Update CharacterController position
2.6 Transform Synchronization:
- Update Vulkan model matrices from GameObject transforms
- Update PhysX actor positions from GameObjects
- Sync CharacterController position to camera
2.7 Camera Update:
- Calculate view matrix from
cameraPositionandcameraRotation - Calculate projection matrix from aspect ratio and FOV
- Update view frustum for rendering
2.8 Audio System Update:
- Update FMOD system (
fmodSystem->update()) - Sync 3D listener to camera position
- Update all 3D sound source positions
- Process audio playback and effects
2.9 Timer Processing:
oneShotTimers.update(currentTime); // Process event timers
Execute callbacks for timers that have elapsed:
engine->addTimer(2.5f, []() {
std::cout << "2.5 seconds have passed\n";
});
2.10 Resource Cleanup:
checkResourceDestroy(); // Clean up destroyed resources
checkGameObjectDestroy(); // Clean up destroyed objects
checkTriggerDestroy(); // Clean up destroyed triggers
checkCharacterControllerDestroy(); // Clean up destroyed controllers
checkSceneDestroy(); // Clean up destroyed scenes
Uses deferred deletion queues for safe Vulkan cleanup.
3. Render (Frame Submission)
engine->render();
Records and submits Vulkan frame:
3.1 Frame Synchronization:
waitForFence(inFlightFences[currentFrame]);
resetFence(inFlightFences[currentFrame]);
Wait for GPU to finish previous frame before reusing buffers.
3.2 Acquire Swapchain Image:
vkAcquireNextImageKHR(swapChain, imageAvailableSemaphore, ...)
Get next image to render to.
3.3 Update Uniform Buffers:
updateUniformBuffer(currentFrame, ubo);
// Contains view and projection matrices
3.4 Command Buffer Recording:
beginCommandBuffer(commandBuffer);
vkCmdBeginRenderPass(renderPass);
vkCmdBindPipeline(graphicsPipeline);
forEach (GameObject with mesh in scene) {
bindVertexBuffer(mesh->vertices);
bindIndexBuffer(mesh->indices);
bindDescriptorSets(frameDescriptorSet, textureDescriptorSet);
vkCmdDrawIndexed(mesh->indexCount);
}
renderUI(); // UI elements
renderImGui(); // Debug UI
vkCmdEndRenderPass();
endCommandBuffer(commandBuffer);
3.5 Command Buffer Submission:
VkSubmitInfo submitInfo = {
.waitSemaphoreCount = 1,
.pWaitSemaphores = &imageAvailableSemaphore,
.commandBufferCount = 1,
.pCommandBuffers = &commandBuffer,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &renderFinishedSemaphore
};
vkQueueSubmit(graphicsQueue, &submitInfo, inFlightFence);
Submit recorded commands to GPU.
3.6 Swapchain Presentation:
VkPresentInfoKHR presentInfo = {
.waitSemaphoreCount = 1,
.pWaitSemaphores = &renderFinishedSemaphore,
.swapchainCount = 1,
.pSwapchains = &swapChain,
.pImageIndices = &imageIndex
};
vkQueuePresentKHR(presentQueue, &presentInfo);
Display rendered frame on screen.
Frame Timing
Delta Time
float getDeltaTime();
// Returns: seconds elapsed since last frame
Used for frame-rate independent movement:
void Update(Engine* engine) {
float dt = engine->getDeltaTime();
Vector3 moveAmount = moveDirection * speed * dt;
transform.position += moveAmount;
}
Frame Rate
Engine targets v-sync (60 FPS typically):
- Waits for vertical blank before presenting
- Prevents screen tearing
- Provides consistent timing
Timing Accuracy
Delta time is accurate to milliseconds; suitable for:
- Physics simulation
- Animation playback
- Smooth movement
- Event scheduling with
addTimer()
Update Order Summary
while (engine->running()) {
│
├─ Scene::UpdateScene() [USER LOGIC]
│
├─ Engine::update()
│ ├─ Input polling (glfwPollEvents)
│ ├─ Delta time calculation
│ ├─ GameObject::Update() for each object [USER LOGIC]
│ ├─ Physics simulation (PhysX step)
│ ├─ Collision processing
│ ├─ Transform synchronization
│ ├─ Camera matrix update
│ ├─ Audio system update
│ ├─ Timer processing (callbacks)
│ └─ Deferred destruction (cleanup queues)
│
└─ Engine::render()
├─ Swapchain synchronization
├─ Uniform buffer update
├─ Vulkan command buffer recording
│ ├─ Render pass begin
│ ├─ Draw GameObjects
│ ├─ Draw UI elements
│ ├─ Render ImGui
│ └─ Render pass end
├─ Command buffer submission
└─ Swapchain presentation
Scene Transitions
Scenes are transitioned at specific points in the loop:
while (engine->running()) {
engine->updateScene();
engine->update();
engine->render();
// Check if scene wants to transition
if (engine->isLastFrame()) { // Transitioned from previous frame
auto scene = static_cast<MyScene*>(engine->getActiveScene());
if (scene && scene->loadNewScene) {
scene->loadNewScene = false;
engine->loadScene(scene->sceneToLoad); // Load new scene
// Old scene: DestroyScene() called, objects destroyed
// New scene: EarlyInitScene(), then InitScene() called
}
}
}
Timing:
- Scene load request happens in
updateScene()orupdate() - Actual transition happens on next loop iteration after
render() - Ensures all systems complete before switching scenes
Initialization Sequence
Called once when engine starts:
engine->init(width, height, "title");
Sequence:
- Window Creation - GLFW window initialization
- Vulkan Initialization:
- Instance creation
- Physical device selection
- Logical device and queues
- Swapchain and framebuffers
- Renderpass and graphics pipeline
- Command pools and buffers
- Descriptor pools and layouts
- Semaphores and fences
- PhysX Initialization:
- Physics foundation
- Physics scene
- Default material
- FMOD Audio Initialization:
- FMOD system creation
- Channel groups
- 3D listener setup
- ImGui Setup:
- ImGui context creation
- GLFW and Vulkan backends
- Input System:
- GLFW input callbacks
- Gamepad polling setup
- DualSense Setup (if available):
- Controller detection
- Haptics support
Cleanup Sequence
Called when engine shuts down:
engine->cleanup();
Engine::Destroy(engine);
Sequence:
- Active Scene Cleanup -
DestroyScene()called - GameObjects Destroyed - All objects in scene destroyed
- Resources Destroyed - Meshes, textures, sounds freed
- ImGui Cleanup - ImGui context destroyed
- FMOD Cleanup - Audio system shut down
- PhysX Cleanup - Physics scene and foundation cleaned up
- Vulkan Cleanup:
- Wait for device idle
- Destroy pipelines, shaders, descriptors
- Destroy buffers and images
- Destroy swapchain and framebuffers
- Destroy device and instance
- Window Cleanup - GLFW window destroyed
Best Practices
- Keep UpdateScene fast: Complex logic should be in GameObject::Update()
- Don’t create/destroy in Update: Use request methods, let engine cleanup
- Frame-rate independent: Always use
getDeltaTime()for movement - Input in UpdateScene: Process input for global logic, per-object in GameObject::Update()
- Physics continuous: Don’t manually move objects with large jumps; use forces instead
- Timer precision: Timers are accurate to about 1 frame; use for event scheduling, not animation
- Avoid blocking calls: Don’t use sleep() or wait(); will freeze the game
- Profile bottlenecks: Use engine profiling to find slow systems
Checking Loop Status
bool running = engine->running();
// false when window closed or engine->exit() called
bool lastFrame = engine->isLastFrame();
// true for one frame after render, useful for deferred operations
Exit Handling
Request engine shutdown gracefully:
engine->exit();
// Sets internal flag; loop exits on next iteration
The main loop then:
while (engine->running()) { // Now false
// Loop exits
}
engine->cleanup();