# 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 0. **Project Setup:** Run the commands: `npm install`, then `npm run dev` in the project repository top folder; alternatively, access the game in [this website](https://pobadoba.com/games/maze) 1. **Start the Game:** Press **R** on the start screen to begin 2. **Navigate:** Use **WASD** to move, mouse to look around 3. **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) 4. **Reach the Exit:** Once you have the key, reach the exit door 5. **Survive the Time:** You have 60 seconds per round. Time runs out = Game Over (Job Application Jumpscare ending) 6. **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 setup - `startRenderLoop(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 settings - `switchCameraMode(scene, canvas, fpCamera, overviewCamera, state)` — Toggles between modes (V key) - `updateOverviewCameraForMaze(overviewCamera, w, h)` — Adjusts overview camera for current maze size - `attachCamera(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 map - `buildLevelFromGrid(scene, grid, state, levelMeshes)` — Creates floor and wall meshes from grid - `placeChestsOnDeadEnds(scene, grid, deadEnds, minCount, seed, state, levelMeshes)` — Places chests, marks key chest - `placeExit(scene, grid, seed, state, levelMeshes)` — Places exit door on available dead-end - `spawnCameraAt(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 chest - `checkExitProximity(playerPos, exitPos, threshold)` — Distance check for win condition - `setChestHighlight(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)` — Registers `scene.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:** 1. Update HUD display and sphere animation 2. Raycast for highlighted chest 3. 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 texture - `createWallMaterial(scene)` — Wall texture - `createChestMaterial(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 sketch - `hideGameOverScreen(state)` — Show canvas, hide p5 overlay, stop sketches - `showStartScreen(state)` — Initialize p5 start screen sketch - `hideStartScreen(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 state - `setLowTimeWarning(isLowTime)` — Add/remove "low-time" CSS class for pulsing red - `updateSphereMesh(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:** ```javascript 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 algorithm - `findDeadEnds(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): ```javascript 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.mazeGameState` prevents 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** 1. **Pointer Lock Exit:** Pointer lock may be annoying for newcomers to web browser games 2. **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.