A Spectacles AR memory game built with Lens Studio. Players memorize a path shown on a virtual grid, then walk the correct route by stepping on tiles in the correct order.
- Clone this repository
- Open in Lens Studio — Open
memory-grid.esprojin Lens Studio (v5.15 or compatible) - Preview or Publish — Use Spectacles Preview or publish to Lens Explorer for Spectacles
The project includes all required packages (Surface Placement, Spectacles Interaction Kit, Spectacles UI Kit, etc.) and is ready to run.
This game uses Device Tracking (World Mode) to track the player's position in real-world space and map it onto a virtual 5×5 grid placed on the floor. The core mechanic relies on detecting when the player steps on specific grid tiles.
- 11 levels — Path length increases from 5 to 25 tiles
- Achievements system — Unlock badges as you progress
- Robot host — Voice-guided instructions and feedback
- FOV-optimized — Progressive reveal, start-anchored grid, designed for Spectacles' limited field of view
This project implements a collider-based vertical activation system for reliable grid-based tile detection in walking-based Spectacles games—replacing error-prone position/distance checks with physics-based overlap detection. Purpose-built for structured tile grids (not open-world or sparse zones). For details on the problem, solution, scope, and how to reuse the pattern:
docs/COLLIDER_BASED_ACTIVATION.md
For a deeper look at the design decisions and development process behind Memory Grid, see the accompanying article on Medium:
Memory Grid: What I Learned Building a Path-Following Game for Spectacles AR
The article covers the inspiration for the project, designing for Spectacles' limited FOV (progressive reveal, start-anchored grid, path generation), solving unreliable tile detection with collider-based triggers, and building the achievement system in Lens Studio. It's written as a narrative rather than documentation—useful if you want to understand the why behind the implementation.
| Main menu | Achievements | Robot host |
|---|---|---|
![]() |
![]() |
![]() |
The Reset Progress button appears on the main menu when you've advanced past Level 1. It lets you clear your level progress and start over from Level 1, while keeping all unlocked achievements.
- When visible: Level 2 or higher (hidden at Level 1 since there's nothing to reset)
- What it does: Resets current level to 1, clears retry counts — achievements remain unlocked
- Reset All (debug): Only visible when
DebugConfig.ENABLEDis true; clears levels, retries, and achievements
The game is designed around Spectacles' limited field of view:
- Progressive reveal — path appears tile-by-tile, guiding attention
- Focal point first — yellow start tile appears immediately after placement
- Start-anchored grid — place the tracker where you want to START, grid extends away from you
Scripts/
├── Core/
│ ├── GameStateManager.js # Component: Main game orchestrator
│ └── PlacementBridge.js # Component: Bridges Surface Placement package
│
├── Grid/
│ ├── GridManager.js # Component: Grid creation and tile management
│ └── PathGenerator.js # Module: Random path generation algorithms
│
├── Host/
│ └── HostManager.js # Component: Robot host with billboard behavior
│
├── Player/
│ └── PlayerTracker.js # Component: Position tracking and validation
│
├── UI/
│ ├── AchievementNotification.js # Component: Manual-position popup for achievement unlocks
│ ├── AchievementsUI.js # Component: Achievements screen with card grid
│ ├── CountdownDisplay.js # Component: 3-2-1 countdown on floor
│ ├── LookDownHint.js # Component: Hint text parented to camera after floor placement
│ ├── MainMenuManager.js # Component: Main menu with Start/Achievements buttons
│ ├── SpaceReminder.js # Component: Pre-placement message ("Make sure you have enough open space")
│ ├── PalmExitButton.js # Component: Exit button (place under wrist in scene; show/hide by game state)
│ ├── StartZoneVisual.js # Component: Start zone marker with collision detection
│ └── StartZoneParticles.js # Component: Rising particle effect for start zone
│
└── Utils/
├── AnimationManager.js # Module: LSTween-based animation helpers
├── AudioManager.js # Component: Voice line audio playback (auto-matches by filename)
├── Constants.js # Module: Game configuration and constants
├── DialogueLines.js # Module: Host dialogue text and audio IDs
├── MathHelpers.js # Module: Math utilities for calculations
└── SaveManager.js # Module: Persistent storage for progress/achievements
Component Scripts (attached to SceneObjects):
- Use
@inputdecorators for Inspector inputs - Export via
script.functionName = functionName
Module Scripts (imported via require):
- Use
module.exportsfor exports - Imported with
require("./path/to/module")
The project is pre-configured—open the .esproj file to run. The following steps are for understanding the architecture or building from scratch:
- Create a new Lens Studio project for Spectacles
- Add Device Tracking component to the Camera:
- Select Camera in Scene Hierarchy
- Add Component → Device Tracking
- Set Tracking Mode to World
- Import the Surface Placement package from the Asset Library
-
Add the Surface Placement prefab to your scene
-
Create your game hierarchy under objectVisuals:
SurfacePlacement (from package) └── ObjectVisuals └── GridParent # Parent for all grid tiles (required)Optional objects (add as needed):
CountdownText- Text component for 3-2-1 countdownHostPlaceholder- Empty object for robot hostSubtitleText- Text for host dialogue
-
Attach PlacementBridge.js to the
ObjectVisualsobject
Placement flow (Start → Game):
flowchart TD
A[User taps Start] --> B[Main menu hides]
B --> C[Surface Placement prefab enabled]
C --> D[User pinch-places grid on floor]
D --> E[PlacementBridge detects objectVisuals enabled]
E --> F[PlacementBridge.onGridPlaced]
F --> G[GameStateManager.onGridPlaced]
G --> H[GridManager.initialize + PlayerTracker.init]
H --> I[GRID_INTRO state]
I --> J[Host intro → COUNTDOWN → ...]
Create empty SceneObjects and attach scripts:
| SceneObject | Script | Key Inputs |
|---|---|---|
| MainMenu | UI/MainMenuManager.js |
InterfaceContainer, MenuContainer, AchievementsContainer, StartButton, AchievementsButton, ResetProgressButton, ResetAllButton (optional, debug), LevelText, SpaceReminder (optional), FloorPlacementPrefab OR FloorPlacementObject, FloorPlacementParent, AchievementsUI |
| SaveManager | Utils/SaveManager.js |
(none - just attach to any SceneObject) |
| AudioManager | Utils/AudioManager.js |
VoiceLines[] (all audio files), AudioPlayer |
| AchievementNotification | UI/AchievementNotification.js |
NotificationRoot, TitleLabelObject, DescriptionLabelObject, IconObject, IconTextures[] (optional if AchievementsUI has icons), NotificationAudio, IconTextureProperty, DisplayDuration |
| AchievementsUI | UI/AchievementsUI.js |
BackButton, CardPrefab, GridContainer, IconTextures[] (all PNGs), LockedTint, MainMenuScript |
| PalmExitButton | UI/PalmExitButton.js |
ExitButton (or ExitButtonContainer as alias) |
| LookDownHint | UI/LookDownHint.js |
Camera, HintContainer, HintText |
| SpaceReminder | UI/SpaceReminder.js |
Camera, ReminderContainer, ReminderMessage, DisplayDuration, FadeOutDuration, PopInAudio (optional) (shows before floor placement) |
| ObjectVisuals* | Core/PlacementBridge.js |
GameStateManager |
| GridManager | Grid/GridManager.js |
GridParent, TilePrefab, TriggerPrefab, ScaleTiles |
| PlayerTracker | Player/PlayerTracker.js |
Camera, GridManager, AudioPlayer, StepTracks[25], CompletionTrack, ErrorTrack, CountdownTrack, WatchTrack |
| GameStateManager | Core/GameStateManager.js |
GridManager, PlayerTracker, HostManager (optional), StartZoneVisual (optional), CountdownDisplay (optional), ConfettiVFX (optional) |
| HostManager | Host/HostManager.js |
HostObject, Camera, SubtitleText (optional), ForwardDistance, HeightOffset, LateralOffset, FollowEasing, RiseFromFloor (optional), RiseFromFloorOffset, RiseDuration |
| StartZoneVisual | UI/StartZoneVisual.js |
StartZonePlane, ZoneCollider (required), ParticlesScript (optional) |
| StartZoneParticles | UI/StartZoneParticles.js |
ParticlePrefab, SpawnRate, RiseSpeed, Lifetime, StartScale |
| CountdownDisplay | UI/CountdownDisplay.js |
CountdownText3D, TextParent |
*ObjectVisuals is from the Surface Placement package
Before floor placement, you can show a message reminding players to ensure they have enough open space. This is optional—if not configured, floor placement appears immediately when Start is pressed.
-
Create a SpaceReminder object:
- Create a new SceneObject (e.g. "SpaceReminder")
- Add a child with a Text or Text3D component for the message
- Attach
UI/SpaceReminder.jsto the parent
-
Assign in SpaceReminder script:
- Camera — Main camera
- Reminder Container — The parent object containing the text
- Reminder Message — Default: "Make sure you have enough open space around you before placing the grid on the floor."
- Display Duration — Seconds to show (default 3)
- Fade Out Duration — Seconds to fade out (default 0.6)
- Pop In Duration — Seconds for the pop-in scale animation (default 0.35)
- Pop In Audio — Optional. AudioComponent that plays when the message appears
-
Assign in MainMenuManager:
- Space Reminder — Reference to the SpaceReminder script component
The message displays for a few seconds, fades out, then floor placement appears.
Create a tile prefab with:
- A box mesh (RenderMeshVisual)
- Material with
baseColorproperty for color changes - Base size: 1 unit (script scales to
TILE_SIZEfromConstants.js, default 50cm, ifscaleTilesis enabled) - Add an "Arrow" child object for direction hints
Add an arrow as a child of your tile prefab:
- Add a child object named "Arrow"
- Add a plane mesh with an arrow texture
- Rotate flat on the tile (X = -90°)
- The script automatically rotates arrows to point toward the next tile
The game uses collision-based detection for reliable tile entry. This filters out head tilts and only detects actual walking.
-
Create a Trigger Prefab:
- Create a new SceneObject with a Physics.ColliderComponent (Box shape)
- Set the collider as a Trigger (not solid)
- Shape it as a vertical plane (e.g., 40cm wide x 60cm tall x 5cm deep)
- Attach
Grid/TileTrigger.jsscript to it - Save as a prefab
-
Add a Collider to Camera:
- Add a Physics.ColliderComponent to your camera object
- Use a small sphere or box shape (e.g., 10cm)
- Set body type to Dynamic or Kinematic
-
Assign in GridManager:
- Drag your trigger prefab to the Trigger Prefab input
How it works:
flowchart TD
A[Grid created] --> B[Trigger plane at each tile center]
B --> C{Player walks forward}
C --> D[Camera collider passes through trigger]
D --> E[onOverlapEnter fires]
E --> F[TileTrigger → PlayerTracker validates]
C -->|Head tilt only| G[Collider doesn't move enough]
G --> H[No trigger - avoids false positives]
- Trigger planes are spawned at each tile center when the grid is created
- When you walk forward, your camera collider passes through the trigger
- Head tilts don't trigger because rotation doesn't move the collider forward enough
-
Add a Collider to Start Zone:
- Add a Physics.ColliderComponent to your start zone object
- Use a Box shape sized to match your start zone visual (e.g., 50cm x 50cm x 60cm tall)
- Set the collider as a Trigger (not solid)
-
Assign in StartZoneVisual:
- Drag the collider to the Zone Collider input on StartZoneVisual script
flowchart TD
A[IDLE] --> B[PLACING_GRID]
B -->|User pinch-places grid on floor| C[GRID_INTRO]
C -->|Dimmed grid, yellow START tile<br/>Host: Welcome/Goal/Level 1 or Level N| D[COUNTDOWN]
D -->|3-2-1-WATCH on floor| E[MEMORIZE]
E -->|Host: Watch!<br/>Path revealed tile-by-tile| F[PLAYING]
F -->|Host: Go!<br/>Player walks path from memory| G{Correct step?}
G -->|No| H[FAILED]
G -->|Yes| I{Reached end tile?}
I -->|No| F
I -->|Yes| J[COMPLETED]
H -->|Fail audio, reveal path| K[WAITING_IN_START_ZONE]
J -->|Success + confetti<br/>Head back to start zone| K
K -->|Start zone marker visible| L{Player enters start zone?}
L -->|Yes| C
L -->|No| K
| State | Description |
|---|---|
| IDLE | Main menu visible |
| PLACING_GRID | User places grid on floor using pinch gesture |
| GRID_INTRO | Dimmed grid with bright yellow START tile; Level 1: Welcome → Goal → Level 1; Level 2+: level announcement only |
| COUNTDOWN | 3-2-1-WATCH on floor |
| MEMORIZE | Path revealed tile-by-tile (~0.5s each), arrows on each |
| PLAYING | Path hidden, player walks from memory; idle prompt after 15s |
| COMPLETED | Success audio, confetti, "Head back to start zone!" |
| FAILED | Fail audio, reveal path, "Step back to start zone!" |
| WAITING_IN_START_ZONE | Start zone marker appears; player enters → level announcement → COUNTDOWN |
When the player walks onto a tile, the collider-based trigger fires and PlayerTracker validates the step:
flowchart TD
A[Camera collider enters tile trigger] --> B{Is tracking active?}
B -->|No| C[Ignore]
B -->|Yes| D{Same tile as before?}
D -->|Yes| C
D -->|No| E{Step matches expected path position?}
E -->|Yes| F[Correct step]
E -->|No| G[Wrong step]
F --> H{Reached end tile?}
H -->|Yes| I[Path complete - COMPLETED]
H -->|No| J[Play step sound, continue]
G --> K[Mark tile red, play error sound]
K --> L[FAILED state]
Spectacles' limited FOV makes scanning a full grid difficult. Instead:
flowchart LR
A[Yellow start tile] --> B[Reveal tile 2]
B --> C[Reveal tile 3]
C --> D[...]
D --> E[Reveal end tile]
- Player's attention starts on the yellow start tile (focal point)
- Tiles appear sequentially (~0.5s each), naturally guiding the eye
- Arrows show direction without needing to see the whole grid
- The path "draws itself" from player's perspective
- ⬜ White (dim) = All tiles at 15% opacity (background)
- 🟨 Yellow (bright) = Start tile at ~77% opacity (focal point)
- Start tile stays highlighted as the focal point (no arrows yet)
- 🟨 Yellow = Start tile (with arrow)
- 🟩 Green = Path tiles revealed one-by-one (with arrows)
- 🟦 Blue = End tile (no arrow, revealed last)
- 🟨 Yellow = Start tile (stays visible)
- ⬜ White = All other tiles at ~50% opacity with a subtle idle alpha pulse
- 🟦 Blue = End tile (stays visible)
- 🟩 Green = Tiles turn green when stepped on correctly
| Success | Wrong step |
|---|---|
![]() |
![]() |
| Achievement notification | Start zone (return here for next round) |
|---|---|
![]() |
![]() |
Edit Assets/Scripts/Utils/Constants.js to customize:
var DebugConfig = {
ENABLED: true, // Enable on-screen debug logging via TextLogger
SKIP_PATH_CHECK: false, // Skip path validation — step on end tile to instantly win
};var GridConfig = {
DEFAULT_ROWS: 5,
DEFAULT_COLUMNS: 5,
TILE_SIZE: 50, // 50cm tiles
TILE_GAP: 5, // 5cm gap between tiles
DEFAULT_ALPHA: 0.5, // Default white tile opacity
VISIBLE_ALPHA: 0.77, // Opacity for highlighted tiles
DIMMED_ALPHA: 0.15, // Background tile opacity
IDLE_PULSE_ENABLED: true,
IDLE_PULSE_ALL_TILES: true,
IDLE_PULSE_SPEED: 0.35,
IDLE_PULSE_MIN_ALPHA: 0.3,
IDLE_PULSE_MAX_ALPHA: 0.8,
};Path length formula: BASE_PATH_LENGTH + (level - 1) * PATH_INCREMENT
- Level 1: 5 tiles
- Level 2: 7 tiles
- Level 3: 9 tiles
- Level 11: 25 tiles (max)
The robot host uses pre-defined dialogue lines in Utils/DialogueLines.js. Audio plays via AudioManager when voice files are imported; falls back to timer-based progression otherwise.
The dialogue audio in this project was generated using OpenAI Text to Speech.
- Generate audio files using ElevenLabs, OpenAI Text to Speech, or similar TTS service (use
voice_lines.jsonas input) - Import audio files into Lens Studio
- Set up AudioManager — attach script, add voice files to Voice Lines array
- Name files to match dialogue IDs exactly (e.g.,
welcome.mp3,level_1.mp3)
flowchart TB
subgraph Input
P[PlacementBridge]
M[MainMenuManager]
end
subgraph Core
G[GameStateManager]
end
subgraph Gameplay
GM[GridManager]
PT[PlayerTracker]
HM[HostManager]
SZ[StartZoneVisual]
end
subgraph Storage
S[SaveManager]
end
P -->|onGridPlaced| G
M -->|Start/destroy placement| G
G -->|state, level, exit| M
G -->|generatePath, show/hide tiles| GM
G -->|start/stop tracking, callbacks| PT
G -->|play dialogue, show/hide| HM
G -->|show/hide zone| SZ
GM -->|onTriggerEntered| PT
SZ -->|onPlayerEntered/Exited| PT
PT -->|onCorrectStep, onWrongStep, onPathCompleted| G
G -->|onLevelCompleted/Failed| S
| Component | Role |
|---|---|
| GameStateManager | Orchestrates game flow, exposes global.PathFinder.Game, global.PathFinder.Placement, global.PathFinder.MainMenu |
| GridManager | Grid creation, path generation, tile visualization |
| PlayerTracker | Collision-based step validation, start zone detection |
| HostManager | Robot host with billboard behavior, dialogue playback |
| SaveManager | Persistent storage via global.PathFinder.Save |
See Assets/Scripts/Core/, Assets/Scripts/Grid/, Assets/Scripts/Player/, Assets/Scripts/Host/, and Assets/Scripts/Utils/ for full API details.
Grid not visible: Check tile prefab, verify GridParent is under ObjectVisuals, grid anchors from placement point.
Start tile not appearing bright: Verify path is generated, check dimGridBackground and showOnlyStartTile are called.
Host not appearing: Verify HostPlaceholder exists, check camera reference. Host is optional.
Arrows not showing: Ensure tile prefab has child named "Arrow", flat on tile (X rotation = -90°).
Position not tracking: Verify Device Tracking is World mode, check camera reference in PlayerTracker.
Tiles trigger too early: Keep only one collider on camera for movement; tile triggers ignore non-Camera colliders.
Grid too big for FOV: Reduce TILE_SIZE in Constants.js (default 50cm).
Countdown not starting: Zone state resets between rounds; step out and back into start zone to re-trigger.
Achievement notification sound but no popup: Ensure NotificationRoot points to the popup object; check it's not under a disabled parent.
The exit button returns to the main menu during gameplay. Place the exit button directly under the right wrist in the Lens Studio hierarchy—this works well on Spectacles. The script shows/hides the button; it binds via SIK onTriggerEnd or UIKit onTriggerUp.
- Create your exit button (e.g., Spectacles UI Kit button)
- Parent it under the Right Wrist object from Hand Tracking
- Attach
PalmExitButton.jsto a SceneObject and assign the button to Exit Button - The button appears when the game starts (after floor placement) and hides when exiting to menu
- Lens Studio (Spectacles target)
- Spectacles device (recommended for testing)
Feel free to fork this project to use, modify, or build upon it.
- Robot host ("Drobo") — by The Dive VR Arcade (CC-BY) via Poly Pizza
- Countdown sound ("808 drum percussion interface click netural count down three seconds.wav") — by ryusa (CC BY 4.0) via Freesound
- Failure jingle ("Failure Jingle.ogg") — by TitanKaempfer (CC BY 4.0) via Freesound
MIT License — see LICENSE for details.















