15 / CONTROL · GRAPHICS

FSAI Unity Driving Simulator

An earlier Unity/C# Formula Student driving sim — procedurally-generated cone circuits via complex Fourier synthesis, rescaled until the tightest corner clears a minimum radius; a dynamic bicycle model (Pacejka tyre, kinematic blend through standstill) driven autonomously by a dual-lookahead path-follower with live telemetry.

Tyre model
Pacejka magic formula
Track generation
complex Fourier synthesis
Low-speed model
kinematic blend ≤3.5 m/s
C# engine
~2.6k LOC

The interactive, game-engine take on the Formula Student driving problem: a Unity/C# sim where a car drives itself around procedurally-generated cone circuits, with the vehicle physics integrated by hand rather than handed to Unity’s engine. It predates the C++ vehicle-dynamics work (velox / the FSAI stack) elsewhere in this portfolio, and fed into it. This is where the ideas were first prototyped with a renderer attached.

Procedurally-generated cone circuits

Tracks aren’t hand-drawn — they’re synthesised in the complex plane. PathGenerator (PathGenerator.cs:58-90) samples the unit circle as z[t] = e^{iθ}, then sums harmonics (frequencies 2…N, each with a random phase) to warp that circle into a closed, wiggly loop. The clever part is enforcing drivability. It computes each point’s corner radius from the path’s curvature, |P′|³ / Im(conj(P′)·P″) (PathGenerator.cs:41-55), finds the tightest corner on the loop, and then scales the entire path by minCornerRadius / R_min (PathGenerator.cs:159-171) so the worst corner lands exactly on the minimum radius a real car could take. One deterministic rescale, no rejection sampling and no retry loop — the shape is kept, the size is corrected. Cones are then placed at a half-track-width offset and checkpoints fall out of the cone geometry.

  z(t) = e^(i*theta)        sample the unit circle
          |
          v
  sum harmonics 2..N        random phase per harmonic
          |
          v
  closed wiggly loop
          |
          v
  R = |P'|^3 / Im(conj(P')*P'')     corner radius per point
          |
          v
  R_min = min(R)  ->  scale = R_target / R_min
          |
          v
  scale every point + radius        tightest corner now
  by that one factor                exactly R_target
          |
          v
  cones at +/- half track width
          |
          v
  checkpoints fall out of the cone geometry

Fig. 1 — track synthesis: a Fourier-warped unit circle, measured for its tightest corner, then uniformly rescaled until that corner is drivable.

The vehicle: a dynamic bicycle that blends to kinematic at low speed

DynamicBicycle (DynamicBicycle.cs) integrates a single-track model. Lateral forces come from a Pacejka “magic formula” tyre, μ_y = D·sin(C·atan(B(1−E)α + E·atan(Bα))) (DynamicBicycle.cs:114-125), with aerodynamic downforce and drag scaling as v², and yaw dynamics from the front and rear cornering forces about the axle distances. The honest engineering touch is the low-speed singularity fix. A pure dynamic bicycle model blows up as v → 0, because slip angles divide by speed. So _fKinCorrection (DynamicBicycle.cs:74-90) forms a blend weight clamp01(0.5·(v − 1.5)) and mixes the dynamic longitudinal velocity, lateral velocity and yaw rate against a kinematic solution: below 1.5 m/s the state is entirely kinematic, the dynamic model fades in linearly across the next 2 m/s, and above 3.5 m/s the kinematic term is gone. The car pulls away from standstill cleanly instead of exploding — the same class of fix the production C++ models later needed.

  kinematic weight = 1 - clamp01(0.5*(v - 1.5))
  1.0 |##########
      |          #####
      |               #####
      |                    #####
  0.0 +--------------------------+-------------------
      0           1.5          3.5 m/s      speed -->
      |<- pure   ->|<- blend ->|<- dynamic bicycle --
      |  kinematic |           |  Pacejka tyre + aero v^2

Fig. 2 — the low-speed blend: fully kinematic below 1.5 m/s, fully dynamic above 3.5 m/s, linear in between.

Driving it: an autonomous lookahead controller

No human input — a RacingAlgorithm (RacingAlgorithm.cs) follows the generated line with separate speed-dependent lookahead horizons for speed and for steering — it scans floor(0.7·v) + 1 checkpoints ahead for the speed plan and only floor(0.1·v) + 1 for steering (RacingAlgorithm.cs:77,83,159-160), the same decoupling that turns up again in the dissertation controller years later. It caches directions and distances to upcoming checkpoints once per step and plans a target speed from the upcoming corner angle. Heading error becomes a steering command via a time-to-react model, clamped to the car’s real limits (±21° steer from the FS-AI API, RacingAlgorithm.cs:289,314-315, and bounded throttle/brake). A telemetry overlay shows angle-to-checkpoint, speed, lateral acceleration, yaw rate and lap time live; a separate test mode in CarController streams the same state to car_test_log.csv for offline analysis.

Each physics step runs the same loop:

  RacingAlgorithm (per physics step)
  +--------------------------------------------+
  | checkpoint cache: directions + distances   |
  |   speed lookahead   |   steering lookahead |
  |   floor(0.7v)+1     |   floor(0.1v)+1      |
  |   checkpoints       |   checkpoints        |
  |    |                    |                  |
  |    v                    v                  |
  | corner angle        heading error          |
  |    |                    |                  |
  |    v                    v                  |
  | target speed        time-to-react model    |
  |    |                    |                  |
  |    v                    v                  |
  | throttle / brake    steering               |
  | (bounded)           (clamped +/- 21 deg)   |
  +--------------------------------------------+
          |
          v
  DynamicBicycle integrates one step
          |
          +--> telemetry overlay + optional CSV
          |
          +--> state feeds the next step

Fig. 3 — the per-step control loop, from the dual checkpoint lookahead to clamped steering, integration, and telemetry.

Honest scope

This is an archived prototype, and it’s framed as one. The controller is a heuristic look-ahead, not an optimal (MPC/LQR) planner. The sim is validated by running (lap completion, sane telemetry), not against real-vehicle data — there are no unit tests, and the project isn’t under version control at all: it survives as a working tree plus a 459 MB packaged zip, so there is no commit history to point at. It’s a single scene with one vehicle model, and of the 238 C# files in Assets/ only 16 (~2.6k lines) are mine — the rest is a vendored YamlDotNet. Its real value is as the Unity-era exploration of vehicle dynamics, procedural tracks, and autonomous control that the production work then re-implemented in C++ (velox, the FSAI simulator): faster and instrumented, embedded in the real driverless pipeline. Shown here for the breadth (game-engine

  • C# + the procedural-track maths) and the lineage, not as the finished article.

ROLE

Sole author (Formula Student). Wrote the dynamic bicycle vehicle model (Pacejka tyre, aero down/drag-force, low-speed kinematic correction), the procedural cone-track generator (a complex-Fourier path with curvature-derived corner radii and a minimum-radius rescale), the autonomous lookahead path-following controller, and the real-time telemetry + CSV logging — all C# on Unity 6 (6000.0.22f1).

DATES / 2024–25

Private repository