I've always wanted to build a flight sim from the ground up instead of just messing around in X-Plane, so over a few weeks I put one together in C++ with OpenGL and ImGui. The goal wasn't a pretty demo — I wanted the plane to actually fly like a plane, so I went with real 6-degree-of-freedom rigid body dynamics instead of faking the motion. Physics updates at a fixed 60 Hz using RK4 integration, which took me embarrassingly long to get right (more on that below).
For the cockpit I built out the standard "six-pack" of instruments — airspeed, altimeter, attitude indicator, heading, VSI, and turn coordinator — all drawn by hand with ImGui's DrawList API instead of using pre-made widgets, since I wanted them to actually look like gauges. There's also a first-person 3D view with an artificial horizon and a compass tape scrolling across the top.
Screen capture of the sim running — physics, gauges, and 3D view all live
I split things up so the physics doesn't know or care about how it's being drawn, which made debugging way easier — I could print raw state to the console without dealing with the render loop at all.
┌─────────────────────────────────────────────────────────┐
│ Main Loop (60 Hz) │
│ (main.cpp) │
└────────────┬────────────────────────────┬────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌─────────────────┐
│ Input Handler │ │ Renderer │
│ │ │ (OpenGL + │
│ - Keyboard │ │ ImGui) │
│ - Controls │ │ │
└────────┬───────┘ └────────┬────────┘
│ │
▼ │
┌────────────────┐ │
│ Aircraft │◄─────────────────┘
│ │ (renders)
│ - State │
│ - Properties │
│ - Aero Coeff │
└────────┬───────┘
│
▼
┌──────────────────┐
│ Flight Dynamics │
│ │
│ - Forces │
│ - Moments │
│ - RK4 Integration│
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Atmosphere │
│ │
│ - ISA Model │
│ - ρ, P, T, a │
└──────────────────┘
User Input ──► InputHandler ──► Aircraft State
│
▼
FlightDynamics.update()
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
calculateForces() calculateMoments()
│ │
└─────────────────┬─────────────────┘
│
▼
RK4 Integration (4 steps)
│
▼
Update Aircraft State
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
Instruments.render() Renderer.render3D()
Holds everything about the plane's current state and aero properties.
Where the actual physics happens — this is the part I rewrote the most.
A simplified ISA model so air density actually changes with altitude.
The cockpit gauges, drawn frame by frame.
This part took the most reading. I pulled together a standard 6-DOF model with linearized stability derivatives — basically the same approach you'd find in an intro flight dynamics textbook (I leaned on Etkin's Dynamics of Flight for a lot of this), scaled down to something I could actually implement in a semester's worth of spare time.
My first pass used plain Euler integration and, predictably, the plane would slowly gain energy out of nowhere and spiral off into space if I left it running for a couple minutes. Switched to 4th-order Runge-Kutta and that drift basically disappeared. It's more math per timestep, but at 60 Hz it's not even close to a bottleneck, and it's what actual flight sim software uses for the same reason.
Why it's worth the extra code:
Keyboard → Aircraft Control
──────────────────────────────────────
W/S, ↑/↓ → Elevator (pitch)
A/D, ←/→ → Aileron (roll)
Q/E → Rudder (yaw)
Z/X, PgUp/PgDn → Throttle
Space → Center controls
P → Pause/Resume
R → Reset
ESC → Exit
Two frames of reference get used throughout, and honestly getting the sign conventions consistent between them was one of the more annoying bugs to track down.
Inertial reference frame
North (x)
↑
│
│
└────► East (y)
╱
╱
↓ Down (z)
Aircraft-fixed reference
Forward (x)
↑
│ (nose)
│
└────► Right (y)
╱
╱
↓ Down (z)
Going between the two just means building the rotation matrix R(φ,θ,ψ) from the current Euler angles.
#include "aircraft.hpp"
#include "flight_dynamics.hpp"
#include "renderer.hpp"
#include "input_handler.hpp"
#include <chrono>
int main() {
// Initialize systems
Renderer renderer;
Aircraft aircraft;
FlightDynamics dynamics(&aircraft);
InputHandler input(&aircraft);
// Main loop at 60 Hz
const double dt = 1.0 / 60.0;
double accumulator = 0.0;
auto lastTime = std::chrono::high_resolution_clock::now();
while (!renderer.shouldClose()) {
auto currentTime = std::chrono::high_resolution_clock::now();
double frameTime = std::chrono::duration<double>(
currentTime - lastTime).count();
lastTime = currentTime;
accumulator += frameTime;
// Physics update with fixed timestep
while (accumulator >= dt) {
input.processInput();
dynamics.update(dt);
accumulator -= dt;
}
// Render frame
renderer.render(aircraft);
}
return 0;
}
If I keep working on this, here's the list I keep coming back to:
This was basically my crash course in:
Honestly the hardest part wasn't any single piece of math — it was getting all the coordinate frames, sign conventions, and unit systems to agree with each other. But watching the plane actually respond correctly to control inputs for the first time, after a lot of debugging sessions that ended with the aircraft spinning into the ground, was a pretty good payoff.