Getting the most from Quake3 C

From ioquake3 wiki
Jump to: navigation, search

Article 3 - Getting the most from Quake3 C

By [hypothermia]

This article is intended to help you get oriented on the Quake3 source code. Most of what is written here should already be known to experienced and capable programmers.

The Quake3 source is written so it will compile using ANSI C. This is of great benefit to the mod developer community because there are already excellent tools out there aimed at the professional coder. However: there is no full implementation of the standard library. There's too much code to provide an account of what everything does. However, articles and tutorials here at Code3Arena should help you get oriented on some more specific areas.

You'll also find that many of the comments I've made are a matter of personal style. There is no 'best' coding style in C, only flame wars about it. There are, however, ways you can help yourself write code that's easier to understand, debug, and change at a later date. There are some links to useful resources at the end of the page.

Getting Started

The first and most important thing is to be able to compile the code using your compiler and header files. If you've got Microsoft Visual C++ then just open up the project and do a test compile. You're up and running already.

For those that have another compiler you might have to do some extra work. I've already written a tutorial ("Compiling without Microsoft Visual C++")that should get you off the ground. Don't forget to check the Code3Arena downloads for solutions others have already prepared for your compiler/platform.

Now you have the ability to build the code, we'll start taking a more detailed look.

Program Structure

The game code is split into three basic modules, source/ui, source/game and source/cgame. Each of these contains the code for the user interface (menus and stuff), the running of a game server, and the display of the information from the server on your (client) machine, respectively. Note that the game server (game) and client (cgame) are separate. Both are required to play the game, but only the client needs to be running on your machine. The server can run on a remote machine (when you make an Internet connection) or on your own machine (when you play single player against the bots).

It's important to understand this model, as it dictates where you need to make modifications. Trying to place a menu in the server code (game) just doesn't make sense. At the very least you wouldn't be able to use this menu while playing online. Each of these modules runs independently, and there are only limited forms of communication between modules.

Source files and functions

Within each of these three modules there are a large number of source code files. Each of these files implements a feature (or a small group of related features) of the game.

This helps considerably when you're trying to find your way around the code. Almost all of the functions required to implement that feature will be within that one source file.

When adding new functions it's beneficial to name them with a unique prefix for that source file. That way, if the function is called from another source file, you have a good idea of where to find it. For example: All the functions in ui_servers2.c are prefixed with ArenaServers_, almost guaranteeing that the name won't be duplicated elsewhere in the source.

You'll find this hasn't been applied consistently: a result of more than one programmer working on the source.

Understanding the code before getting started

While tinkering around in the code is fun, making a serious modification requires a deeper understanding. Make sure you understand the dependencies and relationships between variables and functions.

Strong clues can be found in the way data structures are used, and (obviously) the names of the variables. Concentrate on a function that implements a particular feature, and build up from there.

More clues can be found by the use of static functions and local data, you know there are no modifications outside that source file. When you've made a modification and you're trying to debug it, the effort made to understand the code will reap benefits.


C library functions

There is no (complete) C standard library for Quake3!

If you use or need a function from the C standard library you'll have to implement it yourself. There are definitions in q_shared.c of functions that have the expected behavior. Each is prefixed by a Q_ so I'll call them Q functions. Look there first for library functions. There is also a subset of library functions implemented in bg_lib.c. This file is only included when building for the Quake3 Virtual Machine. It will assist while you make the transition to the Q functions.

If you appear to have any problems with standard C library calls between your binaries and virtual machine bytecode then convert to the Q functions used by the virtual machine. You'll then be getting the same code.

No Malloc!

This is the most obvious omission from the C library. If you use malloc like a crutch then you'll have to change your coding style. The omission is a Good Thing(tm).

It forces coding using data that is static and/or part of the stack. You now have to think about how much space you need for your data in the worst case. In other words you have to think more about the design of your program.

It also means that the Virtual Machine is more stable: no bugged bytecode QVM eating up memory on the server.

Having said that, there are some algorithms that benefit from "memory allocation". It's possible to provide your own malloc() like behavior, but this introduces a whole new class of bugs to worry about.

Calling the Quake 3 executable

There are some things that just need to be done as efficiently as possible. This means a call into the executable. All of these function names start with trap_ and call the executable in the *_syscalls.c files.

The only way to learn what these functions can do for you is by understanding the data structures passed, and how their data is prepared and used within the source code.

Comment your code!

The most accurate documentation of the code is the code itself. It documents every bug as yet undiscovered, and will automatically document changes made to it.

Unfortunately the code doesn't help you understand itself.

Accurate and frequent comments on what you're doing (and how you're doing it) will do wonders when you come to track down that obscure bug whether 6 minutes or 6 months later.

Just make sure they're accurate comments!

Struc-urless code

When you use a data structure in C it needs to be referred to using the keyword struct. There is a neat trick that allows you to get around this and save typing, as well as annoying compiler errors when you forget to put it in. Lets have a look at an example taken from ui/servers2.c:

typedef struct servernode_s {
   char   adrstr[MAX_ADDRESSLENGTH];
   char   hostname[MAX_HOSTNAMELENGTH];
   char   mapname[MAX_MAPNAMELENGTH];
   int    numclients;
   int    maxclients;
   int    pingtime;
   int    gametype;
   int    nettype;
} servernode_t;

You can access the data type in one of two ways:

struct servernode_s* servernodeptr;

or:

servernode_t* servernodeptr;

You choose!

Use constants

If you're using a number to represent something that's used in several places then you should #define it using a descriptive name. Use that #def'd name instead of the number.

If you need to come back to change the code then you have only one dependancy to change. The #def'd name is also another way of helping document your code.

There are many, many examples of this all over the Quake3 Source. In fact they don't write the code any other way.

Get used to it. Now!

Avoid globals: use static declaration

By putting too many data types and functions into header files and making them global you risk a name clash.

You can avoid this by defining the datatype in the source file itself. The definition is only visible within that file, and there will be no clashes with other data types or function names elsewhere.

For functions you can declare them static. This means they can't be accessed from outside the source file they're defined in. No possibility of names colliding. One other benefit: if you define the function before it's first use then you don't need to declare it's prototype.

If you need a datatype or function declaration to be available in more than one source file then use a header file. Put the declaration in q_shared.h as a last resort.

Under no circumstances should you refer to the same datatype or function by using separate declarations in two different source files. You'll get weird synchronization errors when you forget to change one of them.

American spelling

Those of you that use English (rather than American) will find the spelling in the code is... different.

Unfortunately this presents a problem. If you search the code for keywords on a regular basis then you won't catch everything if you've used English spelling only for your modifications.

In order to help searches through the code, I'd suggest you consider using American spelling only. Comments can use any spelling you like!

Resources for further reading

There are quite a few documents and news groups out there that will help you get used to C coding. Note that they are oriented towards a full implementation of ANSI C (libraries and all).

A Meta-FAQ that covers just about all the ground can be found at the C-FAQ Index.

For those who are interested in writing code in a more efficient way then I'd suggest looking at Optimizing database rendering code (Alt). Although it's aimed at implementing an efficient OpenGL driver, its application is more general. Use these techniques in a performance critical section of code. Not all of them are guaranteed to work on the Quake Virtual Machine, but you'll get a few good ideas.


« Article 2 - Vectors | Article 3 - Getting the most from Quake3 C | Article 4 - How to Compile Without Microsoft Visual C++ (Part 1) »