Back in February, I started working on a raycaster. It was really fun, and I put it on GutHub. Well I've actually been working on this engine behind the scenes. It's hard to show in the little thumbnail, but I added jumping. I know that sounds like a stupid thing to add to a raycaster, but trust me, it's more helpful than you probably realize.
Fixing What I Had
Before I could add more features to my raycaster, I had to fix it. You see, there was a bit of fish-eye, but not the normal kind. In a raycaster there are 2 types of fish-eye, one is related to cosine, and one to tangent.
If you know how raycasting works, you would expect that if this blue circle (the camera) is at this position shooting the red rays, then the white wall should be projected perfectly flat. However, this wouldn't be the case, because the rays in the middle are shorter than the rays on the sides. This creates a warping effect, a fish-eye. This is the cosine fish-eye, but there's a second one. Now imagine each ray is shot out at regular intervals, say 1 ray per degree. This is how the tangent fish-eye is created. If we shoot rays out like this onto a flat wall, and then take a look at the distribution we get, we will see we get a lot more collisions in the center of vision, and a lot fewer around the edges. This creates a subtle, but noticeable effect. To fix it, you just have to make sure the rays are cast out and regular intervals screen-wise, not degree-wise.
Floors and Ceilings
An obvious step between Wolfenstein3D and DOOM is the fact that the floors and ceilings have textures. This has been a pain point for me for the whole process. Figuring out how to get it to work was a difficult, but making sure it was performant was extremely difficult.
There's a really cool optimization you can do in this context, and it's because the ceiling and floor are at the same height (relative to the camera, just opposite). For every pixel on screen, you can cast a ray out to the ceiling and floor and get the angle and distance of that ray. This can all be precalculated, saving a lot of time every frame. After this, we get the x and y position that the ray hits, and use that as an offset into a texture to draw that pixel. There's a few more optimizations I've figured out over my time working on the project. Firstly, you might think that you should give less pixels to things further away, but that looks terrible. The truth is, especially at higher resolutions, things that are close have too many pixels, especially with my low resolution textures. Because of this, I can draw less accurately (e.g. bigger pixels) for the floor and ceiling that is closer to the camera. The other optimization is not drawing floors and ceiling behind walls, because you can't see them anyway. To do this in a really efficient way you can draw the floor in columns, bottom up. Once the floor hits the drawn wall, then you move onto the next column.
Tangent Warping on Sprites
For some reason it took me a while to figure out how to draw sprites correctly. It took me a long time to realize I was having the tangent warping issue again. Not really much to say here, other than draw your sprites in the right position and at the right size.
Sprites Behind Walls
One thing that can be a point of a headache is what to do when a sprite is slightly behind a wall. I had to split the drawing of sprites into columns and use a depth buffer to check which columns I should draw. One thing to keep in mind is that issue can occur when a sprite is in a wall, so you know, don't let that happen.
It adds a good effect when enemies aren't, you know, just appearing into existence.
Diagonal Walls
One of the most prominent differences between Wolfenstein3D and DOOM is that lines can be at any angle. This allows for really complex level design, a step above the restricted Wolfenstein levels.
To add this into the engine, it was a massive rework. You can't use raycasting in the original sense anymore. What I do is take a line from the camera, all the way to the maximum view distance. This line is overlaid with every line in the level. It then checks them for a collision, and uses the closest collision point. Sounds slow? Well that foreshadowing for later, but not yet (if only we could partition space in a binary fashion). I used diagonal lines rarely (mostly because I was nervous of whether they'd work well). This was also a good time to add another DOOM special, wall sliding! My implementation isn't too great, but it's a lot better than being stopped dead in your tracks.
Wall Heights
This added a lot of interesting features to my levels. Looks pretty cool, right?
It's not too hard to add different heights for walls... at first. There's a few gotchas that can, well, get you. The first one is what to do when a wall is too short? You can stretch the image, but that looks weird. What you really should do is crop the image to maintain pixel size. Well what if the wall is too tall? You have to draw another image to make up for that little bit extra, because once again, you can't just stretch the image. Now you've got a lot done, but you've also added jumping, and after peaking over a wall you see nothing. Nothing? That's not right, you should see another wall, but we only draw the first wall a ray hits. To fix this, we just have to draw every wall a ray hits, from back to front. You can also add an optimization to eliminate overdraw.
The Next Steps
I'm currently cooking the next feature in the oven, which is different ceiling heights. Different wall heights was cool, but it looks weird if they're not capped by a ceiling. The next step after this is stairs, which may sound hard, but since I've already implemented jumping, it's going to be fairly easy.
Why Even Make a DOOM Engine
In the path towards a fully 3D engine (e.g., a Quake engine), it may seem like a useless detour to work on the 2.5D DOOM engine. But if you can't solve it in 2D, you can't solve it in 3D. Can you implement a BSP tree in 2D? If the answer is no, why do you think you can implement a 3D BSP tree? Things in 3D get complicated real fast, and honestly, DOOM engines are cool. Once I've got a solid engine working, I'll probably make a game with it, but definitely open source it at some point because the technology really should be accessible.