Inspiration

We wanted to find out whether a real 3D racing game could be built for iPhone with nothing but Swift. No Unity, no Unreal, no Godot. No asset store, no model packs, no stock city kit, no downloaded car. Just SwiftUI and SceneKit, Blender for the buildings and trees only, and code we wrote ourselves for everything else on screen: every car, road, bridge and landmark.

That was half of it. The other half was that it had to be a game worth playing, not another hyper-casual clone with an energy bar, a timer and an interstitial every forty seconds. So the brief was an arcade racer with real modes, a campaign that actually ends, a HARD tier that rewards a better line, and a monetization layer that never touches the race.

What it does

A 3D traffic racer with three modes, and no health bar in any of them. One hit ends the run.

  • World Tour — thirty authored races from Paris to Monaco across ten cities, each replayable on NORMAL and HARD for a tighter line and a faster Gold Time. It has a real ending: finish the Tour and it is yours.
  • Rush Hour — the wrong way down a living road forever. Oncoming traffic closing at double speed, a draining tank, and lit tunnels that flip the sky to a new hour and new weather while you are still inside them.
  • Dogfight — a one-lap 1v1 against a rival that reads the lanes, dives for gaps, shoves you toward the barrier, and honks once when it gets past you. No rubber-banding.

Around the driving: thirty-nine trophies with drawn icons and a ceremony when one lands, a Garage of six cars with wraps, stickers and flame colours, Game Center leaderboards, iCloud-synced progress, and seven languages written as native racing slang rather than translated English.

How we built it

SwiftUI and SceneKit. No game engine, and no physics engine either: the car, the traffic, the near misses and the one hit that ends a run are all our own code. Forty-two thousand lines of Swift, plus almost ten thousand more of tests, from a bare prototype in the first week of July to the App Store on August 29.

Every model in the game comes from code we wrote. The road, the cars, the pedestrians, the bridges, the tunnels, the trains and every landmark that makes a place recognisably itself (the Eiffel Tower, the Bosphorus bridge, Christ the Redeemer, the Golden Gate, the Pyramids on the horizon) are built as SceneKit geometry directly in Swift. The street trees and the curbside lot dressing are generated by our own Blender scripts from a small kit of parapets, cornices, bays and awnings, exported as near/far pairs and hung on the same parametric facades the Swift side draws. There is no bought asset pack anywhere in it, no store model, no stock kit. The only image files in the bundle are the app icon and the logo: the trophy icons, the currency, the HUD and the coach marks are SwiftUI paths. Ten palettes × five times of day × three weathers, all relit at runtime.

The technical spine of the project is draw-call economy: ten full cities at 60fps on a phone means merging geometry aggressively while keeping per-lot lighting models, radius-aware cone segments and a stable composite sort intact, plus a thermal governor that sheds MSAA, then render scale, then frame rate, in that order.

On monetization, purchases stay on StoreKit 2, with RevenueCat running alongside in purchasesAreCompletedBy: .myApp observer mode. The AdMob interstitial is served through RevenueCat's official InterstitialAd.loadAndTrack adapter — the delegate and paid-event handler are passed into the load call, because assigning fullScreenContentDelegate afterwards silently strips RevenueCat's tracking wrapper. That single routing decision is what lets ad revenue and purchase revenue be read against each other, per customer, in one dashboard.

Where the ad is, and where it is not. One placement exists in the whole game — replay_after_result, the moment you start the next run after a run has ended. Never mid-race, never at launch, never over a Tour win or a trophy unlock. Four gates sit on top of that single placement: the first two results of a session are exempt entirely, at least two finishes must pass since the last ad, a 120-second cooldown, and a hard cap of three per fifteen minutes. They are five constants in AdGate.swift, and they are covered by tests — so a future refactor cannot quietly loosen the cadence. The One ad. One moment. diagram in the gallery maps every moment in the game against them.

Challenges we ran into

Doing without an engine. Everything an engine hands you had to be written: the chase camera, the traffic choreography, level of detail, geometry merging, a draw census, a thermal governor, the tutorial, the trophy ceremony. Each of those is a checkbox somewhere else. Here each one is a file with our name on it, which is also why every one of them behaves exactly the way this game needs and not the way a generic racer would.

Merging geometry without breaking it. flattenedClone returns empty geometry for parametric primitives, an SCNAction anywhere in a subtree blocks the whole merge, and an LOD node loses its far mesh when absorbed. Each of those survives import and dies quietly at merge time — nothing throws, the city just gets worse. The fix was a draw census that counts draws per node name in more than one city; pinning it to a single city measured a single city and hid the regression.

Proving the ad path actually reported. RevenueCat batches ad events, so nothing leaves the device until the next flush — the app has to be backgrounded before POST /v1/events fires. Until you know that, a correct integration looks broken and you go rewrite code that was already right.

Fitting a 3D game's UI on an iPhone SE. Every screen has to fit without scrolling, and the same layouts have to scale to a single iPad canvas. That constraint rewrote the Garage twice.

Accomplishments that we're proud of

A complete 3D arcade racer on the App Store with no engine underneath it and nothing bought or downloaded inside it.

Ten cities that are recognisably themselves and entirely ours — every one designed from nothing, none of them assembled from parts someone else made.

An ad system a player can go a whole session without ever meeting.

And a racer that actually ends. Finish the Tour and it is yours.

What we learned

That an engine is a convenience, not a requirement. SceneKit and a strict draw budget carry a phone a long way, and when every line is yours, a performance problem is something you fix rather than something you report.

That the constraint is the design. "Ads may only appear after a run the player chose to replay" sounds like a revenue sacrifice; it is the reason the game feels unhurried. Same with cosmetics — once no purchase can move a race, pricing stops being a moral question and becomes a craft one.

And that instrumentation beats intuition. Two of the hardest bugs in this project — the vanishing far meshes and the silent ad-tracking strip — were invisible to reasoning and obvious to a census and a network log.

What's next for Golden Mile: Traffic Racer

More authored cities, multiplayer 1 to 1 Dogfight race, an endless Tour+ tier for players who finish the campaign, and turning the replay ad placement into something measurable now that RevenueCat reports it next to the purchase data.

Built With

Share this project:

Updates

Submission history