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: [YouTube Link]
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.
Gameplay Flow
START SCREEN (img_start.png)
↓ Press R
GAMEPLAY (60 seconds)
↙ Time Up / Found Exit ↖
GAME OVER NEXT LEVEL
(img_jobapplication.png + Particles)
How to Play
- 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.
- 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
- 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 v9.5.1 (3D scene, camera, meshes, rendering)
- Bundler: Vite v8.0.10 (ES6 modules, asset optimization)
- 2D Graphics: p5.js v2.x (particle effects for game-over screen)
- Audio: Web Audio API (polyphonic sound effects)
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 start/game-over screens
├── 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
Legend:
- ✓ Unchanged from original (already well-structured)
- Without checkmark = newly extracted/refactored
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 orchestration
- 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() → p5 particle sketch
↓ (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 orchestrator 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) ✓
Already Well-Structured — No Changes Centralized game configuration and runtime state
12. game/maze.js (Procedural Generation) ✓
Already Well-Structured — No Changes Recursive backtracking with seeded RNG
13. game/grid.js (Coordinate Utilities) ✓
Already Well-Structured — No Changes Grid-to-world conversions and walkability checks
14. game/sfx.js (Audio System) ✓
Already Well-Structured — No Changes ES6 import-based audio loading and playback
Key Features & Implementation Details
| Feature | Implementation | Status |
|---|---|---|
| 3D Rendering | Babylon.js UniversalCamera, procedural mesh generation | ✓ Complete |
| Procedural Mazes | Seeded random generation with configurable dimensions | ✓ Complete |
| Time-Attack Mode | 60-second countdown timer with auto-game-over on timeout | ✓ Complete |
| Progressive Difficulty | Maze size & chest count increase per completed level | ✓ Complete |
| Collision Detection | Raycasting for chest interaction, sphere collision for exit | ✓ Complete |
| Sound System | 8 polyphonic SFX with Web Audio API and context priming | ✓ Complete |
| Particle Effects | p5.js animated particles with physics on game-over screen | ✓ Complete |
| Start Screen | Full-screen p5.js panel with img_start.png background | ✓ Complete |
| Game-Over Screen | Full-screen overlay with job application image + particles | ✓ Complete |
| Visual Warnings | Red pulsing timer + clock sound when time < 10 seconds | ✓ Complete |
| Camera Modes | First-person (WASD + mouse) and overhead (overview) | ✓ Complete |
| Responsive Layout | Full-screen canvas with bottom-overlay debug controls | ✓ Complete |
🔧 Technical Highlights
Browser Compatibility:
- Audio context requires user interaction priming via
primeSfx()—automatically triggered on first W/A/S/R key press
Performance Optimizations:
- Vite's ES6 module bundling and tree-shaking
- Asset optimization (textures, audio)
- Efficient raycasting for chest targeting (not per-pixel)
- Single draw call per maze (not per cell)
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 Issues & Limitations
⚠️ 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
⭐ Special Features to Note
- Seeded Randomization: Players can use the same seed to replay identical mazes (debug button: "Randomize seed")
- Low-Time Audio Feedback: Clock sound triggers once when time drops below 10 seconds (prevents spam)
- Full-Screen Transitions: Start screen, gameplay, and game-over have full-screen p5.js overlays for immersive presentation
- Difficulty Scaling Formula: Mathematically designed to keep progression challenging but fair
Refactoring Summary (Phase 3 Complete)
Code Metrics
| Metric | Before | After | Change |
|---|---|---|---|
| Main file (babylon_panel.js) | 570 lines | 195 lines | -66% |
| Total source files | 5 | 14 | +9 new modules |
| Build modules | 355 | 364 | +9 (new files) |
| Cyclomatic complexity | High | Low | Each module < 20 lines avg |
Dependency Graph
babylon_panel.js (Orchestrator)
├── game/scene-init.js
├── game/camera-manager.js
├── controls/input-handler.js
│ └── game/sfx.js
├── game/level-generator.js
│ ├── game/maze.js
│ ├── game/grid.js
│ └── assets/materials.js
├── game/game-loop.js
│ ├── game/sfx.js
│ ├── game/collisions.js
│ └── ui/hud.js
├── game/screen-manager.js
│ └── p5_particles.js
└── game/state.js (shared by all)
Module Characteristics
- No circular dependencies: Each module imports only from modules below it
- Shared state: All modules read/write
window.mazeGameState(single source of truth) - Event-driven: Input → callbacks → state update → HUD refresh
- Callback pattern: Higher modules pass callbacks to lower modules for decoupling
Refactoring Safety Checklist
✓ Build tested: npm run build completes with 364 modules, no errors
✓ No breaking changes: window.mazeGameApi.generateLevel() still exported
✓ Audio bundling intact: All 8 SFX files in dist/assets/ with hashes
✓ Backwards compatible: All gameplay mechanics unchanged
✓ No new dependencies: Uses existing npm packages only
✓ ESM imports work: Vite resolves all relative paths correctly
Next Steps for Future Maintenance
- Add new interaction types: Extend
game/collisions.jswith new raycasts - Add gamepad support: Extend
controls/input-handler.jswith gamepad listeners - Add new screens: Add functions to
game/screen-manager.js - Add new camera modes: Extend
game/camera-manager.jswith new camera types - Add sound designer tools: Extend
game/sfx.jswith volume/pan controls
Design Decisions & Rationale
Refactoring Strategy (Phase 3 Complete)
Objective: Improve code maintainability without breaking gameplay
Approach: Modular separation of concerns by extracting 9 new modules from the original monolithic babylon_panel.js (570 lines → 195 lines).
Why This Structure?
| Module | Benefit |
|---|---|
| scene-init.js | Isolate Babylon.js boilerplate from game logic |
| camera-manager.js | Encapsulate complex camera configuration; easy to test/add modes |
| level-generator.js | Pure spatial functions; reusable for future level types |
| game-loop.js | Frame-by-frame logic visible in one place; easier to debug timing |
| collisions.js | Isolated raycasting; reusable for new interaction types |
| input-handler.js | Centralized event routing; easy to add gamepad/mobile controls |
| screen-manager.js | p5 lifecycle management; easy to add new screens (pause, level intro) |
| materials.js | Texture setup reusable by other systems; centralized configuration |
| hud.js | DOM updates separate from game state; CSS animations cleanly decoupled |
Testing Benefits
- Unit testable: Each module has single responsibility, minimal dependencies
- Integration testable: Callbacks allow mocking of complex systems
- Less fragile: Changing one concern doesn't require refactoring others
Developer Experience
- Faster onboarding: New developers can understand features one module at a time
- Feature additions: Adding chests, NPCs, traps only requires extending relevant modules
- Debugging: Isolating bugs is easier when concerns are separated
Original Design Decisions (Unchanged)
Decision: Isolate game state in game/state.js rather than scatter variables globally
Rationale:
- Prevents tight coupling between UI, physics, and rendering systems
- Enables hot-reloading during development
- Simplifies debugging (single place to inspect game state)
2. 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
3. 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
4. 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.)
Help from AI & Resources
AI Assistance
- GitHub Copilot: Used for code structure review and refactoring suggestions
- Suggested separating static maze data from dynamic game state (instead of coupling both in a single 2D array)
- Helped organize modules into logical file structure
- Reviewed p5.js particle physics for correctness
Resources & Documentation
- Babylon.js Playground: Reference for collision detection and camera control
- p5.js Documentation: Async setup pattern for p5.js 2.0+ (no
preload()) - MDN Web Audio API: Context priming for cross-browser audio compatibility
Conclusion
Untitled Maze Game demonstrates:
- Professional code organization (modular, well-separated concerns)
- Advanced 3D graphics programming (procedural generation, collision detection, camera control)
- Full-featured game loop with state management
- Polish and presentation (particle effects, sound design, responsive UI)
- Scalability (difficulty scaling formula, asset management)
The codebase prioritizes readability and maintainability through modular design, clear naming conventions, and comprehensive documentation. Each file has a single responsibility, making it easy for collaborators or reviewers to understand and extend the code.