A 2.5D raycasting maze game in Java, written with no libraries and no game engine —
just java.awt and javax.swing. Built by three of us as our AP Computer Science A
final project in high school.
You spawn in a textured maze with a monster hunting you. Grab gold, watch the minimap, and stay ahead of it — it gets faster the longer you survive.
Needs a JDK. Tested on Temurin 17.
./run.shUse ./run.sh --clean to wipe bin/ and rebuild from scratch.
The game must run with its working directory set to APCS Final Project/. Every
asset path is relative, and the wall textures load inside static initializers, so
launching from anywhere else dies with ExceptionInInitializerError instead of a
useful "file not found". run.sh handles this for you.
| Input | Action |
|---|---|
W / S |
Move forward / back |
A / D |
Strafe left / right |
← / → |
Turn |
SPACE |
Pick up adjacent gold |
Click the button on the title screen to begin.
The renderer is a raycaster in the Wolfenstein 3D tradition — the world is a flat 15×15 grid of integers, but it's drawn as though it had height, which is why it's "2.5D" rather than true 3D.
Walls. For each of the 640 screen columns, Screen.update() casts a ray from the
camera and walks the grid with a DDA traversal until it hits a non-zero cell. The
perpendicular distance to that hit determines how tall the wall slice is drawn — near
walls are tall, far walls are short. The exact hit position picks the column of texture
to sample, and rays that hit a north/south face get their color halved
((color >> 1) & 8355711) so adjacent faces read as distinct without any real lighting.
Adapted from the tutorial at lodev.org.
Sprites. Gold and the monster are billboards. Each frame they're insertion-sorted
far-to-near, then transformed into camera space with an inverse-determinant matrix so
they scale and slide correctly with perspective. A per-column ZBuffer recorded during
the wall pass hides sprites behind walls. Black pixels are treated as transparent, which
is how the sprites get cut out from their rectangular source images.
Speed. The pixel array is aliased directly onto the BufferedImage's raster via
DataBufferInt, so writing an int writes a pixel with no per-pixel method call.
The monster hunts greedily: it scores the four cardinal moves by squared distance to the player and takes the best legal one, falling back through the alternatives when walls block it. Its speed increases every move tick, so a long run is always eventually lost. It animates through a three-frame walk cycle.
The minimap redraws the grid each frame but fogs everything more than three tiles away, so you can only see your immediate surroundings. Green dot is you, with an orange wedge for your field of view; red is the monster; yellow circles are gold.
| File | Role |
|---|---|
Driver.java |
Entry point |
Game.java |
JFrame, game loop, minimap, HUD, gold pickup, monster AI |
Screen.java |
The raycaster — walls, then sprites |
Camera.java |
Player position/direction/FOV, and the key + mouse listener |
Texture.java |
64×64 wall textures |
SpriteTexture.java |
Full-size sprite images |
Sprite.java |
Interface implemented by Gold and Monster |
Gold.java, Monster.java |
The two sprite types |
Maze.java |
Maze generator — see below |
Maze.java is a complete recursive-backtracker maze generator that nothing ever calls.
Game.generateMap() picks one of three hand-drawn maps out of assets/MapOptions.txt
instead. It got built and never wired in; it's kept here as-is.
APCS Final Project/ is the finished game and the only version that runs — it's the
only one with a main. The rest are snapshots kept from development, roughly in order:
Raycasting Test/ walls only, no game
test1/ first pass at gold
TreasureUpdate/ scoring
goldAndMonster/ the monster arrives
ALMOST_DONE/ title and end screens
LoadingAndClosing/ polish
FINAL_EDITION/ feature-complete, but has no main()
APCS Final Project/ ← the one that runs
The game originally sized its window with setSize(640, 480) and drew straight onto the
JFrame. That sizes the whole window, title bar included, while the drawing surface
starts at the window's top edge — so on macOS the title bar covered the top 28 rows of
everything drawn. Game.java now draws on a Canvas sized to exactly 640×480, which
sits below the title bar, and pack() grows the window around it. Every drawing
coordinate is unchanged, because a canvas reports mouse clicks in the same space it
draws in.
Beyond that the code is untouched, and the engine itself needed no changes at all — it compiled and ran correctly on a JDK seven major versions newer than the one it was written against.
