ARTIKEL 3 - Werde mit Quake3 C vertraut

von HypoThermia


Dieser Artikel soll dir helfen, dich besser im Quake 3 Source Code zurechtzufinden. Der grösste Teil des Artikels sollte für erfahrene und fähige Programmierer nichts neues sein.
Der Quake3 Code ist in ANSI C geschrieben und wird als solches kompiliert. Das ist für die Mod-Entwickler Szene von grossem Vorteil, da bereits hervorragende Profi-Tools vorhanden sind. Die Standardbibliothek ist allerdings nicht vollständig implementiert.
Der Quake3 Code ist zwar zu umfangreich, um jeden Bereich einzeln zu erläutern, die Code3Arena Artikel und Tutorials sollten aber zumindest die Orientierung in einigen interessanten Bereichen erleichtern.
Du wirst feststellen, dass viele meiner Kommentare meinen individuellen Stil ausdrücken. Es gibt keinen "besten" Programmierstil in C, nur flame wars darüber. Ein paar Konzepte gibt es aber doch, mit deren Hilfe dein Code leichter zu verstehen, zu debuggen und zu verändern ist.
Schau dir dazu auch die Links am Ende dieses Artikels an.

Los gehts
Die erste und wichtigste Sache ist das Kompilieren des Codes mit deinem Compiler und den Headerdateien. Falls du mit Microsoft Visual C++ arbeitest, öffne einfach das Projekt (*.dsw) und versuche es zu erstellen (F7). Es sollte keine Probleme geben.
Falls du einen anderen Compiler verwendest, könnte das etwas Extraaufwand kosten. Das Tutorial "Kompilieren ohne Microsoft Visual C++" sollte dir weiterhelfen. Überprüfe die Code3Arena Download-Sektion, eventuell hat schon jemand eine Lösung für deinen Compiler/deine Plattform erarbeitet.
Da du den Code jetzt erstellen kannst, werden wir nun die Einzelheiten ansehen.


Programmstruktur
Der Code ist grundsätzlich in 3 Module aufgeteilt, code/q3_ui, code/game und code/cgame. Diese enthalten den Code für die Benutzerschnittstelle (Menü usw.), den Server (das eigentliche Spiel) und den Client (Anzeige der Serverdaten).
Beachte, dass das Server (game) und der Client (cgame) getrennt sind. Für das Spiel werden beide benötigt, aber nur der Client muss auf deinem Rechner laufen. Der Server kann durchaus auf einem anderen Rechner laufen (mit dem du z.B. via Internet verbunden bist) aber natürlich auch auf deinem Rechner (z.B. wenn du Singleplayer spielst).
Es ist wichtig, dieses Konzept zu verstehen, um später nicht das falsche Modul zu modifizieren. Es ist wenig sinnvoll, ein Menü in den Server Code (game) zu schreiben. Kein Spieler, ausser dem auf dem Serverrechner, könnte das Menü benutzen.
Alle 3 Module laufen unabhängig voneinander, die Kommunikation zwischen ihnen ist dementsprechend eingeschränkt.


Quellcode Dateien und Funktionen
Jedes der drei Module enthält eine grosse Zahl von Quellcode Dateien. Jede dieser Dateien implementiert ein Feature (Merkmal) des Spieles, oder mehrere zusammengehörige Features.
Das ist eine grosse Hilfe, wenn man versucht, sich im Code zurechtzufinden. So gut wie alle Funktionen, die für die Implementierung eines bestimmten Features relevant sind, wird man in dieser einen Quellcode Datei finden.
Beim Erstellen einer neuen Funktion ist es sinnvoll, ihrem Namen ein Präfix voranzustellen, dass die Quellcode Datei eindeutig identifiziert. Dadurch findest du sie schneller, auch wenn sie aus anderen Dateien aufgerufen wird. Ein Beispiel: Alle Funktionen in ui_servers2.c haben das Präfix ArenaServers_, was sie eindeutig (wenn sich der Programmierer daran hält) von allen anderen Funktionen des Programms unterscheidet.
Du wirst bemerken, dass dieses Konzept nicht konsequent angewendet wurde. So etwas passiert wenn mehr als eine Person, und damit mehr als ein Stil, am Code arbeitet.

Erst Verstehen, dann Verändern
Am Code herumzubasteln ist das Eine, Eine ernsthafte Mod zu erstellen erfordert aber ein tieferes Verständniss. Versuche vor allem die Abhängigkeiten und Beziehungen der Variablen und Funktionen zu verstehen.
Gute Hinweise erhältst du oft durch die Art, auf die eine Struktur benutzt wird, und natürlich durch die Variablennamen selbst. Konzentriere dich auf eine Funktion, die ein bestimmtes Merkmal implementiert, und baue deinen Code um sie herum auf.
Weitere Hinweise geben lokale Variablen und statische Funktionen, weil du sicher sein kannst, dass sie keine Effekte ausserhalb der umgebenden Funkion/Datei haben.
Sobald du deine Mod debuggen musst, wird sich der für das Codeverständniss aufgebrachte Aufwand rentieren.

C Bibliotheksfunktionen
Die C Standardbibliothek ist nicht (komplett) in Quake3 eingebunden !
Wenn du eine Funktion aus der C Standardbibliothek brauchst, wirst du sie selbst implementieren müssen. In q_shared.c sind Funktionen definiert die das Verhalten entsprechender Standardbibliotheksfunktionen nachbilden. Da sie alle das Präfix Q_ besitzen, werde ich sie Q Funktionen nennen. Wenn du eine Funktion aus der Standardbibliothek brauchst, sieh zuerst in q_shared.c nach.
Eine Teilmenge der Standardbibliotheksfunktionen ist auch in bg_lib.c implementiert. Diese Datei wird nur bei der Erstellung von *.qvm's eingebunden. Sie wird dir beim Übergang zu den Q Funktionen hilfreich sein.
Falls die Standardbibliotheksfunktionen zwischen Maschienencode (dll) und Bytecode (qvm) Probleme verursachen, solltest du diejenigen Q Funktionen, die die qvm braucht, konvertieren.

Kein malloc!
Das ist der wichtigste Unterschied zur Standardbibliothek. Falls du malloc exzessiv benutzt, wirst du dein Stil ändern müssen.
Diese Auslassung ist eine gute Sache. Sie zwingt dich, aussschliesslich mit Variablen auszukommen, die statisch sind und/oder auf dem Stack liegen. Du musst also immer genug Speicher für den schlimmsten Fall anlegen. Anders gesagt: du must dich intensiver mit dem Design des Programms auseinandersetzen.
Ein weiterer positiver Effekt ist die Stabilität: keine verbuggte qvm, die dem Server den Speicher wegfrisst.
Andererseits gibt es natürlich auch ein paar Algorithmen die von dynamisch angelegtem Speicher profitieren.
Es ist möglich, die Funktionalität von malloc() selbst zu implementieren. Das bringt aber einen Haufen neuer Probleme mit sich, um die man sich kümmern muss.


Funktionsaufrufe in die quake3.exe
Einige Aufgaben müssen so effektiv wie möglich ausgeführt werden. Für uns bedeuted das einen Funktionsaufruf in die exe. Diese Funktionen finden sich in den *_syscalls.c Dateien, haben alle das Präfix trap_ und übernehmen für uns die Aufrufe in die exe.
Was genau diese Funktionen leisten, lässt sich nur durch die Datentypen der übergebenen Variablen und deren Verwendung im Code herausfinden.


Kommentiere Deinen Code!
Der beste Kommentar ist der Code selbst. Er dokumentiert sogar die verborgensten Bugs und eine Änderung im Code wird ihm nie entgehen.
Dummerweise gibt es Menschen die ihn schlecht verstehen.
Exakte und regelmässige Kommentare darüber, was und warum der Code etwas macht, werden Wunder wirken, wenn du nach unverständlichen Bugs suchst, ob nun nach 6 Minuten oder 6 Monaten.
Versichere dich nur darüber, dass die Kommentare exakt sind.


Strukt-urloser Code
Um in C auf eine Datenstruktur zuzugreifen benögt man das Schlüsselwort struct. Ein kleiner Trick erlaubt es dir, dich davor zu drücken und spart nervende Complierfehler.
Schauen wir uns dieses Beispiel aus ui/servers2.c an:


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;

Cmd_Cloak_f( ent );

Eine Variable dieses Typs lässt sich auf zwei Arten erzeugen:

struct servernode_s* servernodeptr;

oder:

servernode_t* servernodeptr;

Benutze Konstanten
Wenn du eine Zahl an mehreren Stellen im Code verwendest um etwas auszudrücken (z.B. Pi), solltest du sie lieber mit Hilfe von #define durch einen beschreibenden Namen ersetzen. Diesen setzt du dann überall ein, wo vorher die Zahl stand. Das trägt nicht nur zur Wartbarkeit deines Codes bei, sondern auch zur Lesbarkeit. Der Name sollte unbedingt nur aus Grossbuchstaben bestehen, damit er immer von Variablennamen (und auch Teilstrings von Variablennamen) zu unterscheiden ist.
Beispiele dafür gibt es Hunderte, über den ganzen Quake3 Source verteilt. Schau es dir an und mach dich damit vertraut. JETZT!

Vermeide globale Datentypen und Funktionen, nutze static Deklarationen
Wenn du zuviele Datentypen und Funktionen in Headerdaten definierst, und sie damit global zugänglich machst, riskierst du Namenskonflikte.
Du kannst das einfach verhindern, indem du Datentypen in der Quellcode Datei definierst. Dadurch sind sie ausserhalb der Datei unbekannt, und können keine Konflikte verursachen.
Für Funktionen erreichst du das gleiche, indem du sie static deklarierst. Dadurch können sie nicht von ausserhalb ihrer Datei aufgerufen werden. Ein weiterer Vorteil: wenn die Funktion vor ihrem ersten Aufruf definiert wird, musst du keinen Prototypen deklarieren.
Wenn ein Datentyp oder eine Funktion in mehr als einer Quellcode Datei verfügbar sein soll, musst du eine Headerdatei verwenden. Eine Deklaration in q_shared.h ist nur in seltenen Fällen empfehlenswert.
Auf keinen Fall solltest du den gleichen Datentyp / gleiche Funktion in mehreren Quellcode Dateien deklarieren. Es bringt nur Ärger, beide ständig synchron zu halten.

Vertiefende Literatur

<< zurück/back >>