Introduction

In 1997, the computer gaming company id Software released a watershed first-person shooter game called QUAKE II, which went on to sell over one million copies and earn industry accolades as Game of the Year. Later, in December 2001, id Software generously made the QUAKE II 3-D engine available to the public under the GNU General Public License (“GPL”).

Now, in July 2003, Vertigo Software, Inc. is releasing Quake II .NET, a port of the C-language based engine to Visual C++ with a .NET managed heads-up display. We did this to illustrate a point: one can easily port a large amount of C code to C++, and then run the whole application as a managed .NET application using the Microsoft Common Language Runtime (CLR) without noticeable performance delays. Once running as a .NET managed application, adding new features is easy and fun.

This paper discusses what was involved in porting and extending the Quake II engine to Quake II .NET.

Source Code and Files

The complete source code for the Quake II engine is available from id Software at ftp://ftp.idsoftware.com/idstuff/source/quake2.zip. The source code is released under the terms of the GNU General Public License (“GPL”). You should read the accompanying readme.txt and gnu.txt files for more information on the GPL.

There are two parts to the Quake game: the engine and the data.

Game engine

The engine is the code that runs the game and is composed of the following files:

File

Description

quake2.exe

The main game executable.

ref_soft.dll

The software rendering engine.

ref_gl.dll

The OpenGL rendering engine.

gamex86.dll

The core game engine.

The managed version of Quake II .NET contains an additional file that implements the radar extension:

File

Description

Radar.dll

Managed C++ radar extension.

Game data

Maps, monsters, weapons, and other game essentials are contained in a data file (often packaged in a single PAK file) in the baseq2 folder. Multiplayer data is stored in the baseq2\players folder.

How to Run Quake II .NET

Vertigo provides the five files above. However, you need just one more file, pak0.pak in order to run Quake II .NET. The PAK file contains id Software’s copy written 3-D models and images and they’d prefer that you get that file from them:

All of the Q2 data files remain copyrighted and licensed under the original terms, so you cannot redistribute data from the original game... -John Carmack, id Software, from the readme.txt in the GPL’d source code

Therefore, you need to get the PAK file from the official Quake II demo. Here’s what you need to do:

Install the Quake II .NET.msi (see download above). This installs the files for the native and managed versions.

Copy the pak0.pak file from the Quake2 Demo\Install\Data\baseq2 folder to the %ProgramFiles%\Quake II .NET\managed\baseq2 and %ProgramFiles%\Quake II.NET\native\baseq2 folders. This file is big—about 48MB and you’re making two copies. If you uninstall Quake II .NET, you have to remove these two copies by hand.

Run the managed or native version by clicking the shortcut in the Start menu.

How to Build the Code

Building the code is straightforward but you need to copy the generated EXE and DLLs to the runtime before running the app. The steps are outlined below:

Select the target configuration (release or debug, native or managed) and build the solution. Files are generated in the specified build configuration (Release Managed for example).

Copy the engine files from the source location to the Quake II .NET runtime installation (the default folder is %ProgramFiles%\Quake II .NET). The following table shows what files to copy:

File

Copy To

quake2.exe

\Program Files\Quake II .NET\

ref_soft.dll

\Program Files\Quake II .NET\

ref_gl.dll

\Program Files\Quake II .NET\

gamex86.dll

\Program Files\Quake II .NET\baseq2

Radar.dll

\Program Files\Quake II .NET\ (only required for the managed version)

Copying the files could be automated with a custom post build step.

How We Ported the Code

The source code was downloaded from id Software’s FTP site as discussed above. This code contains a Visual Studio 6 workspace file named quake2.dsw. When opening this file, Visual Studio prompts you to update the project files and generates a solution file named quake2.sln. The following changes were made to the projects:

Strong typing

The C++ language is strongly typed so assignment and function arguments require casting if the types don’t match. This was the largest portion of the port. Though tedious, it was easy to port since the compiler identified the exact problem including source file, line number, and the required cast. An example is shown below.

The Quake code uses GetProcAddress to dynamically retrieve the address of functions in other DLLs. All of the calls required casting and it was quickly noted that there were a lot of these calls. A script was created for the portion of the code that read the build log and modified the source code with the proper cast. An example of the required cast is shown below.

The C language does not require that declarations exactly match definitions or declarations in other source files and this caused compiler errors: C2371 (redefinition; different basic types) and C2556 (overloaded functions only differ by return type). Function declarations and definitions were modified to resolve any conflicts. For example, the function declaration below was changed to return an rserr_t instead of an int.

If extern was used to declare the function that was defined in another file, this error was not caught until link time and appeared as an unresolved external.

Using COM objects

The calling convention for COM interfaces is different between C and C++ because vtables (virtual function table) are supported in the C++ language. For the C language, the vtable of the COM interface is explicitly accessed and a ‘this pointer’ is passed as the first argument. An example that calls the Unlock method of a COM object is shown below.

Porting to Managed C++

Managed code runs within the context of the .NET run-time environment. It is not compulsory to use managed code, but there are many advantages to doing so. A program written with managed code using Managed Extensions for C++, for example, can operate with the common language runtime to provide services such as memory management, cross-language integration, code access security, and automatic lifetime control of objects.

The first step required when porting native C++ to managed C++ is to set the /CLR compile switch by enabling Managed Extensions for C++. Depending on your project, this might be the only step required, but the following errors were also encountered when porting Quake to managed C++.

Incompatible switches

The /clr and /YX (Automatic Use of Precompiled Headers) switches are incompatible so the /YX switch was turned off for managed builds.

Mixed DLL loading problem

The code compiled but all of the projects that generated DLLs had the following link warning.

LINK : warning LNK4243: DLL containing objects compiled with /clr is not linked
with /NOENTRY; image may not run correctly

This warning occurs when a managed C++ DLL contains an entry point and the linker is letting you know a deadlock scenario could occur during the loading process. This was fixed by doing the following:

The Quake2 executable contains forward declarations to structures that are defined in other DLLs. The Visual C++ compiler fails to emit the necessary metadata for these structures and a System.TypeLoadException is thrown at runtime indicating that the structure could not be found in the assembly.

// in cl_parse.c// empty definitions for structs that are forward declared// this causes the compiler to emit the proper metadata// and not throw a System.TypeLoadException exceptionstruct image_s {};
struct model_s {};

Extending Quake

Now that we had Quake II running inside in the .NET run-time environment, we wanted to add a significant new feature, written solely in .NET. After looking at the games we play today namely Halo) we settled on a heads-up radar display that shows enemies, power-ups and other interesting objects in birds-eye view.

The radar extension was created in managed C++. Since this is a managed class, GDI features of the .NET Framework are used, including arrow caps, gradient brushes, antialiasing, and window transparency and opacity. Radar items are rotated around the center of the window using Matrix.RotateAt instead of calculating each item’s position with trig functions. A context menu allows visual items, crosshair and field of view for example, to be shown or hidden.

The last menu item (Overlay on Quake) overlays the radar on top of the Quake window; the radar hides the window frame and status bar, sets window transparency and opacity, and resizes itself to fit over the Quake window.

Radar items are stored in an STL vector list. The code snippet below shows how an iterator is used to loop through the list and draw each item on the radar.

Window position changed

The radar needs to know if the Quake window position or size has changed when it is displayed in overlay mode. The Quake code processes the WM_WINDOWPOSCHANGED message and passes the event to the radar extension.

Performance

Getting existing projects into managed code is useful since it offers a lot of design freedom, for example:

Use garbage collection or manage memory yourself.

Use .NET Framework methods or Window API calls directly.

Use .NET Framework classes or existing libraries (STL for example).

However, usefulness only matters if the managed application has the performance you require. Running Quake II.NET in the timedemo test indicates the managed version performs about 85% as fast as the native version. The performance of the managed version was acceptable and testers did not notice a difference between the two versions. You can run the timedemo test by doing the following:

Display the command window by pressing the tilde (~) key.

Enter disconnect if currently playing a game. This is not necessary if Quake is in demo mode.

Enter timedemo 1 and press enter.

Press the tilde (~) key again to close the command window. Quake runs through the demo measuring the frame rate.

Press the tilde (~) key to stop the test. The frame rate is displayed in the command window.

Enter timedemo 0 to turn off the test.

Summary

Porting the C code to native C++ took about 4 days, and porting to managed C++ took another day. The extension took about two days to implement, as did poking around the Quake code to figure out the integration points. The experience overall was very good and we felt productive porting and extending the code. It’s nice to mix native and managed code, to have control over memory management, and to use existing libraries as well as .NET Framework classes, all in the same application.

Ralph Arvesen, Vertigo Software, Inc.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

Share

About the Author

Ralph Arvesen is a software engineer for Vertigo Software and has worked on desktop, web and Pocket PC applications using .NET and C++. Before Vertigo, he designed hardware and firmware for optical inspection systems and has been developing software for the Microsoft platform since Windows 2.0. He co-authored several books and worked as technical editor on others. Ralph lives in the Texas Hill Country west of Austin; his personal site is located at www.lostsprings.com.

Comments and Discussions

I was going through the code, looking for the part sent from the client to the server (client info). I found that the client sends part of the data structure "edict_s" found in g_local.h to the server (i.e. the server use only a part of the structure). However, I need to send more information to the server (let the server use most of edict_s), so how can I modify the code to do so (in which part).

I know this is an old article, but I just came across it, and I love it.

I'm impressed the performance of the managed version is as high as 85% compared to the native version. This just shows that managed code can compete on performance. And this is a plain port, there's plenty of improvements to be made if you wrote from scratch in .Net.

When I try to run the msi file for Quake 2 Net, I get a message saying I need version 1.1.xx of the NET framework. I have version 2 of net framework, which is required for Visual Studio 2K5. I'm pretty sure it is backward compatible, but I don't know how to get the installer to accept it. Any ideas? Thanks.

Asher - It sounds like you need to install the Quake II data files. We could not include these in the installation and they must be downloaded and installed separately. There is more info and instructions at http://www.vertigosoftware.com/Quake2PostInstall.htm.
--
ralph

"The Quake code uses GetProcAddress to dynamically retrieve the address of functions in other DLLs. All of the calls required casting and it was quickly noted that there were a lot of these calls. A script was created for the portion of the code that read the build log and modified the source code with the proper cast. An example of the required cast is shown below."