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:
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