Object-Oriented Design · CSCI 4448
A real-time 2D traffic simulator where cars, buses, emergency vehicles, bikes, and pedestrians all move through one shared, polymorphic agent model, built to show clean object-oriented design from the engine up.
OmniFlow simulates a busy downtown intersection in real time: vehicles obeying signals, buses dwelling at stops, pedestrians crossing in phases, and emergency vehicles cutting through it all. It was the final project for CU Boulder's Object-Oriented Analysis & Design course, built with a teammate (Evan Mohan).
The point was never hyper-realistic traffic modeling. It was to demonstrate clean object orientation: polymorphism, dependency injection, design patterns, a responsive JavaFX UI, and basic persistence, all structured so the architecture is easy to reason about and explain. We deliberately narrowed an early, broader idea down to a single polished scene because it was easier to finish well and defend.
Every moving thing on the map (car, bus, emergency vehicle, bike,
pedestrian) is updated through a single shared Agent interface.
The simulation engine stores and steps agents polymorphically, so each type varies
its own behavior through small override hooks rather than sprawling
if/else trees or switch statements. Adding a new agent
type means writing a class, not editing the engine.
The UI exposes start / pause / single-step controls, a speed slider, agent-type filtering, click-to-select agent details, manual emergency-vehicle spawning, and save/load of the layout state.
An AgentFactory builds agents from shared type names; the engine and persistence layer code against an AgentProvider abstraction, never concrete classes.
A MapLayout interface lets layouts be swapped in; the downtown layout owns its own signal timing, queueing, crossing rules, and preemption logic.
A BaseAgent owns the shared update algorithm, and subclasses customize just the parts they need through small override hooks.
A controller wires UI actions to the model; JavaFX listeners react to control and selection changes, and rendering reads model state instead of owning logic.
The engine depends on abstractions, not implementations:
SimulationEngine takes its world, agent provider, and random source
through its constructor (dependency injection), and stores agents behind the
Agent interface. The result is a simulation you can extend and test
without reaching into its core.
Coverage focuses where it matters most, the model and simulation logic rather than exhaustive UI testing, across five JUnit 5 suites: