Somewhere around a decade ago I built a raycasting engine in Java for fun. No game engine, no OpenGL, no external libraries. Just Swing, a BufferedImage, and the DDA algorithm from Wolfenstein 3D. It had enemies with hand-drawn pixel art, animations, projectiles, A* pathfinding, a state machine, fog effects, primitive enemy AI and a Zelda-style heart system. I was proud of it at the time.

I found the source code recently and decided to review it the same way I’d review a colleague’s merge request. There’s something uncomfortable about reading your own old code. You can’t blame the author. Every shortcut, every misunderstanding, every “I’ll fix this later” that never got fixed is unambiguously yours. At the same time, it’s the clearest possible measure of how far you’ve come. The architecture is surprisingly competent in places. The bugs are spectacular in others.
The Setup Link to heading
22 Java files across 7 packages. No build system. No Maven, no Gradle, not even Ant. Just raw .java files in a src/ directory, run from whatever IDE I was using at the time. The level data is a PNG image painted in GIMP where pixel colours map to tile types: black for walls, grey for pillars, white for open space. The .xcf source file is still in the resources/ folder alongside hand-drawn enemy sprites with their Aseprite files.

The architecture is a classic game engine pattern: a fixed-timestep game loop in GamePanel targeting 60 FPS, a stack-based state machine (MenuState → PlayState → DeathState), and a Screen class that handles all the raycasting. Entities inherit from GameObject with AABB collision. Clean enough. If I saw this package structure in a merge request I’d nod approvingly.
The Raycaster Link to heading
The Screen class is where it gets interesting. Three rendering passes per frame:
- Floor and ceiling: for each row, calculate the distance from camera, sample the appropriate texel from the floor/ceiling texture. Apply fog.
- Walls: for each screen column, cast a ray using DDA. Step through the grid until hitting a wall tile. Calculate perpendicular distance (avoiding fisheye distortion), determine wall height, map texture coordinates, draw the vertical stripe. Store the distance in a z-buffer.
- Sprites: sort by distance (back-to-front), transform into camera space using the inverse camera matrix, project to screen, draw column by column. Only draw if closer than the z-buffer at that column.
All textures are 16×16 pixels. The camera uses a plane vector of -0.66, giving roughly 66° FOV. Standard Wolfenstein setup. The implementation is correct and produces a proper-looking result. I genuinely understood the maths here, which surprised me going back to it1.
The Good Link to heading
The separation of concerns holds up. Rendering knows nothing about game logic. State management is clean. The map-as-PNG approach is genuinely clever for a project of this scale: paint a level in GIMP, save as PNG, the engine reads it. No custom file format, no level editor to maintain.
The utility classes (ArrayUtils, RenderUtils, AStar) all have private constructors that throw IllegalStateException on instantiation. Defensive pattern against accidental instantiation of static utility classes. I was clearly reading Effective Java at the time2.
The sprite rendering uses a proper z-buffer for occlusion behind pillars, walls and other sprites. Most amateur raycasters skip this and just draw sprites on top of everything. Getting the inverse camera matrix transform right for the sprite projection takes some thought; younger me actually seems to have understood the linear algebra.

The Bad Link to heading
There’s a continue statement inside a for-each loop that iterates over the closed and open sets in the A* implementation. The intent is to skip nodes already explored. The problem: the continue applies to the inner loop (the for-each), not the outer loop. Nodes in the closed set are never actually skipped. The pathfinding still produces correct paths because it eventually explores everything, but it’s doing exponentially more work than necessary. Dijkstra wearing an A* costume.
Enemy.update() throws IllegalStateException instead of being abstract. The Enemy class should be abstract. It isn’t. If someone added a new enemy type and forgot to override update(), they’d get a runtime exception instead of a compile error.
DeathState and MenuState are nearly identical. Copy-pasted with different title strings. A single class with a configuration parameter would have sufficed.
Movement speed is per-frame, not per-time. If the game loop runs slower than 60 FPS, everything moves slower. No delta-time multiplication. Classic beginner mistake. It only “works” because the game is simple enough that the machine never drops frames.
The Ugly Link to heading
Here’s the line that made me audibly wince:
ImageIO.read(new File("resources/textures/hand.png"))
This is inside the render loop. It reads the hand texture from disk every single frame while the player is casting a spell. At 60 FPS, that’s 60 file system reads per second for a static 16×16 PNG. It should be loaded once and cached. I can only assume I added the spell effect late in development and never went back to fix the loading.
The fog calculation in renderSprites() creates a new Color object per pixel to extract RGB components. In the hot rendering path. Every pixel of every sprite allocates and immediately discards an object. The GC must have been working overtime. I could have just used bit shifts: (pixel >> 16) & 0xFF for red, (pixel >> 8) & 0xFF for green, pixel & 0xFF for blue. No allocation needed.
There’s a magic number 8355711 used for ceiling darkening. It’s 0x7F7F7F. A bitmask to right-shift each colour channel by 1 (halving brightness). No comment, no named constant. Just a raw integer that happens to work because of how RGB is packed into an int.
And my favourite: textures.get(textures.indexOf(texture)). This looks up a texture by finding its index, then retrieves it by that index. It returns the same object you started with. A no-op that somehow survived into the final version.
What I’d Change Now Link to heading
Aside from using an off-the-shelf engine or at least a proper graphics library, if I wanted to write my own raycaster again:
The rendering should use direct pixel array manipulation (int[] backing the BufferedImage via DataBufferInt) instead of setRGB()/getRGB() calls, which box values through Color. Younger me was close to this pattern already but didn’t fully commit.
The enemy base class should be abstract. The state machine could be an enum-based approach instead of a full stack (for a game this simple, you never actually need more than one state active). The A* should use a HashSet for the closed list instead of a List with linear search.
The architecture is pretty much sound, though. The same package structure, the same rendering pipeline, the same state machine pattern; I’d use all of these again in a similar project. The mistakes are implementation-level, not design-level. That’s reassuring. It means I understood the problem even when I was getting the details wrong.
The Verdict Link to heading
For a project built for fun with no dependencies, no engine, and no prior experience with raycasting, it works. The maths is right. The architecture is right. The details are wrong in entertaining ways. It’s exactly the kind of code I’d expect from someone who understood the theory, had read the right resources, and hadn’t yet learned the hard lessons about garbage collection, file I/O, and the difference between “it works” and “it works efficiently.”
I should probably fix the A* bug. It’s been broken for a decade but if I start fixing bugs and making improvements I’m not sure where I’d stop.
Lode’s Raycasting Tutorial — the definitive resource for this style of raycaster. Almost certainly what I followed at the time. ↩︎
Effective Java, 3rd Edition by Joshua Bloch. Item 4: “Enforce noninstantiability with a private constructor.” ↩︎