SpectaRacer is a car racing themed endless runner lens for Snap Spectacles. Three lanes of enemies and collectibles scroll toward the player at an ever-increasing speed. Survive as long as possible, rack up coins, and track your distance: all overlaid on your real environment.
SpectaRacer.mp4
Point and/or pinch and hold:
- Top yellow part of the steering wheel and steer right/left to move the car.
- Bottom part of the steering wheel to adjust the level position.
| Action | Result |
|---|---|
| Drive into a coin | Score increases, coin SFX plays |
| Collide with an enemy | Damage +33%, crash SFX plays |
| Three enemy hits | Game over panel appears |
| Press pause button | Game freezes, VFX pause, pause panel opens |
| Resume / Restart / Main Menu | All wired through the pause overlay |
Three enemy collisions end the run. The game over screen appears automatically and offers a one-tap restart or return to the main menu.
- 3-lane endless runner with procedurally selected enemy/coin patterns
- Adaptive difficulty: speed and pattern complexity ramp smoothly over 120 seconds
- Full game loop: main menu β gameplay β pause β game over β restart, with no reloads
- Live HUD: real-time damage percentage, distance in metres, and coin score
- Weather system: toggle an environmental effect and its rain VFX mid-game; rain audio tracks the switch state
- Spatial audio: distinct coin collect and crash sound effects; looping ambient rain that respects both menu and weather toggle state
- Complete menu system: four panels (main menu, pause, tutorial, game over) wired entirely in TypeScript with no Inspector callbacks
- Singleton architecture:
PlayPauseManagerandSnapSaberGlobalManageraccessible from any script viagetInstance()
SpectaRacer/
βββ Assets/
β βββ Examples/
β β βββ CustomUI/ # UIKit visual customisation scripts
β β β βββ UIKitCustomVisualsRectangleButton.ts
β β β βββ UIKitCustomVisualsRoundButton.ts
β β β βββ UIKitCustomVisualsFrame.ts
β β β βββ UIKitCustomVisualsSlider.ts
β β β βββ UIKitCustomVisualsSwitch.ts
β β βββ MiniDemos/SnapSaber/TS/ # Core game scripts
β β βββ MainMenu.ts
β β βββ PlayPauseManager.ts
β β βββ SnapSaberGlobalManager.ts
β β βββ SnapSaberCollisionHandler.ts
β β βββ LanePatternController.ts
β β βββ CoinInstantiator.ts
β β βββ EnemyInstantiator.ts
β β βββ RoadInstantiator.ts
β βββ Materials/ # PBR materials
β βββ Prefabs/ # Coin, enemy, and road tile prefabs
β βββ Shader/ # Custom .ss_graph shaders
β βββ Scene.scene
βββ Packages/
β βββ SpectaclesInteractionKit.lspkg
β βββ SpectaclesUIKit.lspkg
βββ Support/
β βββ StudioLib.d.ts # Lens Studio API type declarations
βββ tsconfig.json
| Script | Responsibility |
|---|---|
MainMenu.ts |
Panel navigation (4 panels), level enable/disable, rain audio, weather toggle, game lifecycle |
PlayPauseManager.ts |
Pause/resume/restart, HUD output (damage %, distance, score), game-over detection, VFX freeze |
SnapSaberGlobalManager.ts |
Singleton: score tracking, damage strikes, coin/crash SFX |
SnapSaberCollisionHandler.ts |
Car collision detection, routes hits to GlobalManager |
LanePatternController.ts |
Wave-based pattern selection across 3 lanes, speed ramp, pause/resume/reset |
CoinInstantiator.ts |
Spawns and moves coin prefabs; end-zone cleanup via overlap collider |
EnemyInstantiator.ts |
Spawns and moves enemy prefabs; end-zone cleanup via overlap collider |
RoadInstantiator.ts |
Continuously spawns road tile prefabs at 0.5 s intervals |
Singleton via global scope: SnapSaberGlobalManager and PlayPauseManager register themselves in Lens Studio's global object on onAwake, providing type-safe getInstance() accessors to any script without creating inspector dependencies.
Callback for cross-boundary events: When damage reaches 100%, PlayPauseManager invokes an onGameOver callback registered by MainMenu at game start. This avoids a circular import between the two scripts while keeping the event flow explicit and traceable.
Controller-owned pause: LanePatternController.pauseAll() and resumeAll() freeze all nine lane instantiators (coins, enemies, roads) in a single call. PlayPauseManager needs only one inspector reference to produce a complete, reliable freeze: including VFX components.
Wave state machine: LanePatternController cycles gap β wave β gap. Pattern pools are split into Easy, Medium, and Rest tiers. Difficulty weight shifts from 0 to 1 over difficultyRampTime seconds. A lane-switching enforcement rule tracks consecutive waves on the same safe lane and forces a rotation when the threshold is exceeded: keeping players active rather than camping.
Weather-aware audio: Rain audio is controlled by MainMenu and guards against both menu panel state and the weather switch toggle. startRain() checks weatherToggleSwitch.isOn before playing, so every resume path respects the current weather preference without additional coordination.
- Lens Studio v5.15.4
- Snap Spectacles (physical device) or the Spectacles simulator built into Lens Studio
- Clone or download this repository.
- Open Lens Studio.
- Choose File β Open Project and select the
SpectaRacer/folder. - Lens Studio resolves the packed
.lspkgdependencies from thePackages/folder automatically: no install step needed. - Press Play in Lens Studio to run in the simulator, or push to a paired Spectacles device via the device panel.
No npm, no build commands, no external services. Everything runs inside Lens Studio.
The tsconfig.json is noEmit and exists solely for editor intelligence. To run a type check from the terminal:
cd SpectaRacer
npx tsc --noEmitThe scene is pre-wired in Scene.scene. If you rebuild from scratch, every inspector slot carries a @hint tooltip. The critical connections are summarised below.
Must live on a SceneObject outside
LevelHandleMoverso it stays active while the level is disabled.
| Slot | Assign |
|---|---|
panel1 β panel4 |
Root SceneObject of each panel |
levelHandleMover |
The parent SceneObject for the entire level |
rainAudio |
AudioComponent on the MainMenu SceneObject |
weatherObject |
SceneObject to show/hide for the weather effect |
weatherToggleSwitch |
The Switch component on the weather toggle |
| Button slots | Drag each RectangleButton component from its SceneObject |
Lives inside
LevelHandleMover. Controls game logic only: no button wiring needed here.
| Slot | Assign |
|---|---|
laneController |
The LanePatternController SceneObject |
coinLeft/Middle/Right |
One CoinInstantiator per lane |
enemyLeft/Middle/Right |
One EnemyInstantiator per lane |
roadLeft/Middle/Right |
One RoadInstantiator per lane |
damageText |
Text component for the damage display |
distanceText |
Text component for the distance display |
scoreText |
Text component for the score display |
rainVFX |
VFXComponent for rain particles (paused with game) |
cloudVFX |
VFXComponent for cloud particles: optional |
Coin + crash SFX: Add two AudioComponents to the SnapSaberGlobalManager SceneObject. Assign one .mp3 per component. Drag each into the coinAudio and enemyAudio slots on SnapSaberGlobalManager.
Ambient rain: Add one AudioComponent to the MainMenu SceneObject. Assign the rain .mp3. Set Play On Awake β OFF (the script controls playback). Drag it into the rainAudio slot on MainMenu. Rain plays during gameplay, pauses when the pause menu opens, and respects the weather switch: if the switch is off, rain stays silent on resume.
All parameters are exposed as @input fields on LanePatternController and adjustable in the Inspector without touching code.
| Field | Default | Effect |
|---|---|---|
initialMoveSpeed |
100 | Starting speed (cm/s) |
maxMoveSpeed |
300 | Peak speed after ramp |
speedRampTime |
120 s | Time to reach max speed |
difficultyRampTime |
120 s | Time until hard patterns dominate |
gapFrequency |
5 | Insert a rest wave every N waves |
minCoinsPerWave |
2 | Fewest coins per wave |
maxCoinsPerWave |
6 | Most coins per wave |
gapDuration |
1.5 s | Silence between waves |
Patterns are number[3] arrays inside LanePatternController.ts:
0 = EMPTY 1 = COIN 2 = ENEMY
Add entries to PATTERNS_EASY, PATTERNS_MEDIUM, or PATTERNS_REST. The safety invariant (at least one non-enemy lane per pattern) is enforced by convention: every pattern in the library already satisfies it.
Drop a replacement prefab into the prefab slot on any instantiator. The end-zone collision system identifies objects by name prefix (CoinCube_, EnemyCube_, RoadTile_): spawned instances keep these prefixes automatically since the instantiator sets the name on spawn.
| Package | Version |
|---|---|
| SpectaclesInteractionKit | 0.17.2 |
| SpectaclesUIKit | 0.1.5 |
| SnapDecorators | 1.0.0 |
| Utilities | 1.0.0 |
| Lens Studio API target | 5.15.0 / Snapchat 13.60 |
MIT. Giving credit would be appreciated.