atifcodesalot
/
pyWolf3D
a wolfenstein 3D clone in python, pygame
pyWolf3D
demo3_squashed.mp4
Please excuse my mouse sounds in the demo π
What is pyWolf3D?
PYWOLF3D is a Wolfenstein 3D engine, game clone written from scratch in pygame, Python.
Why?
I am a huge Doom and Wolfenstein fan so I just decided to rewrite one of the originals myself π. It was mostly for fun and learning.
Code authorship
All code present in this project was written by me, without AI code generation or coding agents. AI tools were used for discussion, research, and help locating existing game assets and resources. The implementation, including map parsing, rendering, actor behavior, and game logic is entirely my own work. But don't take my word for it: https://isvibecoded.com/.
Some screenshots
How to play?
Episode 1 shareware version
To play the shareware version of episode 1, simply clone this repository:
git clone https://github.com/atifcodesalot/pyWolf3D
change the directory to the root of the project:
cd pyWolf3D
β¦
Want to play yourself?
The source is at https://github.com/atifcodesalot/pyWolf3D. I haven't made a release yet, so you would have to clone the repo and run the package. Enjoy!
How did the project start and why?
It was a rainy afternoon; I was mentally reviewing one of my failed Raycaster attempts. Suddenly I decided to make one that actually works, and since I enjoyed remaking stuff, why not remake the game? It basically started it all. The answer was simply, why not? I would learn a lot and do what I love most!
How it went?
Not particularly bad. I was determined not to use AI, not even for debugging, because I wanted to recreate it myself. The source was already made public more than a decade ago, so what would be the point of using AI?
Rendering
So I started building a raycaster for wall rendering, and it wasn't going bad. The maps are 64x64 grids in Wolfenstein3D, and the DDA algorithm is used for generating collisions. If you have played the originals, you knew that there are also doors and pushwalls (secrets) too, though. They are edge cases that are handled explicitly! I also implemented sprite rendering for the first time for a raycaster, and again it wasn't that bad. The sprites are billboarding, so they only require to be projected onto a virtual viewport to find their screen coordinate, then they are rendered as plain images! Though you do need to draw the image column by column to emulate partial occlusion. Believe it or not, this is the core rendering of Wolfenstein 3D; everything that moves is a sprite, and everything that doesn't is a wall π. There are additionally projectiles such as rockets and fireballs, but they are also sprites that just travel in a direction. But how can, for example, a vase or a lamp that seems very hollow be 64x64 sprites? The answer is transparency. You see, a lamp in Wolfenstein 3D is just a sparse image that is mostly transparent pixels. A guard and a lamp actually have the same resolution! This actually isn't only used to signal hollowness but also to emulate the illusion of size; for example, a guard has more transparent pixels than the boss Hans GrΓΆsse, therefore making Hans appear larger. Another complexity is orientation for some actors: a guard sprite has 8 orientations; for example, that means when looked at from different angles, it yields different sprites. To choose the right sprite, the player must be transformed into the actor's space. That means actors with orientation have their own basis vectors.
Texturing
Walls are textured very simply. After a collision has been generated, the texture column offset from the beginning of the wall is found via coll % wall_width, therefore effectively one column in the screen is assumed to be perspectively invariant.
Lighting
There is no actual lighting; pre-baked textures are used for emulating lighting. The raycaster determines if a wall has been hit vertically or horizontally and renders the respective pre-shaded texture.
Game Logic
Game logic is straightforward, except for actor behavior. Actors are programmed via state chains and themselves are state machines. They contain one state at a time, such as ChaseState or DyingState or PatrolState. States transition and therefore are chained together to create simple AI. One thing to note is that actors use a stateless, greedy search to find the player when alerted. There is no pathfinding. For example, a guard that is standing has a GuardingState whose next state is itself. If the player alerts it, then it transitions to its ChaseState, which also points to itself.
Map Layout
The map layout is extracted from GAMEMAPS.WL6 or MAPTEMP.WL1 (one episode shareware version).`use RLEW and Carmack compression](https://moddingwiki.shikadi.net/wiki/Carmack_compression), whereasMAPTEMP.WL1` only RLEW. When decompressed, they contain data for a map in two byte planes: one for wall layout and area information (for sound propagation) and one for sprite and actor placement. Difficulty is determined by how many actors are there in a map. An actor entry in data plane 1 also contains information about what difficulty that actor belongs to.
Sound
The sound effects are played via WAV files obtained from the internet. I also wrote a simple sound manager for panning, directional audio, and attenuation. The music is actually synthesized from the original game files! The original had specific files for music data, but they are not sample data for your modern PC's speaker to play! They are instructions for driving the Yamaha OPL chip for it to synthesize music in real time. This is where PyOPL came to my rescue. It emulates the chip for me so I can convert the tracks to WAV files using the original audio files.
Sound propagation
The sounds made by the player by shooting are propagated into the map via the preprocessed area codes defined in the data plane 0 in the GAMEMAPS.WL6 file. Doors connect areas upon fully opening, and once two areas are connected. If the player shoots in area X, and area Y is connected to X, then an actor at Y gets alerted.
Optimizations I made later on
Initially, transparency was handled with full transparent pixels: pixels with alpha channels of 0. But this was a huge mistake, as
pygameit tries to do blending math per transparent pixel, which created a huge performance sink. The transparency in the sprites was binary, either fully opaque or transparent. I then realized this and switched to color keys, which tells pygame to skip the pixel entirely while blitting if it is of a certain color, instead of doing unnecessary blending math.Each time the player moves, the rays it casts need to be rotated.
I had been doing this naively by multiplying each ray by a rotation matrix individually. Then I started using numpy: I multiply every ray with one dot call with the rotation matrix by arranging the ray directions in a single numpy array. After the multiplication is done, I convert it back to an list again, because individual access is too costly for a numpy array.
These are just some of the things I should do. There is still lots of room for optimization, and I look forward to implementing it. π
The result?
Well... Wolfenstein 3D. You can also play with different FOVs and different rays (for raycasting). I plan on doing other stuff with this project, but we'll see. It was a very rewarding one. Enjoy!
Top comments (1)
Might implement multiplayer soon...