為甚麼及如何在不使用 AI 的情況下以 Python 從零重製 Wolfenstein 3D
why and how did I rebuild wolfenstein 3D from scratch via python with no AI?
作者以 Python 從零重製 Wolfenstein 3D,並刻意不使用 AI,包括除錯,以親自完成重製。遊戲以 raycaster 渲染牆壁和精靈圖,地圖資料取自原版遊戲檔案,音樂則透過 PyOPL 模擬 Yamaha OPL 晶片合成。項目尚未發布,玩家需複製程式碼庫並執行套件。
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!
來源:DEV Community · dev.to