Untitled Maze Game - ID30011 Midterm Project
A 3D First-Person Time-Attack Maze Game with Progressive Difficulty
Project Information
- Name: Bumgyu Suh
- Student ID: 20240905
- Student Email: bumgyu@kaist.ac.kr
- Repository URL: https://git.prototyping.id/20240905/Untitled-Maze-Game
- Video Demonstration: https://youtu.be/8LDcLpsNJck (turn on youtube subtitles)
- Play Link: https://pobadoba.com/games/maze
Game Overview
Concept
Untitled Maze Game is a 3D first-person maze escape game built with Babylon.js and p5.js. Players must navigate procedurally-generated mazes in a race against time, collecting a key and finding the exit within 60 seconds, but there's a twist! You must not open a chest that you have already opened before! Each successful round increases difficulty—maze size grows, more chests appear, and players advance to the next level. If you fail... there is a little surprising waiting for you (made with p5.js).
Gameplay Flow
START SCREEN
↓ Press R
GAMEPLAY (60 seconds)
↙ Time Up / Found Exit ↖
GAME OVER NEXT LEVEL
(Job Application
Jumpscare)
How to Play
- Project Setup: Run the commands:
npm install, thennpm run devin the project repository top folder; alternatively, access the game in this website - Start the Game: Press R on the start screen to begin
- Navigate: Use WASD to move, mouse to look around
- Find the Key: Left-Click chests until you find the key, do not click on a chest you have opened before (leads to game over)
- Reach the Exit: Once you have the key, reach the exit door
- Survive the Time: You have 60 seconds per round. Time runs out = Game Over (Job Application Jumpscare ending)
- Progress: Successfully exiting unlocks the next level with a larger maze and more chests
Controls
| Key | Action |
|---|---|
| W/A/S/D | Move forward/left/backward/right |
| Mouse | Look around (first-person) |
| Left Click | Open a highlighted chest |
| R | Start game (from start screen) or restart (from game-over screen) |
| B | (For debugging purposes) Toggle debug panel (hidden by default) |
| V | (For debugging purposes) Switch camera mode (first-person ↔ overview) |
| ESC | Exit pointer lock |
Code Documentation
Packages Used
- 3D Graphics: Babylon.js (3D scene, camera, meshes, rendering)
- 2D Graphics: p5.js (particle effects for game-over screen)
- Audio: Web Audio API
Files Structure
src/
├── babylon_panel.js # Game orchestrator (scene init, controller, API)
├── html_panel.js # Debug UI state management
├── p5_particles.js # Particle effects for game over screen
├── game/
│ ├── state.js # Shared game state (config + runtime)
│ ├── maze.js # Procedural maze generation (seeded RNG)
│ ├── grid.js # Coordinate conversion, collision helpers
│ ├── sfx.js # Sound effect playback system
│ ├── scene-init.js # Babylon.js engine & scene initialization
│ ├── camera-manager.js # Camera creation, mode switching, updates
│ ├── game-loop.js # Main render loop, game state machine
│ ├── level-generator.js # Level building, mesh placement, spawning
│ ├── collisions.js # Raycasting, proximity checks, highlighting
│ └── screen-manager.js # Start/game-over screen transitions
├── controls/
│ └── input-handler.js # Keyboard, pointer, click event handling
├── assets/
│ └── materials.js # Babylon.js material & texture factories
└── ui/
└── hud.js # HUD updates, visual animations
css/
└── style.css # Responsive layout, HUD styling, animations
img/
├── img_start.png # Start screen background
├── img_jobapplication.png # Game-over screen background
├── img_chest.png # Chest texture
├── img_door.png # Exit door texture
├── img_wall.png # Wall texture
└── img_ground.png # Floor texture
sfx/
├── sfx_click.wav # UI interaction sound
├── sfx_chest_open.wav # Chest opening sound
├── sfx_key.wav # Key collection sound
├── sfx_clock.wav # Low-time warning alarm
├── sfx_step.wav # Footstep sound
├── sfx_win.wav # Level complete sound
├── sfx_lose.wav # Game over sound
└── sfx_chest_close.wav # Chest closing sound
Module Organization:
- game/: Core game logic (state, generation, collision, audio)
- controls/: Input and event handling
- assets/: Reusable factories (materials, textures)
- ui/: Visual feedback (HUD, overlays)
- Root level (babylon_panel.js): Main script that ties everything together
Core Modules
1. babylon_panel.js (Game Orchestrator)
Purpose: Main application controller that coordinates all game systems
Responsibilities:
- Scene initialization via
initializeScene() - Camera setup and attachment via
createCameras() - Input handler registration via
setupInputHandlers() - Game loop registration via
registerGameLoop() - Screen transitions via
showGameOverScreen(),hideGameOverScreen(), etc. - Level generation coordination
- State management and API exposure
Key Flow:
Page Load → babylon_panel.js
↓ initialize scene, cameras, sphere
↓ setupInputHandlers() → register keyboard/pointer events
↓ registerGameLoop() → register frame-by-frame updates
↓ showStartScreen()
↓ (user presses R)
↓ startRunFromStartScreen() → generateLevel()
↓ (gameplay loop runs until win/lose)
↓ showGameOverScreen() → p5 particle sketch
Exports:
window.mazeGameApi.generateLevel()— Called by html_panel.js (debug controls)
2. game/scene-init.js (Scene Initialization)
Purpose: Babylon.js engine and scene setup
Functions:
initializeScene(canvas)— Creates engine, scene, lighting, gravity, collision setupstartRenderLoop(engine, scene)— Starts the main render loop and resize handler
Benefits:
- Encapsulates Babylon.js boilerplate
- Easier to swap or test rendering configuration
- Decouples main scene runner from engine details
3. game/camera-manager.js (Camera Management)
Purpose: First-person and overview camera creation and switching
Functions:
createCameras(scene, canvas)— Creates both fpCamera and overviewCamera with all settingsswitchCameraMode(scene, canvas, fpCamera, overviewCamera, state)— Toggles between modes (V key)updateOverviewCameraForMaze(overviewCamera, w, h)— Adjusts overview camera for current maze sizeattachCamera(scene, camera, canvas)— Attaches camera to scene and activates it
Benefits:
- Isolates complex camera configuration
- Pointer lock exit handled cleanly during mode switches
- Easier to add new camera modes (e.g., isometric, cinematic)
4. game/level-generator.js (Level Generation & Building)
Purpose: Procedural maze-to-scene conversion
Functions:
clearLevelMeshes(levelMeshes, state)— Disposes old meshes, clears chest mapbuildLevelFromGrid(scene, grid, state, levelMeshes)— Creates floor and wall meshes from gridplaceChestsOnDeadEnds(scene, grid, deadEnds, minCount, seed, state, levelMeshes)— Places chests, marks key chestplaceExit(scene, grid, seed, state, levelMeshes)— Places exit door on available dead-endspawnCameraAt(scene, grid, camera, state)— Positions camera far from exit
Benefits:
- Pure spatial logic, no game state mutations beyond scene meshes
- Can be tested with mock scenes
- Easy to add new level features (traps, collectibles, etc.)
5. game/collisions.js (Interaction Detection)
Purpose: Raycasting and proximity checks for game interactions
Functions:
checkChestRaycast(scene, fpCamera, maxDistance)— Raycast from camera to detect highlighted chestcheckExitProximity(playerPos, exitPos, threshold)— Distance check for win conditionsetChestHighlight(mesh)— Apply/remove outline highlight
Benefits:
- Isolated raycasting logic
- Reusable collision checks
- Easier to add new interaction types
6. game/game-loop.js (Main Game Loop)
Purpose: Frame-by-frame game state updates and logic
Functions:
registerGameLoop(scene, engine, state, callbacks)— Registersscene.registerBeforeRender()with:- HUD updates (time, key, rounds)
- Chest raycasting and highlighting
- Timer countdown
- Low-time alert (< 10 seconds)
- Footstep audio based on movement
- Exit proximity check for win
- Time-up check for lose
Game Loop Sequence:
- Update HUD display and sphere animation
- Raycast for highlighted chest
- If gameplay active:
- Decrement timer
- Check low-time threshold (play clock sound once)
- Update footsteps (0.75 distance, 220ms min)
- Check exit proximity (< 1.8 units)
- If time up: call onGameOver callback
Benefits:
- Separates frame logic from initialization
- Callbacks for win/lose handled by orchestrator
- Easy to debug timing and collision issues
7. controls/input-handler.js (Input Management)
Purpose: Keyboard, pointer lock, and click event handling
Functions:
setupInputHandlers(canvas, state, callbacks)— Registers:- Click for pointer lock (requestPointerLockSafely)
- Keyboard: W/A/S/D/V/R/B with audio priming
- Pointer events for chest interaction
Event Bindings:
| Input | Action |
|---|---|
| Click | Request pointer lock |
| W/A/S/D | Prime audio context + movement |
| V | onCameraToggle callback |
| R | onRestart (if game over) or onStartGame (if on start screen) |
| B | onDebugToggle callback |
| Left Click on Chest | Check chest state, mark as opened, play sounds |
Benefits:
- Centralized input routing
- Callbacks allow orchestrator to control behavior
- Easy to add gamepad support
8. assets/materials.js (Material Factories)
Purpose: Create reusable Babylon.js materials with textures
Functions:
createFloorMaterial(scene, width, height)— Ground with repeating texturecreateWallMaterial(scene)— Wall texturecreateChestMaterial(scene, isKey)— Chest with optional golden emissive (key variant)createExitMaterial(scene)— Door texture
Benefits:
- Decouples texture setup from level generation
- Reusable material factories for future features
- Centralized texture configuration
9. game/screen-manager.js (Screen Transitions)
Purpose: Show/hide start and game-over screens with p5.js sketches
Functions:
showGameOverScreen(state)— Hide canvas, show p5 overlay, start particle sketchhideGameOverScreen(state)— Show canvas, hide p5 overlay, stop sketchesshowStartScreen(state)— Initialize p5 start screen sketchhideStartScreen(state)— Stop start screen sketch
Benefits:
- Encapsulates p5 lifecycle management
- Clean separation of screen logic
- Easy to add new screens (level intro, pause menu, etc.)
10. ui/hud.js (HUD Display)
Purpose: Update on-screen HUD elements and visual feedback
Functions:
updateHUD(state)— Update time, key, rounds displays from shared statesetLowTimeWarning(isLowTime)— Add/remove "low-time" CSS class for pulsing redupdateSphereMesh(sphere, level)— Rotate and scale sphere based on level
Benefits:
- Separates DOM updates from game logic
- Reusable styling/animation controls
- Can easily add new HUD elements
11. game/state.js (Shared State)
Purpose: Centralized game state management to prevent coupling between modules
Structure:
window.mazeGameState = {
config: {
seed, // Reproducible maze generation
level, // Current progression level
mazeWidth: 11, // Base width (increases per level)
mazeHeight: 11, // Base height
minChestDeadEnds: 2 // Min chests to place
},
runtime: {
runActive, // Is gameplay active?
hasKey, // Player collected key?
roundsCompleted, // Levels completed this run
elapsedSeconds, // Countdown timer (60 → 0)
message // Status text for HUD
}
}
Benefits:
- Single source of truth for all game state
- All modules read/write to same object (no data duplication)
- Easy to serialize/save game state
- Enables time-travel debugging via console inspection
12. game/maze.js (Procedural Generation)
Purpose: Generate solvable, deterministic mazes using seeded randomization
Functions:
seededRng(seed)— Deterministic random number generator (same seed = same sequence)generateMazeGrid(w, h, seed)— Recursive backtracking maze algorithmfindDeadEnds(grid)— Locate dead-end cells for chest/exit placement
Algorithm: Recursive backtracking with seeded RNG ensures:
- Every maze is solvable (connected)
- Same seed produces identical maze (perfect for replays)
- Configurable difficulty (width/height parameters)
Benefits:
- Deterministic yet varied level generation
- No performance issues (pre-computed before rendering)
- Perfect reproducibility for competitive play
13. game/grid.js (Coordinate Utilities)
Purpose: Convert between grid coordinates and 3D world space for collision detection
Functions:
gridCellToWorld(grid, x, y, cellSize)— Grid cell → world position (returns{x, z})isWalkableCell(grid, x, y)— Check if cell is navigable (not a wall)
Key Insight:
Mazes are generated as 2D grids (1 = wall, 0 = path). This module bridges the gap between grid coordinates used by maze.js and 3D world positions used by level-generator.js.
Benefits:
- Decouples grid logic from spatial positioning
- Makes collision detection and pathfinding calculations simpler
- Reusable for any grid-based game mechanic
14. game/sfx.js (Audio System)
Purpose: Load and playback polyphonic sound effects with Web Audio API
Functions:
playSfx(name, volume)— Play a sound effect by name (cloned for polyphony)primeSfx()— Initialize audio context (required by browser for first sound)
Audio Files (imported via ES6, bundled by Vite):
import clickUrl from "../../sfx/sfx_click.wav"
import chestOpenUrl from "../../sfx/sfx_chest_open.wav"
import chestCloseUrl from "../../sfx/sfx_chest_close.wav"
import keyUrl from "../../sfx/sfx_key.wav"
import clockUrl from "../../sfx/sfx_clock.wav"
import stepUrl from "../../sfx/sfx_step.wav"
import winUrl from "../../sfx/sfx_win.wav"
import loseUrl from "../../sfx/sfx_lose.wav"
Polyphony via Cloning:
Each call to playSfx() clones the audio node, allowing multiple sounds to play simultaneously (e.g., footsteps + clock alarm).
Benefits:
- Polyphonic sound effects (no cutting each other off)
- Browser-compatible audio context priming
- Centralized audio management (easy to add volume controls, reverb, etc.)
Key Features & Implementation Details
| Feature | Implementation |
|---|---|
| 3D Rendering | Babylon.js UniversalCamera, procedural mesh generation |
| Procedural Mazes | Seeded random generation with configurable dimensions |
| Time-Attack Mode | 60-second countdown timer with auto-game-over on timeout |
| Progressive Difficulty | Maze size & chest count increase per completed level |
| Collision Detection | Raycasting for chest interaction, sphere collision for exit |
| Sound System | polyphonic sound effects with Web Audio API |
| Particle Effects | p5.js animated particles with physics on game-over screen |
| Start Screen | Full-screen p5.js panel with starting image |
| Game-Over Screen | Full-screen overlay with job application image + particles |
| Visual Warnings | Red pulsing timer + clock sound when time < 10 seconds |
| Camera Modes | First-person (WASD + mouse) and overhead orbital view |
| Responsive Layout | Full-screen canvas with bottom-overlay debug controls |
Code Quality Patterns:
- Shared State Pattern:
window.mazeGameStateprevents data coupling across modules - Factory Pattern:
generateLevel()creates and caches mesh instances - Observer Pattern:
registerBeforeRender()for frame-synchronized updates - Async/Await: p5.js async image loading for non-blocking resource loading
Known Problems
- Pointer Lock Exit: Pointer lock may be annoying for newcomers to web browser games
- Chunk Size Warning: Built JavaScript is ~9.1 MB due to image assets
- Not an issue for local play
Design Decisions & Rationale
1. Modular Architecture with Separation of Concerns
Decision: Organize code into 14 focused modules rather than monolithic files
Rationale:
- Each module has a single responsibility (scene setup, input handling, collision detection, etc.)
- No circular dependencies — clean dependency flow from orchestrator down to utilities
- Easier to test, debug, and extend individual features
- New developers can understand one module without understanding the whole codebase
- Easy to add features: extend relevant modules instead of modifying monolithic files
Architecture Pattern:
babylon_panel.js (orchestrator) ← high-level control
├→ game/scene-init.js ← rendering setup
├→ game/camera-manager.js ← camera control
├→ controls/input-handler.js ← event routing
├→ game/level-generator.js ← spatial layout
├→ game/game-loop.js ← frame updates
├→ game/screen-manager.js ← UI transitions
└→ game/state.js ← shared data (all modules)
2. Shared State Pattern via window.mazeGameState
Decision: Centralize all game state in single window.mazeGameState object
Rationale:
- Prevents tight coupling between UI, physics, and rendering systems
- All modules read/write from same source of truth (no data duplication)
- Simplifies debugging (inspect state in console at any time)
- Enables hot-reloading during development
3. Callback-Based Event Routing
Decision: Lower modules don't know about higher modules; communication via callbacks
Rationale:
- Reduces coupling between layers
- Easy to mock/test: pass different callbacks to change behavior
- Flexible event handling: same module used for different purposes
- Example:
registerGameLoop(scene, state, callbacks)doesn't know about UI — it just calls callbacks
4. Babylon.js Over Three.js
Decision: Used Babylon.js for 3D graphics
Rationale:
- Built-in collision detection and raycasting
- Superior camera controls (UniversalCamera with pointer lock)
- Efficient mesh instancing for maze cells
- Excellent documentation for procedural generation
5. p5.js for Particle Effects
Decision: Delegated particle rendering to p5.js instead of Babylon.js
Rationale:
- Cleaner separation of concerns (game logic ≠ visual effects)
- p5.js's simple drawing API reduces code complexity
- Easy to swap/experiment with particle physics without affecting core game
- Full-screen 2D canvas doesn't compete with 3D rendering pipeline
6. Time-Attack Mode (vs. Exploration, Level Editing)
Decision: 60-second countdown instead of unlimited time
Rationale:
- Creates urgency and strategic decision-making (pick efficient paths vs. explore)
- Enables meaningful progression (faster times unlock harder mazes)
- Reduces scope (no need for complex AI, item management, etc.)
7. Procedural Maze Generation with Seeded RNG
Decision: Use seeded random number generator for deterministic maze generation
Rationale:
- Varied layouts via different seeds (infinite replayability)
- Better than storing pre-made levels (scalable to any difficulty)
- Recursive backtracking ensures every maze is solvable
Help from AI & Resources
AI Assistance
- GitHub Copilot: Used throughout development for code quality, structure, and refactoring
- Early Development: Suggested separating static maze data from dynamic game state (prevents coupling)
- Module Organization: Proposed logical file structure to improve maintainability
- Code Review: Reviewed p5.js particle physics, Babylon.js camera control, Web Audio API integration
- Refactoring: Assisted with modularizing 570-line monolithic file into 14 focused modules
- Extracted scene initialization, camera management, game loop, level generation, collision detection
- Created input handler, material factories, HUD updates, screen manager modules
- Verified no breaking changes, ensured build compatibility, tested audio bundling
- Documentation:
Resources & Documentation
- Babylon.js Playground: Reference for collision detection, camera control, and mesh creation
Conclusion
Untitled Maze Game was an interesting project as it made me realize that a lot is possible simply through javascript libraries and today's LLMs. AI really saved a lot of time that would have normally taken me hours to debug, and having dialogue with the AI over the code structure and such was quite helpful and interesting. I think the advantage of AI is I can spend a lot more time on thinking about the big-picture structure of the code rather than the little details and line-by-line code, which is more fun.