Back to Articles

Building an F1 Race Replay System: When Game Engines Meet Real Telemetry

[ View on GitHub ]

Building an F1 Race Replay System: When Game Engines Meet Real Telemetry

Hook

Formula 1 broadcasts don't show you Safety Car telemetry because the FIA doesn't publish it. So how do you build a race replay tool that includes it? You fake it—and the solution is more interesting than you'd think.

Context

Formula 1 generates massive amounts of telemetry data during every session—GPS coordinates, speed, throttle, brake pressure, gear selection—all streamed at 3-5Hz and archived for analysis. The FastF1 Python library provides programmatic access to this treasure trove through official F1 timing APIs, making it possible for developers to build custom analysis tools without scraping or reverse-engineering protocols.

But raw telemetry is just numbers in a dataframe. Translating GPS coordinates into visual race positions, synchronizing 20 drivers across a two-hour race, and providing interactive playback controls requires architectural decisions about rendering, caching, and state management. The f1-race-replay project tackles these challenges using an unexpected approach: building a sports visualization tool on top of Arcade, a Python game engine typically used for platformers and shooters. This choice reveals fundamental similarities between game development and data visualization—both need smooth frame rates, sprite management, and user input handling.

Technical Insight

Visualization Layer

Data Layer

session params

session params

load session

GPS + telemetry

compute distances

serialize

deserialize frames

sprite system

playback controls

CLI Interface

f1_data.py

Session Loader

GUI Interface

FastF1 API

Telemetry Source

Pickle Cache

Serialized Frames

GPS to Track

Position Mapper

Frame Generator

Position + Status

arcade_replay.py

Arcade Renderer

2D Track Display

Interactive Controls

System architecture — auto-generated

The architecture splits cleanly into data processing (f1_data.py) and visualization (arcade_replay.py). The data layer loads sessions via FastF1, computes telemetry frames, and serializes them with pickle for caching. The visualization layer deserializes frames and renders them using Arcade's sprite system with interactive controls.

Here's where it gets interesting: race positions aren't directly available in the telemetry. You get GPS coordinates, but rendering cars on a 2D track representation requires projecting those coordinates onto the track's centerline polyline. The compute_race_positions function handles this by finding the closest point on the track polyline for each GPS coordinate, then calculating distance along the centerline:

# Simplified from f1_data.py
def compute_positions_from_gps(lap_data, track_polyline):
    positions = []
    for _, row in lap_data.iterrows():
        gps_point = (row['X'], row['Y'])
        # Find closest point on track polyline
        closest_idx = find_nearest_polyline_point(gps_point, track_polyline)
        # Calculate distance along track from start
        distance = sum_polyline_distance(track_polyline[:closest_idx])
        positions.append({
            'driver': row['Driver'],
            'distance': distance,
            'speed': row['Speed'],
            'compound': row['Compound']  # Tire type for visualization
        })
    return positions

This approach transforms 3D GPS data (latitude, longitude, elevation) into 1D race positions suitable for a 2D side-scrolling view. The track polyline acts as a coordinate system specific to each circuit—Monza's polyline differs from Monaco's—and the projection ensures cars stay "on track" visually even when GPS wobbles.

The Safety Car simulation showcases creative problem-solving when official data doesn't exist. Since the FIA doesn't publish SC telemetry, the tool synthesizes it by positioning a virtual Safety Car 500 meters ahead of the race leader:

# Conceptual implementation from f1_data.py
def simulate_safety_car(leader_position, track_polyline, sc_status):
    if sc_status == 'DEPLOYING':
        # SC animates from pit lane to track
        return calculate_sc_deployment_position()
    elif sc_status == 'ON_TRACK':
        # Position SC 500m ahead of leader on track polyline
        sc_distance = leader_position['distance'] + 500
        # Wrap around if past finish line
        sc_distance = sc_distance % total_track_length
        return polyline_point_at_distance(track_polyline, sc_distance)
    elif sc_status == 'RETURNING':
        # SC animates back to pit lane
        return calculate_sc_return_position()

This simulation includes three phases: deploying (SC exits pits), on-track (following the leader at fixed distance), and returning (SC enters pits). While not accurate to real SC behavior—which varies speed to bunch the field—it provides visual context for race events. The 500m distance is arbitrary but roughly matches typical SC gaps shown in broadcasts.

The frame pre-computation strategy trades upfront processing for smooth playback. Instead of rendering telemetry on-the-fly (which would stutter during data queries), the tool generates all frames first, serializing them as pickle files:

# Pattern from f1_data.py
def generate_race_frames(session):
    frames = []
    laps = session.laps
    # Create frame for each time step (typically 1Hz)
    for timestamp in pd.date_range(start=session.start, end=session.end, freq='1S'):
        frame_data = {
            'time': timestamp,
            'positions': compute_positions_at_time(laps, timestamp),
            'events': get_events_at_time(session, timestamp),  # Flags, pit stops
            'leader_gap': calculate_gaps(positions)
        }
        frames.append(frame_data)
    
    # Cache to disk
    cache_path = f"cache/{session.name}_{session.date}.pkl"
    with open(cache_path, 'wb') as f:
        pickle.dump(frames, f)
    return frames

This design enables rewind and fast-forward without re-querying the FastF1 API or recomputing projections. Users can scrub through a two-hour race instantly because all positional data is pre-baked. The downside? Adding new features (like the SC simulation) requires cache invalidation via --refresh-data, reprocessing entire sessions that might take minutes.

The Arcade library choice is unconventional but pragmatic. Arcade provides sprite management, collision detection (unused here but available), and a game loop architecture that naturally fits visualization needs. The arcade_replay.py file extends arcade.Window and implements on_update() for animation logic and on_draw() for rendering—patterns familiar to game developers but less common in data visualization projects that typically use matplotlib or Plotly.

Qualifying sessions receive different treatment: instead of race positions over time, the tool renders telemetry traces (speed, throttle, brake, gear) synchronized with track position. This lets you compare racing lines between laps, seeing exactly where drivers brake later or carry more speed through corners. The implementation reuses the polyline projection system but renders line graphs instead of car sprites.

Gotcha

The Safety Car simulation, while creative, can mislead if you treat it as ground truth. Real Safety Cars don't maintain fixed distances from leaders—they slow down strategically to compress the field, sometimes even weaving to scrub speed from tires. The synthetic SC always stays 500m ahead, which doesn't reflect actual race control decisions. If you're using this for serious analysis rather than casual viewing, be aware that SC phases won't align with real-world Safety Car behavior shown in broadcast footage.

Caching becomes a maintenance burden as features evolve. The pickle serialization locks frames to a specific data schema—add a new field and all existing caches become invalid. The --refresh-data flag forces reprocessing, but there's no incremental update mechanism. For a hobby project analyzing occasional races, this is fine. For production use tracking every F1 session, you'd need migration strategies or a proper database with schema versioning. Additionally, the pre-computation approach means you can't do live visualization of ongoing races; the tool is strictly for post-race analysis after FastF1 archives the data.

Verdict

Use if: You're learning sports data visualization, want an interactive way to review F1 races with friends, or need a foundation for building custom telemetry tools and appreciate seeing how game engines apply to data problems. The codebase is readable, the architectural split is clean, and the polyline projection technique is genuinely useful for any GPS-to-track-position visualization. Skip if: You need production-ready F1 analysis with accurate Safety Car data, want live race visualization (this only works post-race), or require a mature tool with active maintenance and community support—the 5-star count and 'in development' qualifying mode suggest this is an experimental project. For serious analysis, use FastF1 directly with Jupyter notebooks, or invest in commercial platforms like MultiViewer that provide polished, real-time telemetry with official data feeds.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/4f4d-f1-race-replay.svg)](https://starlog.is/api/badge-click/developer-tools/4f4d-f1-race-replay)