A first-person maze navigation game built in Java (2019-2020) that simulates 3D perspective using 2D polygon rendering techniques. This project demonstrates raycasting-inspired principles similar to early first-person games like Wolfenstein 3D, but implemented using pure Java AWT/Swing without any 3D graphics libraries.
This maze game presents a dual-view interface:
- Top-down map view: Shows the entire maze layout with your position
- First-person 3D view: Renders a pseudo-3D hallway perspective that updates based on your position and facing direction
The player navigates through a maze loaded from a text file, avoiding traps and death tiles, using teleporters, and trying to reach the exit with the minimum number of moves.
The main entry point and game controller that extends JPanel and implements KeyListener and MouseListener.
Key Responsibilities:
- Manages the JFrame window and rendering loop
- Handles keyboard input (arrow keys for movement/rotation)
- Coordinates game state (finished, death, teleportation)
- Loads maze from file (
maze.txt) - Renders both 2D overhead map and 3D first-person view
- Implements game mechanics (traps, teleportation, death tiles)
Important Methods:
setBoard()(lines 136-215): Parses maze file and initializes game statepaintComponent()(lines 38-135): Main rendering methodkeyPressed()/keyReleased()(lines 222-351): Input handlingcheckTrap(),checkTeleport(),checkDeath()(lines 247-301): Game mechanics
Contains the pseudo-3D rendering logic that creates the illusion of a first-person hallway view.
Key Features:
- Defines 13 polygon shapes representing hallway segments at different depths
- Implements
drawSurrounding()(lines 33-412): The core 3D rendering engine - Handles all four cardinal directions (ZERO=East, NINETY=North, ONEEIGHTY=West, TWOSEVENTY=South)
- Uses perspective scaling and depth-based shading
Polygon Structure:
ceiling1, ceiling2, ceiling3: Three depth levels of ceilingfloor1, floor2, floor3: Three depth levels of floorleft1, left2, left3: Three depth levels of left wallsright1, right2, right3: Three depth levels of right wallswall1, wall2, wall3: Three depth levels of front-facing walls
Represents the player character navigating the maze.
Key Features:
- Tracks position (
Location loc) and facing direction - Manages turn counter (score system)
- Implements movement and rotation logic
- Uses constants for facing directions (ZERO=1, NINETY=2, ONEEIGHTY=3, TWOSEVENTY=4)
Important Methods:
move()(lines 35-60): Executes actual movement after validationpotentialMove()(lines 61-80): Calculates proposed movement for collision detectionturnLeft(),turnRight()(lines 81-116): Rotation logic
Represents a single tile in the maze grid (despite the name, it can be walls, paths, traps, etc.).
Properties:
isWall: Solid wall (impassable)trap: Penalty tile (adds 20 moves)teleport: Portal to another teleport tiledeath: Instant game over
Simple coordinate wrapper for x,y positions in pixel space.
Note: Uses relative modification methods (setX(int i) adds to x, doesn't set it absolutely)
The maze is defined in a text file using ASCII characters:
#= Wall (impassable)(space) = Path (walkable)*= Trap (adds 20 moves penalty)T= Teleporter (transports to another T tile)X= Death tile (instant game over)- First space character = Starting position
- Last row space = Exit goal
- UP Arrow: Move forward (in the direction you're facing)
- LEFT Arrow: Turn left (rotate 90° counterclockwise)
- RIGHT Arrow: Turn right (rotate 90° clockwise)
Your score is the number of moves taken to complete the maze:
- ≤ 110 moves: IMPOSSIBLE
- 111-130 moves: GREAT JOB
- 131-209 moves: BETTER LUCK NEXT TIME
- ≥ 210 moves: Death (ran out of moves)
Top-down map:
- Gray: Walls
- Light Gray: Paths
- Yellow: Traps
- Blue: Teleporters
- Red: Death tiles
- Red outline: Your position
3D view:
- Uses depth-based shading (
.darker()for distance) - Red outlines on wall edges for definition
How do you create a 3D first-person view using only 2D polygons? This project solves that problem using a technique inspired by raycasting but implemented with pre-defined polygon shapes.
The key insight is that 3D perspective can be simulated by drawing 2D polygons that shrink toward a vanishing point at the screen center.
The renderer creates three "depth layers" representing:
- Layer 1: Immediate surroundings (1 tile away)
- Layer 2: Mid-distance (2 tiles away)
- Layer 3: Far distance (3 tiles away)
Each layer uses progressively smaller polygons positioned closer to the center of the screen.
Floor Example:
floor1: Large trapezoid at bottom (close)
[250,650,750,150] x [525,525,600,600]
floor2: Medium trapezoid (middle distance)
[300,600,650,250] x [487.5,487.5,525,525]
floor3: Small trapezoid (far distance)
[325,575,600,300] x [468.75,468.75,487.5,487.5]
Notice how:
- Width narrows: 500px → 350px → 250px
- Height compresses: 75px → 37.5px → 18.75px
- Converges toward center (450px horizontal, ~350px vertical)
For each depth level (1-3 tiles ahead), the drawSurrounding() method:
- Checks bounds to prevent array out of bounds
- Queries the maze at that position in the facing direction
- Conditionally renders based on what's there:
- If there's a wall ahead → draw front wall polygon
- If path continues → draw floor and check sides
- If walls to left/right → draw side wall polygons
Example (Facing East - Case 1, lines 100-172):
// Can the player see 1 tile ahead?
if(!walls[p.getR()][p.getC()+1].isWall()) {
g.fillPolygon(floor2); // Draw mid-distance floor
// Are there walls to the sides at this depth?
if(walls[p.getR()+1][p.getC()+1].isWall())
g.fillPolygon(right2); // Draw right wall
if(walls[p.getR()-1][p.getC()+1].isWall())
g.fillPolygon(left2); // Draw left wall
// Can player see 2 tiles ahead? (recurse deeper)
if(!walls[p.getR()][p.getC()+2].isWall()) {
// ... check for walls and draw floor3/left3/right3
}
}To enhance depth perception, farther objects are rendered darker:
g.setColor(Color.GRAY); // Close (layer 1)
g.setColor(Color.GRAY.darker()); // Mid (layer 2)
g.setColor(Color.GRAY.darker().darker()); // Far (layer 3)This simulates atmospheric fog and helps the player judge distance.
Pros:
- No complex 3D math or matrices required
- Runs efficiently on any Java-capable system
- Educational: clearly shows perspective principles
- Retro aesthetic similar to 1990s dungeon crawlers
Cons:
- Limited to rectilinear (grid-based) mazes
- Only renders 3 tiles deep
- Fixed viewing angles (90° rotations only)
- Manual polygon definition for each direction
Games like Wolfenstein 3D use raycasting, where rays are cast from the player's position to detect walls at arbitrary angles and distances. This project uses a simplified approach:
- Raycasting: Cast rays → measure distance → calculate column height
- This project: Pre-defined polygons → query maze at fixed depths → conditionally render
This is more like portal rendering or BSP-based techniques, where you pre-compute viewable regions.
The project uses two coordinate systems:
- Pixel Coordinates: Screen positions (0-1000 width, 0-800 height)
- Grid Coordinates: Maze array indices (row, column)
Conversion: Each grid cell is 5×5 pixels
gridRow = pixelY / 5gridCol = pixelX / 5
The game uses a two-step movement system:
- Potential Move: Calculate where player wants to go (
Explorer.potentialMove()) - Validation: Check if that position is valid (
Maze.canMove()) - Execute: If valid, commit the move (
Explorer.move())
This prevents walking through walls and allows for collision detection.
The Explorer class uses numeric constants for cardinal directions:
NINETY (2)
↑
|
ONEEIGHTY (3) ← + → ZERO (1)
|
↓
TWOSEVENTY (4)
This maps to array coordinates:
- ZERO (1) = +X (right)
- NINETY (2) = -Y (up)
- ONEEIGHTY (3) = -X (left)
- TWOSEVENTY (4) = +Y (down)
Teleporters work by:
- Detecting when player enters a teleport tile
- Storing the current teleport location
- Scanning maze for the OTHER teleport tile
- Moving player to that location
- Using a
teleportedflag to prevent instant re-teleportation
The program displays a rotated arrow image showing which direction you're facing (lines 314-345). The image file is swapped based on the facing direction after rotation.
- Java Development Kit (JDK) 8 or higher
javac MazeProgram.javaThis will automatically compile all dependent classes.
java MazeProgramThe program will:
- Load
maze.txtfrom the current directory - Open a 1000×800 window
- Start with the 3D view enabled
- Display arrow images from
arrow.png,arrowUp.png,arrowDown.png,arrowBack.png
This project demonstrates several important computer science concepts:
- Polygon rendering with
java.awt.Polygon - Double buffering (implicit in Swing)
- Event-driven GUI programming
- Game loop architecture
- Input handling
- State management (game states, flags)
- Collision detection
- 2D array representation of grid-based worlds
- Coordinate transformations
- Conditional rendering based on view direction
- Perspective projection (vanishing point geometry)
- Grid-to-pixel coordinate mapping
- Rotation matrices (simplified to 90° increments)
- Depth cueing (atmospheric perspective)
- Painter's algorithm (drawing back-to-front)
- Portal/BSP rendering concepts
This project is a modern educational implementation of techniques used in early 3D games:
- Wolfenstein 3D (1992): Raycasting for pseudo-3D
- Doom (1993): BSP trees and portal rendering
- Duke Nukem 3D (1996): Build engine portal-based rendering
While modern games use true 3D graphics with GPUs, understanding these foundational techniques helps programmers appreciate:
- How to create compelling visuals with limited resources
- The difference between 2.5D and true 3D
- The evolution of graphics programming
Ideas for enhancing this project:
- Textured walls: Use images instead of solid colors
- Mini-map toggle: Hide/show the overhead view
- Multiple maze levels: Load different maze files
- Enemy AI: Add moving obstacles
- Collectibles: Items to gather before exit
- Diagonal movement: Allow 45° angles
- Variable tile sizes: Support different perspectives
- Lighting effects: Flashlight cone, darkness
- Sound effects: Footsteps, door sounds, ambient audio
- Level editor: GUI tool to create maze.txt files
This project represents a significant achievement in understanding how classic 3D games worked before modern GPU acceleration. The hand-coded polygon definitions in Maze.java show careful geometric planning to create convincing perspective.
The code demonstrates:
- Strong object-oriented design (separation of concerns)
- Practical problem-solving (collision detection, state management)
- Mathematical thinking (coordinate systems, perspective)
- Persistence (manually defining 52 polygon coordinates!)
While the code could be refactored for cleaner architecture (the switch statements in drawSurrounding() are quite repetitive), it works effectively and shows clear logic flow.
Educational project - feel free to learn from and modify.
Created as a high school senior project (2019-2020) exploring data structures and algorithms.