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 consistantly: a result of more than one programmer
working on the source.
Understand the code before making changes
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 behaviour. 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 behaviour, but this introduces a
whole new class of bugs to worry about.
Calls into the Quake3 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.
Commenting 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!
Struct-ureless 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.
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.
|