2026-05-10 23:33:56 +09:00
2026-05-10 18:05:17 +09:00
2026-05-10 16:49:59 +09:00
2026-05-10 23:33:56 +09:00
2026-05-10 16:49:59 +09:00
2026-05-10 23:33:56 +09:00
2026-05-05 18:17:28 +09:00
2026-05-10 17:43:03 +09:00
2026-05-10 17:43:03 +09:00
2026-05-10 23:33:56 +09:00

Untitled Maze Game - ID30011 Midterm Project

A 3D First-Person Time-Attack Maze Game with Progressive Difficulty


Project Information


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

  1. Project Setup: Run the commands: npm install, then npm run dev in the project repository top folder; alternatively, access the game in this website
  2. Start the Game: Press R on the start screen to begin
  3. Navigate: Use WASD to move, mouse to look around
  4. 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)
  5. Reach the Exit: Once you have the key, reach the exit door
  6. Survive the Time: You have 60 seconds per round. Time runs out = Game Over (Job Application Jumpscare ending)
  7. 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:

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):

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.

Description
No description provided
Readme MIT 9.3 MiB
Languages
JavaScript 81.3%
CSS 13.3%
HTML 5.4%