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 i ui_servers2.c are prefixed
with ArenaSernvers_, 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 subsetset 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 most of 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.
|