Building an Airport Simulator State Machine

Flight simulator, Cardiff Wales Airport   geograph.org.uk   5761356

Moving a virtual aircraft from the gate to the runway isn't as simple as updating a few coordinates in a database. If you just teleport the plane or slide it along a vector, you're ignoring the reality of how flight systems actually work. You need a robust state machine and a timing engine that doesn't drift, or the whole simulation falls apart the moment a pilot pushes the throttle.

I've seen plenty of devs try to hack this with simple timers, but that approach fails as soon as you add network latency or complex taxiway logic. It's an exercise in managing edge cases. You're dealing with physics, scheduling, and the stubborn reality that things rarely happen exactly when the code thinks they should.

The real challenge is keeping the state synchronized across the board without killing performance. It's a balancing act between precision and stability. The question is how to build a system that feels fluid to the user but remains surgically precise under the hood.

The State Machine of a Flight

A flight is a state machine, not a loop. If you try to handle airport logic with a simple while loop or a series of if statements, you'll eventually hit a wall where the plane is somehow airborne while still parked at the gate. You need a strict set of transitions to ensure the plane can't jump from Scheduled to Airborne without first taxiing and taking off.

The transitions are the hard part. Moving from Taxiing to Takeoff requires a specific trigger—like reaching the runway threshold—and once that happens, the state is locked until the altitude hits a specific floor. This part is genuinely confusing because real-world airport logic is messy; you have to account for hold-short lines and ATC clearances that don't always follow a linear path.

class FlightState:
    def __init__(self):
        self.state = "SCHEDULED"

    def transition(self, new_state):
        # Define allowed movements to prevent illegal jumps
        valid_paths = {
            "SCHEDULED": ["TAXIING"],
            "TAXIING": ["TAKEOFF"],
            "TAKEOFF": ["AIRBORNE"],
            "AIRBORNE": ["LANDING"]
        }
        if new_state in valid_paths.get(self.state, []):
            self.state = new_state
        else:
            print(f"Invalid transition from {self.state} to {new_state}")

flight = FlightState()
flight.transition("TAXIING") # Works
flight.transition("AIRBORNE") # Fails: must takeoff first

I've seen a few people try to build these simulators as a race, and it's a great way to realize how quickly the edge cases pile up. When you're staring at a dashboard showing 0 landings, 0 departures, and a 0:00 duration, the logic feels trivial. But the moment you add a second plane, the state machine has to handle shared resources like runways, or you'll end up with two planes occupying the same coordinate.

Managing the Queue

The runway logic is a simple FIFO (First-In-First-Out) queue. You can't have two planes on the strip at once, so the system tracks "Landings" and "Departures" as two separate counters that feed into a single occupancy state. It's a basic priority system: if a plane is landing, the runway is blocked until that operation completes.

Handling conflicts between a landing request and a departing request is where this gets genuinely confusing. If both trigger at the same millisecond, the system has to decide who wins. In this implementation, landings take priority. It's a safety-first approach; a plane in the air has less flexibility than one sitting on the tarmac.

from collections import deque

runway_queue = deque()

def request_runway(plane_id, plane_type):
    # Priority: Landings (L) always jump ahead of Departures (D)
    if plane_type == "L":
        runway_queue.appendleft(plane_id)
    else:
        runway_queue.append(plane_id)
    return f"Plane {plane_id} is in queue"

The performance is tracked through a few specific metrics to keep the simulation honest:

  • 0 Landings
  • 0 Departures
  • 0.0/min Pace
  • 0:00 Duration

I'm not sure if the "Pace" metric is actually useful for the player or if it's just there to make the UI look like a real ATC dashboard. It feels like a bit of fluff, but it gives you a concrete number to beat when you're racing a friend to see who can manage the highest volume of traffic.

Handling the Timing Engine

The timing engine here is doing a lot of the heavy lifting to mimic the stress of real air traffic control, but I suspect the "nostalgia" people are feeling is actually just a familiarity with the Flight Control loop. When you're managing vectors and descent rates, the mechanical feel of the clock is what creates the tension. If the timing is too permissive, it's just a puzzle game; if it's too rigid, it feels like a chore.

The community is mostly leaning into the fun of the simulation, but the mention of UI differences in the ATC is where the real friction lies. I think the "fun" factor depends entirely on whether the interface stays out of the way of the timing. If you're fighting the menu while the clock is ticking, the simulation breaks.

I'm not convinced the current UI can scale if the complexity of the airport layouts increases. Will the timing engine still feel fair when you're managing twenty planes instead of five, or will the interface become the primary bottleneck?

Scaling the Simulation

The nostalgia for Flight Control is a strong signal, but I think focusing on the "fun" misses the actual technical friction here. Most simulation games fail because they can't balance the gap between a casual loop and a hardcore simulation. If this is just a polished version of a decade-old mobile game, it's a nice distraction. But if the goal is a true ATC simulation, the UI feedback we're seeing from the community suggests the interface isn't quite communicating the complexity of real-world airspace management yet.

I'm skeptical that the "nostalgia" factor will sustain the game if the simulation depth doesn't scale. People love the idea of managing an airport, but they usually drop off the moment the cognitive load exceeds the reward of a clean landing. Whether this project can move past a simple "move the plane" mechanic into something that actually simulates systemic failure and recovery is the only thing that matters for its longevity.

I'm still not convinced the current UI can handle the density of a major hub without becoming a cluttered mess. Can a simplified interface actually convey the precision required for high-altitude sequencing, or are we just playing a glorified sorting game?

Conclusion

Building a state machine for something as chaotic as airport traffic is a good exercise in timing and queue management, but it's still just a simulation. Whether you're tracking landings and departures in English or Finnish, the logic remains the same: you're fighting the clock to keep the pace consistent.

I'm still not entirely convinced that a standard state machine is the most efficient way to scale this as the aircraft count grows. At some point, the overhead of managing every individual flight's state becomes a bottleneck.

If you're actually going to build this, start with the timing engine. If your Lentoaika is off by a few seconds, the rest of the simulation is just a fancy way to display the wrong numbers.