📜 ⬆️ ⬇️

Duke Nukem 3D Source Code Analysis: Part 2

image

[The translation of the first part is here .]

Inherited code


Build is an outstanding engine, and the many games that used it brought great and deserved fame to both Ken Silverman and 3D Realms.
')
Ken Silverman fulfilled the terms of the contract: he provided a binary file of a stunning 3D engine with well-documented methods and resource formats . In recognition of his achievements, 3D Realms indicated his name in the credits as "Ken 'I can do that' Silverman" (Ken "I can do it" Silverman). But Build's development was focused on features and speed, not the convenience of porting and reading. After studying the code, I think that open source developers have avoided it for the following reasons:


In this article, I have listed some of the difficulties encountered. I also released the Chocolate Duke Nukem 3D port to solve these problems. I wanted people to remember what level of genius was needed to create a 3D engine at the time. In addition, I wanted them to realize how a teenager driven by passion could contribute to one of the greatest games of all time.

Difficulty in understanding



Porting complexity



Lost resources


Ken Silverman once contributed to the creation of the JonoF port and published many explanations for his algorithms. Unfortunately, the forum is no longer working because of spam. It seems that all this valuable information is lost!

I regretfully closed the forum on the site. Because of the spam bots, it became impossible to manage it, and although there is valuable content in the posts, my time and the moderator time are not worth it.

If you want to keep in touch with the community, then I suggest to refer to the forums on Duke4.net.

image

Chocolate Duke Nukem 3D


Chocolate Duke Nukem 3D is a Duke Nukem 3D port for training . Its main goal is to streamline the code so that programmers can conveniently get knowledge from it and are better aware of what it was like to program game engines in the 90s.

Like an archaeologist working with bones, it was important for me to keep everything “as it is,” and to get rid of “dust” only, focusing on:


Binary code


This is a port for game developers who want to learn about Duke Nukem 3D architecture and source code. If you just want to play a game, I recommend using EDuke32 .

If you still want to play Chocolate Duke Nukem 3D, then simply download the source code, which is a XCode / Visual Studio project, and build it: link to the source code .

Portability


Lack of portability was a problem. Now Chocolate Duke Nukem 3D is compiled under Windows, Intel MacOS X and Linux. Here is how it was done:


The code has become much more portable, but still not ready for 64 bits: you need to work more on the interface between the engine module and the drawing module , in which memory addresses are processed as 32-bit integers. It is necessary to spend many hours for this part, and I am not sure that I can spend so much time.

Comprehensibility


Most of the effort went into simplifying the readability of the code. Here is how I achieved this:

Reassign Modules




The "vanilla" source code essentially consisted of three broadcast modules:




The code was repartitioned into modules, giving a clear idea of ​​what is contained inside:

I was tempted to break Engine.c into a frontend and backend in imitation of the Quake3 / Doom3 architecture, which consists of two parts, exchanging data via the stack. But as a result, I decided that I would be too distant from the spirit of the original engine, so I rejected this idea.

Data structure


For data exchange with the game module through build.h, the Build engine used a struct , but inside everything was organized through arrays of primitive data types, without a struct and typedef .

I made changes, especially in the part related to the definition of visible surfaces (Visual Surface Determination) and the file system:

Before :

 long numgroupfiles = 0; long gnumfiles[MAXGROUPFILES]; long groupfil[MAXGROUPFILES] = {-1,-1,-1,-1}; long groupfilpos[MAXGROUPFILES]; char *gfilelist[MAXGROUPFILES]; long *gfileoffs[MAXGROUPFILES]; char filegrp[MAXOPENFILES]; long filepos[MAXOPENFILES]; long filehan[MAXOPENFILES]; 

After :

 //    GRP: // - 12     // - 4     typedef uint8_t grpIndexEntry_t[16]; typedef struct grpArchive_s{ int32_t numFiles ;//   . grpIndexEntry_t *gfilelist ;//,   . int32_t *fileOffsets ;//,   . int32_t *filesizes ;//,   . int fileDescriptor ;//fd      . uint32_t crc32 ;//    GRP: Duke Shareware, Duke plutonimum  .... } grpArchive_t; //  GRP     typedef struct grpSet_s{ grpArchive_t archives[MAXGROUPFILES]; int32_t num; } grpSet_t; 

Improving symbolic names


I changed those variable names that were of little help in understanding their purpose:

Before :

 static long xb1[MAXWALLSB], yb1[MAXWALLSB], xb2[MAXWALLSB], yb2[MAXWALLSB]; static long rx1[MAXWALLSB], ry1[MAXWALLSB], rx2[MAXWALLSB], ry2[MAXWALLSB]; static short p2[MAXWALLSB], thesector[MAXWALLSB], thewall[MAXWALLSB]; 

After :

 enum vector_index_e {VEC_X=0,VEC_Y=1}; enum screenSpaceCoo_index_e {VEC_COL=0,VEC_DIST=1}; typedef int32_t vector_t[2]; typedef int32_t coo2D_t[2]; //  ,      . //       . typedef struct pvWall_s{ vector_t cameraSpaceCoo[2]; //      .    vector_index_e. int16_t sectorId; // ,    ,    . int16_t worldWallId; //     . coo2D_t screenSpaceCoo[2]; //      .    screenSpaceCoo_index_e. } pvWall_t; //       . pvWall_t pvWalls[MAXWALLSB]; 

Comments and documentation



"Magic" numbers


I did not have time to get rid of all the "magic" numbers. Replacing decimal literals with enum or #define will significantly improve readability of the code.

Memory allocation


In Chocolate Duke, I tried to avoid global variables. Especially if they are used during the lifetime of the frame. In such cases, the memory used will be on the stack:

Before :

 long globalzd, globalbufplc, globalyscale, globalorientation; long globalx1, globaly1, globalx2, globaly2, globalx3, globaly3, globalzx; long globalx, globaly, globalz; static short sectorborder[256], sectorbordercnt; static char tablesloaded = 0; long pageoffset, ydim16, qsetmode = 0; 

After :

 /* FCS:      ( -     nextsector >= 0).   ,        . */ static void scansector (short sectnum) { //,  ,   . short sectorsToVisit[256], numSectorsToVisit; . . . } 

Note: Be careful when using the stack frame to store large variables. The following code was normally executed when compiling into clang and gcc, but resulted in an error in Visual Studio:

 int32_t initgroupfile(const char *filename) { uint8_t buf[16] ; int32_t i, j, k ; grpArchive_t* archive ; uint8_t crcBuffer[ 1 << 20] ; printf("Loading %s ...\n", filename) ; . . . } 

A stack overflow error occurs because by default, Visual Studio reserves only 1 MB for the stack. Attempting to use chkstk results in a stack overflow, which is not good at chkstk digesting. This code will run normally in Clang on Mac OS X.

Source


Source code is available on Github .

Source: https://habr.com/ru/post/323684/


All Articles