Dissecting the Code: Inside Wolfenstein 3D
A beginner-friendly walkthrough of the original id Software source code, file by file.
WL_DEF.H — The Blueprint
Every building needs a blueprint before anyone picks up a hammer. In Wolfenstein 3D, that blueprint is WL_DEF.H. It is a header file — meaning it doesn't do anything by itself, but every other .C file in the game reads it first. It defines all the rules, structures, and named constants that the rest of the code lives by.
The Hard Limits
The first thing you notice in WL_DEF.H is a set of four hard limits. MAXACTORS is set to 150, MAXSTATS to 400, MAXDOORS to 64, and MAXWALLTILES to 64. These are the game's absolute ceilings. A level can never have more than 150 moving enemies, 400 decorations, 64 doors, or 64 wall types — not because of design choice, but because the game pre-allocates exactly that much memory before the level loads. Arrays in C have fixed sizes, and on a machine with 640KB of RAM in 1992, every byte was precious.
The Coordinate System
One of the cleverest tricks in the entire codebase is how it handles positions. GLOBAL1 is defined as 1 shifted left 16 bits, which equals 65,536 internal units per tile. TILESHIFT is 16, and UNSIGNEDSHIFT is 8. The game uses "fixed-point" arithmetic — a way of doing decimal math using only integers. One tile in the game world equals 65,536 internal units. Why? Because on a 486 processor, integer math was far faster than floating-point math. By scaling everything up by 65,536, the programmers could represent fractional positions (like "0.73 tiles from the left") as whole numbers (47,972), and do the math without ever touching the slow floating-point unit.
The Player's State: gametype
The gametype structure holds everything that would be saved to disk if you pressed Save Game. It contains: difficulty, mapon, oldscore, score, nextextra, lives, health, ammo, keys, bestweapon, weapon, chosenweapon, faceframe, attackframe, attackcount, weaponframe, episode, secretcount, treasurecount, killcount, TimeCount, and victoryflag. That is the player's entire existence in one package — health, ammo, score, lives, weapon, level, and an internal timer. Notice chosenweapon versus weapon: you can choose the chaingun, but if you run out of ammo, weapon temporarily drops to the knife while chosenweapon remembers what you really want.
The Enemy Roster
Every enemy type in the game is listed in an enum called enemy_t. In order: en_guard, en_officer, en_ss, en_dog, en_boss, en_schabbs, en_fake, en_hitler, en_mutant, and several others. en_guard is 0, en_officer is 1, and so on. When code elsewhere needs to ask "is this a dog?", it checks whether the object's class equals dogobj. Human-readable names, but the computer just sees a number.
The Actor Structure: objtype
The most important structure in the game is objtype. Every enemy, the player, and even projectiles are objtype objects. It contains: active, ticcount, obclass, state, flags, distance, dir, x, y, tilex, tiley, areanumber, viewx, viewheight, transx, transy, angle, hitpoints, speed, temp1, temp2, temp3, and pointers to the next and previous actor in the list. Those next and prev pointers are what make this a linked list — each actor knows who the next actor is, forming a chain. Every game frame, the engine walks this chain and updates every actor in turn. transx and transy are the actor's transformed coordinates — where they appear after the 3D perspective math has been applied, used for deciding where to draw them on screen.
WL_MAIN.C — Where It All Begins
WL_MAIN.C is the first file the operating system hands control to. It contains main() — the C function that every program starts from. From here, the entire game bootstraps itself into existence.
The Beta Expiry Check
The very first thing the game does in a beta build is check today's date against a hardcoded expiry. YEAR, MONTH, and DAY are defined in WL_DEF.H as 1992, 9, and 30. If a tester tried to run the game after September 30th 1992, it would print "Sorry, BETA-TESTING is over. Thanks for your help." and exit immediately. The surrounding #ifdef BETA means this code only compiles into the beta version — in the final release, this block is completely absent.
Reading the Config File
The ReadConfig function opens CONFIG.WL6 from disk. There's no JSON, no XML — just raw binary data read directly from a file into memory. One read() call loads the high scores. The next loads the sound mode. Then mouse enabled status, joystick status, key bindings, and view size — all plucked out in sequence. If no config file exists at all, the game auto-detects available hardware (Sound Blaster, AdLib, joystick) and sets sensible defaults.
The Demo Loop
The DemoLoop function is the game's idle state — what it does when nobody is playing. It runs in a continuous outer loop. Inside, it shows the title screen for 15 seconds, the credits screen for 10 seconds, the high score table for 10 seconds, and then plays one of four pre-recorded demos. The IN_UserInput call at each stage returns true immediately if you press a key, which is why pressing any key during the title screen snaps you straight to the menu. After the inner loop breaks, the game shows the control panel. If startgame or loadedgame becomes true, it calls GameLoop() to run the actual game.
The Entry Point
The entire main() function is just five lines. CheckForEpisodes() looks at which data files are on disk to determine what episodes are unlocked. Patch386() detects whether you have a 386 or 486 processor and enables optimizations. InitGame() sets up every subsystem — video, sound, memory, input. DemoLoop() runs forever. The Quit() at the very end exists only as a safety net — it should never actually be reached, which is why the message reads "Demo loop exited???" with three question marks.
WL_PLAY.C — The Game Loop
If WL_MAIN.C is where the game starts, WL_PLAY.C is where it runs. The star of this file is PlayLoop() — the central heartbeat that fires every single frame while you are in a level.
Reading Your Input: PollControls()
Every frame, PollControls() wipes controlx and controly back to zero, then asks each input device to add their contribution. First it copies the current buttonstate into buttonheld so the game remembers what was pressed last frame, then clears buttonstate for the new frame. Then it calls PollKeyboardButtons(), and if a mouse or joystick is enabled, PollMouseMove() and PollJoystickMove() as well. Keyboard, mouse, and joystick all feed into the same two variables. The final step clamps both values to 100 times tics — this makes movement frame-rate independent so the game runs at the same real-world speed regardless of CPU speed.
Mouse Input
PollMouseMove() calls a DOS interrupt to get how far the mouse has moved since last time. The horizontal movement comes back in the CPU register CX, and vertical movement in DX. These are then divided by (13 minus mouseadjustment) and added to controlx and controly. Putting the sensitivity setting in the denominator means a higher sensitivity value produces a smaller divisor, giving more movement per pixel of mouse travel.
The Music Playlist
The songs array in WL_PLAY.C is literally a flat list of music track constants, one per level, in order. GETTHEM_MUS plays on level 1 of episode 1, SEARCHN_MUS on level 2, and so on. Level 9 of each episode plays WARMARCH_MUS (the boss music) and level 10 plays the secret level music. To find which song a level uses, the game just looks up the index in this array. There is no complex lookup table — it is a glorified playlist.
The Main Loop: PlayLoop()
PlayLoop() is the entire game in miniature. Every iteration of its do-while loop is one frame. First it reads controls with PollControls(). Then it resets madenoise to false, moves any sliding doors with MoveDoors(), moves any pushable walls with MovePWalls(). Then it walks every actor in the linked list — player first, then every enemy — calling DoActor() on each, which runs their AI or player logic. Then it calls ThreeDRefresh() to redraw the 3D view, polls for sound events, and checks for special key presses like Escape. The loop repeats until playstate is no longer zero — death, level completion, or loading a game all set it to a non-zero value that breaks out.
WL_DRAW.C — The 3D Engine
This is the engine that creates the illusion of 3D. Wolfenstein 3D uses a technique called raycasting — for each vertical column of pixels on screen, it fires an invisible ray into the map and calculates how far away the nearest wall is. Closer walls get drawn tall; distant walls get drawn short. WL_DRAW.C contains the math and orchestration for all of this.
Fixed-Point Multiplication: FixedByFrac()
FixedByFrac() multiplies two fixed-point numbers together — and it is written entirely in inline assembly language, bypassing C entirely for maximum speed. On 1992 hardware this function was called thousands of times per frame, so every nanosecond counted. The asm keyword drops straight into Intel x86 assembly, operating directly on CPU registers. The return value is left in the registers rather than going through the normal C return path — a pragma above it suppresses the compiler warning about the technically missing return statement.
Projecting an Enemy onto the Screen: TransformActor()
TransformActor() converts an enemy's position in the game world into an X position and a height on screen. First, the enemy's world coordinates are made relative to the player by subtracting viewx and viewy. Then the coordinate system is rotated to align with the player's viewing direction using sine and cosine multiplications via FixedByFrac(). The result nx is the enemy's depth — how far away it is. ny is how far left or right it is from center. The screen X position is centerx plus ny times scale divided by nx, a classic perspective divide. The height is a constant divided by nx, computed with inline assembly for speed, and stored in viewheight.
Setting Up the Ray Cast: WallRefresh()
WallRefresh() sets up the virtual camera before the raycasting begins. It reads the player's current angle, looks up the sine and cosine from pre-calculated tables (sintable and costable), and computes viewx and viewy — the focal point slightly behind the player that gives the correct field of view. Using lookup tables instead of calling sin() and cos() from the math library is dramatically faster. Then it calls AsmRefresh(), a hand-optimised assembly routine in WL_DR_A.ASM that does the actual ray marching across every column of the screen.
The Master Refresh Function: ThreeDRefresh()
Everything visible in the game flows through ThreeDRefresh(). It clears the screen with VGAClearScreen(), draws the walls with WallRefresh(), draws all scaled sprites (enemies and items) with DrawScaleds(), and draws the player's weapon with DrawPlayerWeapon(). Then it writes directly to the VGA card's CRTC registers using inline assembly to switch which page of video memory is being displayed — a technique called page flipping. The game keeps three screen pages in video memory and cycles through them, always drawing the next frame in a page that is currently invisible, which eliminates flicker entirely.
WL_AGENT.C — The Player Character
WL_AGENT.C is everything you are in Wolfenstein 3D. Movement, combat, pickups, and the status bar all live here.
Moving Through the World: Thrust() and ClipMove()
Thrust() takes an angle and a speed, converts them into X and Y movement values using FixedByFrac() with the cosine and sine tables, and passes them to ClipMove(). The negative applied to ymove is because the Y axis is inverted in this coordinate system — south is positive Y, but northward sine values are positive. After moving, the player's tile coordinates are updated by right-shifting the precise world position by TILESHIFT (16 bits), which divides by 65,536 and converts the fine-grained position into a tile grid coordinate.
ClipMove() implements wall sliding. It tries the full requested move first — both X and Y together. If TryMove() returns false (something is in the way), it tries moving only along X with Y held at the original position. If that fails, it tries only along Y. If that fails too, the player stays put. This three-stage fallback is why you glide smoothly along walls rather than stopping dead when you brush against them.
Firing Your Weapon: GunAttack()
When you fire, GunAttack() loops through every actor in the linked list looking for the closest one that is flagged as shootable, is currently visible, and whose screen X position is within shootdelta pixels of the center of the screen. Once it finds the closest candidate, it calls CheckLine() to verify no wall is sitting between you and the target. Then it calculates distance using the tile coordinates and rolls a random damage number with US_RndT(). If the enemy is very close (distance under 2 tiles), damage is US_RndT() divided by 4 — high potential damage. At medium range it divides by 6. At long range there is an additional chance to miss entirely based on how far away the target is.
Picking Up Items: GetBonus()
GetBonus() is called whenever the player walks over a collectible. It uses a switch statement on the item's itemnumber. The first aid kit returns immediately if you are already at 100 health — it simply doesn't work. The chaingun pickup draws a special excited face sprite on the status bar and sets a gotgatgun flag. The gibs on the floor give you 1 point of health but only if you are below 10 HP — otherwise walking over them does nothing. Every case ends by setting the item's shapenum to -1, which is the universal signal to stop drawing that object on the map.
BJ's Face: DrawFace()
DrawFace() picks which face sprite to show on the status bar using a single formula. It takes 100 minus your current health, divides by 16, and multiplies by 3 to get a base sprite index. That selects one of eight health bands — the lower your health, the higher the index, and the bloodier the face. Then it adds faceframe (0, 1, or 2) to pick one of three animation frames within that band. At zero health it skips the formula entirely and draws FACE8APIC, the death face. UpdateFace() calls DrawFace() at random intervals based on elapsed time, making BJ's eyes appear to dart around nervously.
WL_STATE.C — The Enemy Brain
While WL_AGENT.C governs the player, WL_STATE.C governs everything else that moves. This is the AI engine — how enemies decide where to walk, how they react to being shot, and what happens when they die.
State Machines: How Enemies Work
Every actor in the game lives in a statetype structure. Each state contains: a rotate flag (does the sprite rotate to face the player?), a shapenum (which sprite image to show), a tictime (how many game ticks to stay in this state), a think function pointer (called every frame), an action function pointer (called once on entry), and a pointer to the next state. When tictime expires, the enemy automatically transitions to whatever state next points to. This means enemy animations are just chains of states. A guard noticing the player transitions from s_grdstand to s_grdchase1 to s_grdchase2 and loops back to s_grdchase1, creating a walking animation — all without any explicit animation code, just data.
Chasing the Player: SelectChaseDir()
SelectChaseDir() calculates deltax and deltay between the enemy's tile position and the player's tile position. If deltax is positive the player is to the east; if negative, west. If deltay is positive the player is to the south; if negative, north. The function then tries the more dominant axis first — if the player is further away vertically than horizontally, it tries the vertical direction before the horizontal. It calls TryWalk() for each preferred direction. If all preferred directions are blocked, it tries random directions. Only as a last resort does it allow the enemy to reverse direction. There is no graph-based pathfinding here — just a greedy step toward the player, every frame.
Getting Hit: DamageActor()
DamageActor() first sets madenoise to true, which is the flag that tells nearby enemies a combat sound has occurred. Then, if the enemy is not yet in attack mode (meaning they haven't spotted the player yet), the damage is doubled by shifting it left one bit. This rewards shooting enemies before they see you. The damage is subtracted from hitpoints. If hitpoints drop to zero, KillActor() is called. If the enemy survives, FirstSighting() wakes them up and puts them into combat mode. Then a pain animation state is chosen — if hitpoints is odd, one pain frame is shown; if even, an alternate frame. This gives two alternating flinch animations without any extra logic.
Killing an Enemy: KillActor()
KillActor() uses a switch on the enemy's obclass. Guards give 100 points, transition to their death animation state, and drop a small ammo clip. SS soldiers give 500 points and drop a machine gun if you don't already have one, or a clip if you do — clever game design baked directly into the kill code. Boss characters give 5,000 points and drop a key. Every case ends with gamestate.killcount being incremented, the FL_SHOOTABLE flag being cleared so you can't keep shooting the corpse, and the actor's slot in the actorat grid being set to NULL so other actors can walk through that tile.
WL_ACT1.C — Doors, Items, and the World
WL_ACT1.C manages the physical world: the decorations in each room, the items on the floor, and most importantly, the sliding doors that are one of Wolfenstein 3D's most distinctive features.
The Item Table
Every placeable object in the game is defined in the statinfo array. Each entry pairs a sprite number with a behaviour type. A puddle has just a sprite number and no type — it is purely decorative. A green barrel has the block type, meaning you cannot walk through it. A dog food can has the bo_alpo type, making it collectible for 4 HP. A first aid kit has bo_firstaid for 25 HP. Ammo clips have bo_clip and bo_clip2 for different amounts. Treasure items like the cross (bo_cross, 100 points), chalice (bo_chalice, 500 points), bible (bo_bible, 1000 points), and crown (bo_crown, 5000 points) all have their own types. The array is terminated with a -1 entry that tells any loop it has reached the end.
Spawning Doors: SpawnDoor()
When a level loads, every door tile calls SpawnDoor(). The door's position in the doorposition array is set to 0, meaning fully closed. Its tile coordinates, vertical/horizontal orientation, and lock type are stored in doorobjlist. Then its tile in the actorat grid and tilemap is set to the door number OR'd with 0x80 — 128 in decimal. The 0x80 bit flags the tile as a door rather than a wall, and the lower 7 bits store which door number it is. This clever bit-packing lets the renderer and collision code tell at a glance whether a tile is a solid wall (value below 128) or a door (value 128 or higher, door number in the low bits).
Keeping Doors Safe to Close: CloseDoor()
Doors auto-close after being open for a set number of ticks. Before allowing this, CloseDoor() runs several safety checks. If any actor is standing on the door tile, it returns without closing. If the player's tile coordinates match the door tile, it returns. Even if the player's precise world position plus MINDIST (their collision radius) would overlap the door tile, it returns. This is why you can stand in a doorway forever without being crushed. Only when all checks pass does it set the door's action to dr_closing.
The Area Connection System: RecursiveConnect()
The map is divided into up to 37 numbered areas representing rooms and corridors. The areabyplayer array tracks which areas are currently reachable from the player's position through open doors. RecursiveConnect() is a recursive flood-fill: starting from the player's area, it checks every other area to see if areaconnect links them. If so, it marks that area as reachable and recurses into it. This matters because enemies only hear sounds and receive alerts if they are in an area connected to the player's area. When a door opens, it increments the connection count between the two areas it joins. When it closes, it decrements that count.
WL_ACT2.C — Enemy Behaviour
WL_ACT2.C is where enemy personalities are defined and where their specific attacks and behaviours live.
Difficulty and Hit Points
The starthitpoints array is a 4-by-22 table — four difficulty levels, 22 enemy types. A regular guard has 25 HP on all four difficulties. A dog always has 1 HP — any weapon, any difficulty, one hit. But the bosses scale dramatically. Schabbs has 850 HP on Baby mode and 2,400 HP on Death Incarnate. Mecha Hitler goes from 800 to 1,200. The Angel of Death goes from 1,450 to 2,000. The design philosophy is visible in the numbers: cannon fodder enemies stay constant, while boss encounters become genuinely different experiences at higher difficulties. A designer wanting to rebalance any enemy just changed a number in this table.
State Machine Chains: Rocket Smoke
The rocket and smoke states show the state machine system working as pure data. The rocket state has T_Projectile as its think function (moves the rocket each frame), A_Smoke as its action function (spawns a smoke puff), and points back to itself as next — so it keeps moving indefinitely. The four smoke states (s_smoke1 through s_smoke4) form a one-way chain: each shows a different smoke sprite for 3 ticks, then transitions to the next. s_smoke4 has NULL as next, so when it expires the actor removes itself. The entire smoke trail animation — spawning, animating, disappearing — is described in five lines of data with no special-case code.
The Direction Table
The dirtable array is a 3x3 grid of compass directions: northwest, north, northeast in the first row; west, nodir, east in the second; southwest, south, southeast in the third. By calculating deltax and deltay between two positions and mapping each to -1, 0, or 1, any pair of values indexes directly into this table and returns the correct compass direction. A single array lookup replaces what would otherwise be a deeply nested if/else chain comparing eight cases.
WL_SCALE.C — The Sprite Scaler
WL_SCALE.C solves a deceptively hard problem: how do you draw a sprite at different sizes depending on how far away it is, as fast as possible on 1992 hardware? The answer id Software came up with is remarkable — compile custom machine code at runtime.
Compiled Scalers: BuildCompScale()
When the game starts, for every possible sprite height from 1 pixel to 256 pixels tall, BuildCompScale() writes actual machine code into memory. It iterates through the 64 vertical slices of a sprite and for each slice calculates how many screen pixels it should produce at the target height. It then emits raw x86 opcode bytes directly: 0x8a and 0x44 to produce a "move from source" instruction, then 0x26, 0x88, and 0x85 followed by a screen offset to produce a "write to screen" instruction. A final 0xcb byte emits a RETF (return far) instruction to end the generated function. The result is a unique piece of machine code for each possible height — when called, it runs straight through with no loops, no branches, just direct memory writes. John Carmack called this a "compiled scaler" and it was one of the key reasons Wolfenstein 3D ran at 70fps on modest hardware.
Drawing a Sprite: ScaleShape()
ScaleShape() draws a sprite column by column, working outward from the sprite's horizontal center. For each column, before drawing anything, it reads wallheight at that screen X position. That array was filled in by WallRefresh() and holds the pixel height of whatever wall is at each screen column. If the wall there is taller than the sprite, the sprite column is skipped entirely — it is hidden behind the wall. This is how enemies can stand partially behind walls, with only part of their body visible. Once the column passes the wall check, ScaleLine() is called, which invokes the appropriate pre-compiled scaler for the current height.
WL_DEBUG.C — The Developer's Toolbox
WL_DEBUG.C is code that players were never meant to see. Activated by holding the Tab key and typing a letter, it is the collection of cheats and diagnostic tools that the id Software team used while building the game.
The Debug Key Handler: DebugKeys()
Pressing G while in debug mode toggles god mode using XOR with 1, a standard bit-toggling trick that flips between 0 and 1 on each press. A confirmation message is shown and the change takes effect immediately.
Pressing I runs the item cheat: it calls GivePoints(100000), HealSelf(99), adds 50 ammo, and calls GiveWeapon with bestweapon plus 1 — stepping you up exactly one weapon level rather than jumping straight to the chaingun. This means repeated presses eventually give you everything, but in a controlled sequence.
Pressing W opens a small text input box using US_LineInput(). Whatever number you type is converted with atoi() and validated to be between 1 and 10. If valid, gamestate.mapon is set to that number minus 1 (since the internal level numbering starts at 0), and playstate is set to ex_warped, which triggers the warp at the end of the current frame.
Counting Objects: CountObjects()
CountObjects() prints a diagnostic census of the current level's contents. It calculates the total number of static objects by subtracting the address of the first element of statobjlist from the laststatobj pointer — pointer arithmetic that gives the count directly. It then walks the actor linked list, incrementing active or inactive counters for each actor. Inactive actors exist in memory but are too far from the player to update every frame — a performance optimization that avoids running AI logic for enemies in distant rooms.
WL_TEXT.C — Help Screens and Story Text
WL_TEXT.C is the game's text rendering engine — used for the help screens, the end-of-episode story text, and any other multi-page text display. It includes a small custom markup language so designers could format screens without writing C code.
The Markup Language
The story text files use a caret character as an escape prefix. The P command starts a new page. The E command marks the end of the file. The G command followed by Y coordinate, X coordinate, and picture number draws a graphic image and reflows the surrounding text around it by adjusting the left or right margin for the affected rows. The C command followed by a two-digit hex value changes the current text color. The L command positions the text cursor at a specific pixel location. The B command draws a solid colored rectangle. This tiny scripting system meant artists and writers on the team could edit the story screens and help pages directly without touching a line of C.
Parsing a Word: HandleWord()
HandleWord() implements word wrapping from scratch. It copies characters from the text pointer into a local buffer, stopping when it hits any character with an ASCII value of 32 or below (spaces, tabs, newlines). It then calls VW_MeasurePropString() to find out how many pixels wide that word would be in the current proportional font. If the current cursor position plus the word width would exceed the right margin for the current row, it calls NewLine() to advance down and tries again. Once the word fits, VWB_DrawPropString() draws it and the cursor advances. Any trailing spaces are consumed and added to the cursor position individually at SPACEWIDTH pixels each.
WL_MENU.H — The Menu System's Interface
WL_MENU.H defines the entire menu system's public interface — its colour constants, layout positions, shared data structures, and function prototypes.
Two Skins, One Codebase
The same source code compiled into both Wolfenstein 3D and its standalone expansion Spear of Destiny. The #ifdef SPEAR blocks at the top of WL_MENU.H swap out the colour constants. Wolf3D uses BORDCOLOR of 0x29, a reddish-brown palette value. Spear of Destiny uses 0x99, a blue-grey. Even the MenuFadeOut macro is different — Wolf3D fades the screen toward red (43, 0, 0 in the RGB-ish fade parameters), Spear fades to black (0, 0, 51). Every visual difference between the two games' menus comes from these few constant swaps at compile time.
The Menu Item Structure: CP_itemtype
Each entry in any menu is a CP_itemtype structure. It has three fields: an active integer that determines whether the item can be selected (inactive items appear dimmed), a string of up to 35 characters that is the display label, and a function pointer called routine. When the player selects an item, the game simply calls whatever function is stored in that pointer. New Game calls CP_NewGame. Sound calls CP_Sound. Load Game calls CP_LoadGame. Adding a new menu item means adding one entry to the array with the right label and the right function.
The Main Menu Enum: menuitems
The main menu items are defined as an enum: newgame, soundmenu, control, loadgame, savegame, changeview, readthis, viewscores, backtodemo, and quit. Because C enums start at zero and count upward, newgame is 0, soundmenu is 1, and so on. This makes them usable as array indices and loop counters. The code can check whether the selected item is quit by name rather than by magic number 9, which makes the code readable without requiring comments to explain what each number means.
Closing Note
These twelve files represent roughly 12,000 lines of C and assembly code that shipped in 1992 and changed gaming forever. Reading through them, a few things stand out.
First, the code is remarkably honest. Comments like "DEBUG: use assembly divide", the question mark at the end of the quit message reading "Demo loop exited???", and small notes left mid-thought show a team working fast and thinking out loud in their source files.
Second, almost every unusual technique — the fixed-point arithmetic, the compiled scalers, the inline assembly, the pre-calculated trig tables — exists for the same reason. PC hardware in 1992 was brutally slow, and every frame of that 70fps experience was fought for, instruction by instruction.
Third, the game logic is genuinely simple. The AI doesn't pathfind. The combat doesn't simulate ballistics. The "3D" is a flat grid of square tiles. The genius of Wolfenstein 3D is that it created an overwhelming experience of speed and danger out of extraordinarily simple building blocks — and that simplicity is what makes the code still readable thirty years later.
