Best Practices for Using Dear ImGui in Game Development
Dear ImGui is an immediate-mode GUI library designed for real-time game loops, and following best practices means keeping UI construction side-effect-free, initializing backends in the exact frame order shown in the official examples, and rendering the draw data after your game scene but before post-processing.
Dear ImGui, maintained in the ocornut/imgui repository, is an immediate-mode GUI library purpose-built for real-time applications such as games. Because it redraws every frame, integrating it correctly requires adhering to patterns defined in the core source (imgui.h and imgui.cpp) and the official backend files. This article outlines the essential best practices for using ImGui in game development, derived directly from the library's source code and example projects.
Understand the Three-Layer Architecture
Dear ImGui is divided into three loosely coupled layers. Recognizing this separation prevents common integration mistakes.
-
Core API — The
ImGui::namespace inimgui.handimgui.cppdefines widgets, layout logic, and internal state. This layer is platform and renderer agnostic. -
Backend — Platform-specific code that feeds input and handles rendering. Each backend follows the
imgui_impl_XXXnaming convention, such asbackends/imgui_impl_glfw.cppfor input andbackends/imgui_impl_opengl3.cppfor rendering. -
Examples — Minimal demos such as
examples/example_glfw_opengl3/show the full startup, frame, and shutdown flow. These are the canonical reference for integration order.
Follow the Canonical Frame Lifecycle
As documented in docs/EXAMPLES.md, the frame lifecycle must be followed precisely to avoid input lag or rendering errors.
-
Create a context with
ImGui::CreateContext(). -
Initialize each backend with
ImGui_ImplXXX_Init(). -
Each frame, call the backend's
NewFrame()helpers first, thenImGui::NewFrame(). -
Build the UI using
ImGui::widget calls. -
Finalize and render by calling
ImGui::Render(), thenImGui_ImplXXX_RenderDrawData(). -
On shutdown, call
ImGui_ImplXXX_Shutdown()followed byImGui::DestroyContext().
Calling ImGui::NewFrame() before the backend's NewFrame() is a frequent source of input offset and lag.
Keep UI Code Side-Effect-Free
Immediate-mode UI runs every frame, so mutating game state during UI construction can produce inconsistent frames. The safest pattern is to read state into temporary variables and apply changes after the frame ends.
// UI layer only reads
bool wantPause = ImGui::Checkbox("Pause", &gamePaused);
// Apply side effects after ImGui::Render()
if (wantPause) SetPaused(gamePaused);
Avoid Per-Frame Heap Allocations
Allocation overhead can stall the frame budget, especially on consoles. Use static buffers or stack storage for text inputs and temporary arrays rather than constructing standard containers every frame.
static char nameBuf[64];
ImGui::InputText("Name", nameBuf, IM_ARRAYSIZE(nameBuf));
Batch Draw Calls and Minimize State Changes
Dear ImGui already batches vertices internally in its draw lists. The provided OpenGL3, Vulkan, and DirectX backends are optimized to emit a minimal number of draw calls. Avoid switching shaders or viewports inside the ImGui render pass, and keep the backend implementations largely unchanged unless your engine has specific constraints.
Manage Fonts and High-DPI Scaling
Large font atlases increase texture memory and bandwidth. Load only the fonts you need, and respect display scaling by setting io.FontGlobalScale for high-DPI displays.
ImGuiIO& io = ImGui::GetIO();
io.Fonts->AddFontFromFileTTF("Roboto-Medium.ttf", 16.0f);
io.FontGlobalScale = 1.0f / io.DisplayFramebufferScale.x;
Separate the UI Render Pass
Render ImGui after your main scene geometry but before post-processing effects such as bloom or tone-mapping. This keeps UI elements crisp and on top without being affected by world-space shaders.
In a typical forward renderer, the order is:
RenderScene();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
PostProcessUI(); // if applying any UI-specific effects
Persist UI State Across Sessions
Storing window open states, collapse flags, and user preferences prevents players from rearranging the UI every session. You can use ImGui::GetStateStorage() to cache values by ID.
ImGuiStorage* storage = ImGui::GetStateStorage();
bool open = storage->GetBool(storage->GetIntId("MyWindowOpen"), true);
ImGui::Begin("MyWindow", &open);
storage->SetBool(storage->GetIntId("MyWindowOpen"), open);
ImGui::End();
Profile UI Cost Using Built-In Metrics
Keep an eye on vertex throughput and draw-list growth using ImGuiIO metrics counters. Spikes in vertex count often indicate overly complex panels.
ImGuiIO& io = ImGui::GetIO();
printf("Vertices: %d\n", io.MetricsRenderVertices);
Backend-Specific Best Practices
GLFW and OpenGL3
Use the modern imgui_impl_opengl3.cpp backend for programmable-pipeline engines. The legacy OpenGL2 backend is only appropriate for fixed-function pipelines. The minimal integration in examples/example_glfw_opengl3/ is the canonical starting point.
DirectX 12
In backends/imgui_impl_dx12.cpp, the application must manage command-list and descriptor-heap lifetimes. Create a persistent command list and reuse descriptor heaps rather than allocating them per frame.
Vulkan
The Vulkan backend in backends/imgui_impl_vulkan.cpp caches pipelines per render pass. Pre-create a VkDescriptorSetLayout for ImGui textures, and ensure your render pass is compatible across frames to avoid pipeline recreation.
Metal
The Metal backend in backends/imgui_impl_metal.mm uses a single MTLRenderPassDescriptor. Set clearColor to transparent when overlaying UI so that the game scene remains visible underneath.
Common Pitfalls to Avoid
Several recurring mistakes are documented in the source comments and docs/FAQ.md.
-
Calling
ImGui::NewFrame()before the backendNewFrame()produces offset UI and input lag. Always callImGui_ImplXXX_NewFrame()first. -
Mixing OpenGL2 and OpenGL3 backends causes crashes or missing shaders. Match the backend to your engine's graphics API and pipeline model.
-
Enabling
io.MouseDrawCursorcontinuously creates sluggish cursor behavior at high frame rates. Enable it only during drag operations; otherwise, let the OS handle cursor drawing. -
Using large per-frame buffers in
InputTextleads to excessive heap traffic. Prefer fixed-size static buffers orImGui::InputTextWithHintwith a predetermined size. -
Neglecting DPI scaling makes UI elements appear tiny on Hi-DPI monitors. Always multiply sizes and positions by
io.DisplayFramebufferScale.
Production-Ready Integration Example
Below is a minimal, production-ready integration that applies the best practices above. It keeps UI state separate from game state, initializes the GLFW and OpenGL3 backends, and follows the exact frame order prescribed in docs/EXAMPLES.md.
// ------------------------------------------------------------
// 1. Global structs (keep UI state separate from game state)
// ------------------------------------------------------------
struct UIState {
bool showDemo = false;
bool showConsole = true;
};
static UIState gUI;
// ------------------------------------------------------------
// 2. Initialization (once, at engine start)
// ------------------------------------------------------------
void InitializeImGui(GLFWwindow* window)
{
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
// ---- Font (optional) ----
io.Fonts->AddFontFromFileTTF("misc/fonts/Roboto-Medium.ttf", 16.0f);
io.FontGlobalScale = 1.0f / io.DisplayFramebufferScale.x;
// ---- Backend init ----
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330 core");
}
// ------------------------------------------------------------
// 3. Per-frame UI code (called after game update, before present)
// ------------------------------------------------------------
void RenderImGui()
{
// 3.1 Backend new-frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// --------------------------------------------------------
// 3.2 Build UI
// --------------------------------------------------------
if (gUI.showDemo)
ImGui::ShowDemoWindow(&gUI.showDemo);
// Example: simple console overlay
if (gUI.showConsole) {
ImGui::Begin("Console", &gUI.showConsole);
static char cmdBuf[256] = "";
ImGui::InputText("Command", cmdBuf, IM_ARRAYSIZE(cmdBuf));
if (ImGui::Button("Run")) {
// Process command after the frame to keep UI side-effect-free
// (store it somewhere, execute later)
}
ImGui::End();
}
// --------------------------------------------------------
// 3.3 Rendering
// --------------------------------------------------------
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
// ------------------------------------------------------------
// 4. Shutdown (engine exit)
// ------------------------------------------------------------
void ShutdownImGui()
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
}
Summary
-
Dear ImGui divides work into the core API (
imgui.h/imgui.cpp), platform backends (imgui_impl_XXX), and official examples. -
Frame order matters: always call backend
NewFrame()beforeImGui::NewFrame(), and render after the scene but before post-processing. -
Keep UI code side-effect-free by reading game state into temporaries and applying changes after the frame ends.
-
Avoid per-frame allocations with static buffers, and profile cost using
io.MetricsRenderVertices. -
Choose the backend that matches your renderer, and manage backend-specific resources such as descriptor heaps or render passes carefully.
Frequently Asked Questions
What is the correct order for ImGui NewFrame calls?
You must call the backend-specific NewFrame functions before ImGui::NewFrame(). For example, in a GLFW and OpenGL3 integration, call ImGui_ImplOpenGL3_NewFrame() and ImGui_ImplGlfw_NewFrame() first, then ImGui::NewFrame(). Reversing this order causes input lag and widget offset, as noted in docs/EXAMPLES.md and the reference example code.
Should I remove ImGui::ShowDemoWindow from shipping game builds?
Yes. ImGui::ShowDemoWindow is defined in imgui_demo.cpp and is an excellent learning tool, but it adds significant binary bloat and unnecessary code paths. Wrap it in a preprocessor macro or development-only flag so it is excluded from retail builds.
How do I handle high-DPI displays with Dear ImGui?
Query io.DisplayFramebufferScale after creating the context, then multiply font sizes and optionally set io.FontGlobalScale to match. Loading fonts at a base size and scaling globally prevents tiny UI elements on Hi-DPI monitors and ensures readable text across resolutions.
Can I modify game state directly inside an ImGui button callback?
You should avoid direct mutation. Immediate-mode UI reconstructs every frame, so changing game state during UI construction can lead to frame inconsistencies and subtle bugs. Capture the intent in a temporary variable or queue the action, then apply it after ImGui::Render() or inside your game's fixed update step.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →