Game Programming · April 12, 2026
Building a Simple Game Loop for a 2D Shmup
Every game runs on a loop: read input, update state, render, repeat. Getting that loop right is more subtle than it looks. Here is a practical breakdown, with code sketches, for a basic shoot-em-up architecture.
Published April 12, 2026
The game loop is the heartbeat of every interactive program. It runs continuously while the game is active, advancing the simulation by one step each iteration. For a shoot-'em-up, the loop has to be fast enough to maintain a smooth frame rate, consistent enough that movement and physics behave the same regardless of how fast or slow the host machine is, and structured cleanly enough that adding new game objects does not require rewriting the core logic.
The naive loop and its problem
The simplest possible game loop looks like this:
This works in principle, but it has a critical flaw: on a fast machine, it runs more iterations per second than on a slow machine. An enemy that moves 5 pixels per update will move 5 pixels at 30 FPS and 5 pixels at 120 FPS — but the effective speed on the fast machine is four times higher. The game becomes easier or harder depending purely on hardware.
Fixed timestep: the right answer
The standard solution is the fixed timestep loop. Instead of updating once per rendered frame, the game logic advances in fixed time increments (typically 1/60th of a second) regardless of how often the screen is drawn.
The inner loop runs the simulation in 1/60-second steps until the accumulator is exhausted. The render call uses the leftover fraction to interpolate between the previous and current state, producing smooth visuals even when the simulation and render rates differ.
Structuring game objects
A shmup has many objects: the player ship, dozens to hundreds of bullets, multiple enemies, possible power-ups, and environmental elements. The most maintainable approach for a small project is a simple list-based system: maintain separate lists for each object type, and iterate over them in a fixed order each tick.
Order matters here. Updating the player before enemies means enemy AI sees the player's new position. Checking collisions after all movement updates means you are working with the final positions for this tick, not a mix of old and new states.
Object pooling for bullets
In a bullet-hell game, bullets are created and destroyed constantly — potentially hundreds per second. Allocating and freeing memory that frequently in a garbage-collected language can introduce stutters. The standard solution is object pooling: pre-allocate a fixed array of bullet objects at startup, mark them as active or inactive, and reuse them rather than allocating new ones.
The pool exhaustion case (all 1024 bullets in use) should be sized generously so it never happens in normal play. If you start dropping bullets during heavy boss patterns, increase MAX_BULLETS.
Keeping the render layer separate
The game loop above separates update logic from rendering entirely. This is the correct structure. The update functions should never draw anything; the render function should never modify game state. This separation makes the code easier to reason about (you can read an update function and know it cannot have visual side effects) and easier to profile (frame time issues are immediately localizable to either the update or render path).
For a shmup specifically, this separation also enables practice/replay features: you can record the sequence of inputs, then replay them by feeding those inputs back into the update functions, producing a deterministic playback without any special code in the render path.
When to reach for a framework
Writing a raw game loop in C, Rust, or even Python is feasible and educational. But for an actual shipping project, frameworks like SDL2 (C/C++), Raylib, or Godot handle platform windowing, input events, and audio, letting you focus on game-specific logic. Flukz originally used its own engine; modern indie shmup projects often start with Godot for its built-in 2D tools and physics, then optimize where needed. The game loop structure above applies regardless of the framework underneath it.