“In the information age, the barriers just aren’t there. The barriers are self-imposed. If you want to set off and go develop some grand new thing…you need enough pizza and Diet Coke to stick in your refrigerator, a cheap PC to work on, and enough dedication to go through with it.” - John Carmack

This idea started about three years ago, when two things coincided: I had started poking around in the source code for the browser version of Wolfenstein, and I’d heard about how Legend of Zelda: Breath of the Wild, a 3D game for Nintendo Switch, had been prototyped as a 2D game. I thought it would be fun to do the opposite with Wolfenstein 3D, id Software’s groundbreaking first-person shooter from 1992, and turn it into a top-down Pacman-style maze game. I didn’t know it at the time, but Wolfenstein itself was based on an earlier 2D maze game, Castle Wolfenstein (1981). The idea sat in my head and on various to-do lists for years. It took me a while to get back to it.

Getting back to it

As with most of the projects on my site, this one got started and finished during COVID-19 lockdown. You’ve got to find whatever positives you can in a pandemic, and putting time into projects I usually wouldn’t think I had time for was a great way to do it. I didn’t really expect to finish it, but surprised myself by getting it done in about six days. Only one of those was a really long day. I first sat down and dug through the browser version source to see if I could figure out how it worked. My recent reading of Fabien Sanglard’s Game Engine Black Book: Wolfenstein 3D was fresh in my mind, so I at least knew what raycasting was, why it was there, and how it worked, plus a few other details of what makes the engine tick. I soon started to think the conversion to 2D would be easier than I thought, given that most of the engine would work as normal, and I would just have to replace the part that updates the screen, throwing out most of the clever groundbreaking stuff that makes it look 3D. I couldn’t quite tell yet if I was right, or if I just didn’t know enough yet to be able to know how complicated it was.

Required knowledge

Some of the following I learned from reading the Game Engine Black Book, and some of it I found in the source code (some of it is specific to the browser version):

  • the level maps are 64x64 tiles, and each tile contains a wall, a door or nothing
  • there are two systems for defining the location of an object on the map: tile (ie x and y coordinates, integers between 0 and 64), or pos, which has a lot more precision
  • a pos can be converted to a tile by shifting it 16 bits to the left, and vice versa
  • sprites have a pos and can be decorative (tables, ceiling lamps, human remains), an enemy, or powerups that can be collected (ammo, health, treasure)
  • the player and the enemies are able to face in a direction, and angles are recorded in degrees, shifted 7 bits to the right.
  • non-directional sprites look the same from any angle.
  • all the wall textures are stored tiled vertically together in one file, probably so the browser only has to fetch one file (smart!)
  • sprites are tiled into one file for all the powerups/decorations, and each enemy has their own sprite file, tiled horizontally
  • enemy sprite files contain images of the enemy facing in all eight compass directions
  • the screen is divided into vertical slices, and each one contains a vertical slice of a wall texture. The height of the slice determines how far away that slice of the wall appears. Read Sanglard’s book for more details about how raycasting works..

How Wolf3D works in the browser

This was a rewarding project because, having never really seriously tried to make a game before, it gave me a great view under the hood of a real game that actual game developers made. I learned a lot. In Wolf3D (and possibly all games, I guess I’ll gradually find out) there are two loops going on. One is for the game model, updating the state of the game and all the objects within it. In game.js at line 192 is the nextCycle() function, which is scheduled 30 times per second and does the following:

  • update the player (including whether they’re moving/shooting/etc based on keyboard input (which is read by event handlers))
  • update the position/activity of actors (ie enemies)
  • update doors and pushwalls (are they open, should they move)
  • update the HUD (score, lives, which weapon we’re holding, what BJ’s face is doing (which relies on tics))

The second loop that is constantly running is the renderer, which updates the display. At line 564 is startRenderCycle(), which makes a call to updateScreen(), at line 474, which in turn calls Renderer.draw() which can be found in renderer.js at line 186:

function draw(viewport, level, tracers, visibleTiles) {
    var n, tracePoint;
    
    for (var n=0,len=tracers.length;n<len;++n) {
        tracePoint = tracers[n];
        if (!tracePoint.oob) {
            if (tracePoint.flags & Wolf.TRACE_HIT_DOOR) {
                drawDoor(n, viewport, tracePoint, level);
            } else {
                drawWall(n, viewport, tracePoint, level);
            }
        }
    }
    drawSprites(viewport, level, visibleTiles);
  }

That’s the one I needed to override and mangle into 2D. It uses tracers, which are generated by the raycaster, to determine the height of each vertical strip of wall, but this isn’t necessary in 2D: we can just draw all the walls the same size. I threw out most of the draw() function, replacing it with this:

function draw(viewport, level) {
    drawWalls(viewport, level); // also draws doors
    drawSprites(viewport, level);
    drawWeapon(viewport);
}

Drawing the weapon is something I added so we can see which direction BJ is facing. We’ll come back to that.

After a failed attempt at drawing the entire map every frame whether it fits on the screen or not (my CPU fan going crazy was the first sign that this was a bad idea), I realised all we needed was as many 64x64px divs as will fit on screen, and to update them with the right textures depending on where the player currently is. BJ will always be drawn in the center of the screen, with the map in his vicinity drawn around him, and therefore we can use his POS to calculate the on-screen position of the other walls and sprites given their POS or TILE. I had some messy and guessy ways of doing this until I wrote the following function, which gives us distance from the top and left of the screen in pixels for a given x and y POS, and a given viewport (BJ’s position):

function topLeftForPos(x, y, viewport){
    var bjTop = Wolf.YRES / 2 + 32,
        bjLeft = Wolf.XRES / 2 - 32,
        dx = x - viewport.x,
        dy = y - viewport.y;
    return {top: Wolf.YRES - (bjTop + (64 * dy/TILEGLOBAL)), left: bjLeft + (64 * dx/TILEGLOBAL)};
}

What we needed next was the background image for each tile. I use the same method as the 3D version. Each tile in the DOM is actually a div within another div, both 64x64px, and the inner one has the huge background image containing all the wall textures. The texture for each tile is found with level.wallTexX[levelX][levelY], giving us an integer, which we use to offset the position of the inner div by increments of 64 pixels so the correct section of the background image is visible through the outer div. Utilising a few lines from the 3D version’s drawWall() function:

var itop = -(texture - 1) * 64;
img.css({
    backgroundImage: "url(" + textureSrc + ")",
    top: itop + "px"
});

For example, if we’re at position (15,20) in the level, and wallTexX[15][20] returns 15, the inner div will be rendered as top: "-896px". It subtracts 1 from the texture number, so that if there is no texture (ie. wallTexX[x][y] = 0) it will give a negative top value which will move the background image down 64px and out of view so that there is no texture showing. There is also a wallTexY but…I didn’t use it. I remember reading that there is a light and dark version of each texture, to make vertical and horizontal walls (as they appear on the map) appear different, but as I’m not using the raycaster anymore (I’m pretty sure) I couldn’t detect if each wall was vertical or horizontal, so I just used the X version all the time. There’s probably a way to do it but I had bigger fish to fry at the time. Apologies to (probably) Adrian Carmack, who would have put a lot of work into making two versions of each texture.

The exception to all of this is that if it’s a door, we just show the door texture instead. level.state.doorMap is another 2d array that we can check to see if the current tile is a door. Doors can be in one of four states: closed, opening, closing, or open. If it’s open, we draw nothing. If it’s closed, we draw the door. If it’s opening or closing, we draw the door opening or closing, ie drawing the texture further to the left depending on how open it is. Luckily, it’s tics to the rescue again, and door.tics gives us a number from 0 to 63 of how open the door currently is. Hey, wow, that coincides with the number of pixels we need to move it by!

img.css({
    left: -(door.ticcount) + "px"
});

That was a freebie. The other special case is push walls: the ones that reveal secret areas when you move them. I had to change their behaviour a bit: the 3D version changes the texture of the tile behind when the front tile starts moving, which looks weird from a bird’s eye view, like the wall just cloned itself. So I changed the order of things in pushwall.js, and then in renderer we use the pushwall’s pointsMoved to move the tile in the right direction.

var pwall = Wolf.PushWall.get(),
    offset = pwall.pointsMoved / 128;
img.parent().css({
    marginLeft: pwall.dx * 64 * offset + "px",
    marginTop: -pwall.dy * 64 * offset + "px"
});

Note: in quite a few places I corrected for things moving in the wrong direction vertically by adding a negative sign and/or subtracting from the height of the screen so things would be the right distance from the top. Instead of moving things from the bottom instead. I’ll know for next time.

Sprites

I handled sprites differently than the map tiles. Map tiles keep recycling the same set of divs over and over, but as sprites could be located absolutely anywhere and there’s less of them, I just instantiated divs whenever I needed to draw a new sprite, and chucked them out when they moved off screen. The level state has an array of sprites, from which we can get their tile or pos, and their current texture. My drawSprites function iterates through level.sprites, only using the ones that are close enough to BJ to be visible, and again we can use topLeftForPos() to get the right on-screen position for each one. There is a similar system to the wall textures with an inner and outer div, and offsetting the inner div to allow the right texture to be visible: left = -texture.idx * size. Because I’d done most of this before, the sprites were seeming to be an easy problem to solve. But that’s before I realised the guards were pretty much always facing the wrong way, and spinning around on the spot when I changed direction. That’s because in the 3D version sprites are drawn relative to the location of BJ, so they appear to be looking at you when they’re facing you. I found some references in actors.js to “8_DIR”, ie the 8 directions a guard can be drawn facing. Defined up the top of actors.js was this:

var add8dir = [4, 5, 6, 7, 0, 1, 2, 3, 0],
    r_add8dir = [4, 7, 6, 5, 0, 1, 2, 3, 0];

Further down, where it determines the current texture for the actor, was this nightmare of a one-liner:

tex += add8dir[Wolf.Math.get8dir( Wolf.Angle.distCW(Wolf.FINE2RAD(player.angle), Wolf.FINE2RAD(guard.angle)))];

So it’s getting the clockwise distance between the angle the player is facing and the angle the guard is facing, converting that to one of the 8 enummed directions in math.js, then using that as an index to add8dir, which returns an offset so we access the right texture for the angle. It’s very clever, but I didn’t need it. I needed everyone in the level to be drawn in relation to the camera. After some head-scratching, drawing diagrams, and standing in the middle of the room and facing various directions while pointing other directions, I went with this:

tex += add8dir[Wolf.Math.get8dir(Math.PI - (Wolf.Angle.normalize(Wolf.FINE2RAD(guard.angle) + Math.PI / 2)))];

Pi minus the guard angle plus half of pi? That did it. I had some confusion over whether 0º means east or north, and whether I’d been drawing the level rotated 90º clockwise the whole time, but it works. The guards face the right way, including the ones who haven’t seen you yet. Speaking of angles…

Hang on, which way are we facing?

At some point, this thing I was building was starting to look pretty good, but it was still impossible to play through because you could never tell which way you were facing. I rectified that by drawing the weapon next to BJ’s head, facing in the direction he was facing, the bonus being that it would fire (or stab) too. Basically, just draw the weapon from the HUD (which uses a texture-offset system similar to the walls and sprites), but smaller, in the middle of the screen, positioned and rotated relative to viewport.angle. And for that, kids, we need a real-world application of my favourite high school maths acronym: SOHCAHTOA. That’s right, it’s triangles all the way. Here’s how it works:

function drawWeapon(viewport){
    var bjCenterX = Wolf.XRES / 2,
        bjCenterY = Wolf.YRES / 2,
        weapon = $("#game .renderer .player-weapon"),
        angle = Wolf.FINE2DEG(viewport.angle),
        radius = 32,
        h = radius * Wolf.Math.SinTable[viewport.angle],
        w = radius * Wolf.Math.CosTable[viewport.angle];
    weapon.css({
        top: bjCenterY - h + "px",
        left: bjCenterX + w + "px",
        transform: "rotate(" + (90 - angle) + "deg)"
    });

}

A little bit of coding style I picked up from the 3D version was only typing ‘var’ once when declaring a whole lot of variables. And also declaring all the variables at the top of the function. The latter screams “I’m used to writing C” but it does make things easier to find if they’re all declared in one place. I like it. Anyhow, with the player direction working, I then gave it a serious playthrough, fixed a few bugs (sprites kept hanging around on screen after you die or finish a level), and gradually realised that it did everything I wanted to do. I was having fun. I was just playing Wolf2D and having a great time. I remembered that’s why I work on these projects: for the sense of achievement when you get something working that you weren’t sure you’d ever be able to finish. But the cool thing about working on a game is you get to play it. And I got there without any Diet Coke, John Carmack. But I did make three pizzas that week, perhaps that helped.

Further reading

Game Engine Black Book: Wolfenstein 3D by Fabien Sanglard

Let’s Compile Like It’s 1992, blog post by Fabien Sanglard

Masters of Doom by David Kushner

The live demo is here.

The code is here.