This project is an interactive Python / Pygame flight simulation demonstrating how a simple Fly-By-Wire (FBW) attitude control system stabilizes an unstable aircraft during a bombing mission inside hostile airspace.
The full simulation includes:
FBW Block Diagram Used for Code

Below, each block in the diagram is mapped directly to the Python code that implements it.
The pilot controls:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
if event.type == pygame.KEYDOWN and not self.game_over:
if event.key == pygame.K_b and self.bombs_remaining > 0:
sensors = self.aircraft.get_sensors()
new_bomb = Bomb(sensors["x"], sensors["y"], sensors["h"], sensors["psi"])
self.bombs.append(new_bomb)
self.bombs_remaining -= 1
self.bomb_away_time = self.sim_time
if event.key == pygame.K_c:
self.fbw_on = not self.fbw_on
self.fbw_mode_text = "FBW: ON (attitude hold)" if self.fbw_on else "FBW: OFF (direct surfaces)"
if event.key == pygame.K_f:
sensors = self.aircraft.get_sensors()
self.deploy_flares(sensors)
keys = pygame.key.get_pressed()
if keys[pygame.K_ESCAPE]:
self.running = False
def update_commands_from_keys(self, keys, dt):
cmd_rate_deg = 20.0
if keys[pygame.K_UP]:
self.theta_cmd += math.radians(cmd_rate_deg) * dt
if keys[pygame.K_DOWN]:
self.theta_cmd -= math.radians(cmd_rate_deg) * dt
if keys[pygame.K_RIGHT]:
self.phi_cmd += math.radians(cmd_rate_deg) * dt
if keys[pygame.K_LEFT]:
self.phi_cmd -= math.radians(cmd_rate_deg) * dt
max_cmd = math.radians(30.0)
self.theta_cmd = max(-max_cmd, min(max_cmd, self.theta_cmd))
self.phi_cmd = max(-max_cmd, min(max_cmd, self.phi_cmd))
A PD FBW controller that stabilizes pitch and roll.
KP_THETA = 2.5
KD_THETA = 1.2
KP_PHI = 2.5
KD_PHI = 1.2
MAX_DEFLECTION = 1.0
def compute_controls(self, sensors):
theta = sensors["theta"]
q = sensors["q"]
phi = sensors["phi"]
p = sensors["p"]
e_theta = self.theta_cmd - theta
delta_e = KP_THETA * e_theta - KD_THETA * q
e_phi = self.phi_cmd - phi
delta_a = KP_PHI * e_phi - KD_PHI * p
delta_e = max(-MAX_DEFLECTION, min(MAX_DEFLECTION, delta_e))
delta_a = max(-MAX_DEFLECTION, min(MAX_DEFLECTION, delta_a))
return delta_e, delta_a
Chooses whether FBW or direct pilot input drives the control surfaces.
if self.fbw_on:
self.controller.update_commands_from_keys(keys, DT)
sensors = self.aircraft.get_sensors()
delta_e, delta_a = self.controller.compute_controls(sensors)
else:
delta_e = 0.0
delta_a = 0.0
step = 0.5
if keys[pygame.K_UP]:
delta_e += step
if keys[pygame.K_DOWN]:
delta_e -= step
if keys[pygame.K_RIGHT]:
delta_a += step
if keys[pygame.K_LEFT]:
delta_a -= step
delta_e = max(-MAX_DEFLECTION, min(MAX_DEFLECTION, delta_e))
delta_a = max(-MAX_DEFLECTION, min(MAX_DEFLECTION, delta_a))
FBW OFF makes the aircraft nearly unflyable because of disturbances.
The dynamic model simulates:
def step(self, delta_e, delta_a, dt):
# pitch dynamics
theta_dot = self.q
q_dot = -PITCH_DAMP * self.q + PITCH_GAIN * delta_e
q_dot += PITCH_DISTURB * random.gauss(0.0, 1.0)
# roll dynamics
phi_dot = self.p
p_dot = -ROLL_DAMP * self.p + ROLL_GAIN * delta_a
p_dot += ROLL_DISTURB * random.gauss(0.0, 1.0)
# integrate
self.theta += theta_dot * dt
self.q += q_dot * dt
self.phi += phi_dot * dt
self.p += p_dot * dt
# altitude from pitch
h_dot = FORWARD_SPEED * math.sin(self.theta)
self.h += h_dot * dt
self.h = max(self.h, 0)
# heading from roll
psi_dot = -TURN_GAIN * self.phi
self.psi += psi_dot * dt
# ground track
self.x += FORWARD_SPEED * math.sin(self.psi) * dt
self.y += FORWARD_SPEED * math.cos(self.psi) * dt
The IMU provides clean measurements to the controller, missiles, flares, and HUD.
def get_sensors(self):
return {
"theta": self.theta,
"q": self.q,
"phi": self.phi,
"p": self.p,
"h": self.h,
"x": self.x,
"y": self.y,
"psi": self.psi,
}
This includes:
def draw_horizon(self, theta, phi):
...
pygame.draw.line(self.screen, (255, 255, 255), pts_screen[0], pts_screen[1], 3)
pygame.draw.rect(self.screen, (200, 50, 50), (tl[0], tl[1], rect_w, rect_h), 2)
pygame.draw.circle(self.screen, (0, 255, 0), (ax, ay), 4)
lines = [
f"Pitch: {theta_deg:.1f} deg Cmd: {math.degrees(self.controller.theta_cmd):.1f} deg",
f"Roll : {phi_deg:.1f} deg Cmd: {math.degrees(self.controller.phi_cmd):.1f} deg",
f"Alt : {h:.1f} m",
...
]
Gameplay

Fly stably with FBW ON while disturbances try to destabilize you.
Press B over the target square.
Heat-seeking missiles spawn behind you.
Press F to deploy flares that spoof missiles.
Toggle C to see how unstable the aircraft becomes without FBW.
Requirements:
This simulation demonstrates: