TUTORIAL 16 - Intelligenter Waffenwechsel
von Mark "WarZone" Smeltzer
Mit Hilfe dieses Tutorials wird es jedem Client deines Mods möglich sein, das Waffenwechseln
seinem Geschmack anzupassen. Bei Quake2 waren die Prioritäten der Waffen unabänderlich aber beim
guten alten Quake konnte jeder Spieler noch seine eigene Waffenwechsel-reihenfolge festlegen.
In Quake2 war z.B. die BFG die "beste" Waffe, gefolgt von der Railgun, dem Hyperblaster, dem
Rocketlauncher usw. Genau diese Reihenfolge kann jeder Client in Quake und in diesem Mod
selbst festlegen.
1. Quake3 Waffenwechsel ?
Also: welche coolen Waffenwechsel Merkmale spendiert uns Quake3 ? Keine, na toll. In Quake3
gibt es genau 2 Arten die Waffe zu wechseln: automatisch und manuell. Automatisch heisst:
sobald man eine Waffe einsammelt wird diese ausgewählt. Auch wenn man mit der BFG rumläuft
und eine Shotgun einsammelt, wird sofort die Shotgun ausgewählt. Kurz gesagt: absolut
unbrauchbar... manueller Waffenwechsel ist auch nicht so viel besser. Wie der Name schon
vermuten lässt, wechselt der manuelle Modus nie ohne unser zutun die Waffe, man muss also
immer selbst die Waffe auswählen, nachdem man sie Aufgenommen hat.
Beide Varianten sind nicht gerade clever, also werden wir da mal Hand anlegen :)
Dieses Tutorial wird also zeigen wie man Waffenwechsel à la Quake implementiert, Clientseitig
völlig variabel. Jeder Client bekommt eine cvar "cg_weaponOrder" mit der die Waffenprioritäten
bestimmt werden. Na dann los...
Wir werden folgende Dateien modifizieren:
cg_weapons.c
cg_main.c
cg_local.c
cg_event.c
g_items.c
g_main.c
bg_public.c
2. Definiere einige globale Variablen und Funktionsprototypen
Öffne zuerst cg_local.h und füge folgendes zu den "cg_main.c" Definitionen (Z 1190):
//
// cg_main.c
//
const char *CG_ConfigString( int index );
const char *CG_Argv( int arg );
void QDECL CG_Printf( const char *msg, ... );
void QDECL CG_Error( const char *msg, ... );
void CG_StartMusic( void );
void CG_UpdateCvars( void );
int CG_CrosshairPlayer( void );
int CG_LastAttacker( void );
void CG_LoadMenus(const char *menuFile);
void CG_KeyEvent(int key, qboolean down);
void CG_MouseEvent(int x, int y);
void CG_EventHandling(int type);
void CG_RankRunFrame( void );
void CG_SetScoreSelection(void *menu);
score_t *CG_GetSelectedScore();
void CG_BuildSpectatorString();
//WarZone
#define NUM_WEAPS 9
extern int cg_weaponsCount;
extern int weaponOrder[NUM_WEAPS];
extern int weaponRawOrder[NUM_WEAPS];
int RateWeapon (int weapon);
int NextWeapon (int curr);
int PrevWeapon (int curr);
Speichern und schliessen. Mit cg_local.h bist du fertig.
3. Die cvar Definieren
Öffne als nächstes cg_main.c und ergänze die cvar Definitionen um folgende
(Z 140):
vmCvar_t cg_deferPlayers;
vmCvar_t cg_drawTeamOverlay;
vmCvar_t cg_teamOverlayUserinfo;
vmCvar_t cg_weaponOrder; //WarZone
int cg_weaponsCount = -1; //WarZone
und in der cvar Tabelle (Z 300):
{ &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE},
{ &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE},
{ &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE } //WarZone
// { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE }
vergiss das Komma am Ende der
cg_trueLightning-Zeile nicht!
4. Die Hilfsfunktionen definieren
Jetzt folgt der Grossteil des neuen Codes. Wir platzieren ihn direkt unter
die cvar Tabelle (Z 305).
{ &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE},
{ &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE},
{ &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE } //WarZone
// { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE }
};
static int cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );
//<WarZone>
int weaponOrder[NUM_WEAPS];
int weaponRawOrder[NUM_WEAPS];
int NextWeapon (int curr)
{
int i;
int w = -1;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weaponRawOrder[i] == curr)
{
w = i;
break;
}
}
if (w == -1)
return curr; //shouldn't happen
return weaponRawOrder[(w + 1) % NUM_WEAPS];
}
int PrevWeapon (int curr)
{
int i;
int w = -1;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weaponRawOrder[i] == curr)
{
w = i;
break;
}
}
if (w == -1)
return curr; //shouldn't happen
return weaponRawOrder[w - 1 >= 0 ? w - 1 : NUM_WEAPS - 1];
}
int RateWeapon (int weapon)
{
weapon--;
if (weapon > 8 || weapon < 0)
return 0; //bad weapon
return weaponOrder[weapon];
}
int contains(int *list, int size, int number)
{
int i;
for (i = 0; i < size; i++)
if (list[i] == number) return 1;
return 0;
}
void UpdateWeaponOrder (void)
{
char *order = cg_weaponOrder.string;
char weapon[3];
int i, start;
int tempOrder[NUM_WEAPS];
int weapUsed[NUM_WEAPS];
int temp;
weapon[1] = '\0';
memset(tempOrder, 0, sizeof(tempOrder));
memset(weapUsed, 0, sizeof(weapUsed));
i = 0;
while (order != NULL && *order != '\0' && i < NUM_WEAPS)
{
weapon[0] = *order;
order++;
if (*order != '\\' && *order != '/')
{
weapon[1] = *order;
weapon[2] = '\0';
order++;
} else {
weapon[1] = '\0';
}
if (*order != '\0')
order++;
temp = atoi( weapon );
if (temp < 1 || temp > NUM_WEAPS)
{
CG_Printf( "Error: %i is out of range. Ignoring..\n", temp );
}
else if ( contains( tempOrder, sizeof(tempOrder)/sizeof(tempOrder[0]), temp ) )
{
CG_Printf( "Error: %s (%i) already in list. Ignoring..\n",
(BG_FindItemForWeapon( temp ))->pickup_name, temp );
} else {
tempOrder[i] = temp;
weapUsed[temp - 1] = 1;
i++;
}
}
//error checking..
start = 0;
for (i = 0; i < NUM_WEAPS; i++)
{
if (weapUsed[i])
continue;
CG_Printf( "Error: %s (%i) not in list. Adding it to front of the list..\n",
(BG_FindItemForWeapon( i + 1 ))->pickup_name, i + 1 );
weaponRawOrder[start++] = i + 1;
}
//build the raw order list
for (i = start; i < NUM_WEAPS; i++)
weaponRawOrder[i] = tempOrder[i - start];
//built the remaping table
for (i = 0; i < NUM_WEAPS; i++)
weaponOrder[weaponRawOrder[i] - 1] = i + 1;
}
//</WarZone>
Also gut, was macht dieser Code ? Wir haben die Funktionen NextWeapon(), PrevWeapon(), RateWeapon(),
contains() und UpdateWeaponOrder() definiert. UpdateWeaponOrder() wird immer dann aufgerufen, wenn der
Client die Waffenproritäten ändert. NextWeapon() und PrevWeapon() dienen, um vorwärts und rückwärts durch
die Waffen zu wechseln. RateWeapon() liefert die Priorität ( die "Güte" entsprechend den Vorlieben des Spielers)
einer Waffe, dies wird z.B. gebraucht um festzustellen, ob eine aufgenommene Waffe besser als die bisher
getragene ist. contains() ist eine einfache Hilfsfunktion, die überprüft, ob das übergebene Feld den übergebenen
Wert enthält.
5. Auf die Änderung der cvar reagieren
Immernoch in cg_main.c, ändern wir die Funktion CG_UpdateCvars() (Z 490):
void CG_UpdateCvars( void ) {
int i;
cvarTable_t *cv;
for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) {
trap_Cvar_Update( cv->vmCvar );
}
// check for modications here
// If team overlay is on, ask for updates from the server. If its off,
// let the server know so we don't receive it
if ( drawTeamOverlayModificationCount != cg_drawTeamOverlay.modificationCount ) {
drawTeamOverlayModificationCount = cg_drawTeamOverlay.modificationCount;
if ( cg_drawTeamOverlay.integer > 0 ) {
trap_Cvar_Set( "teamoverlay", "1" );
} else {
trap_Cvar_Set( "teamoverlay", "0" );
}
// FIXME E3 HACK
trap_Cvar_Set( "teamoverlay", "1" );
}
// if force model changed
if ( forceModelModificationCount != cg_forceModel.modificationCount ) {
forceModelModificationCount = cg_forceModel.modificationCount;
CG_ForceModelChange();
}
//WarZone
if ( cg_weaponsCount != cg_weaponOrder.modificationCount )
{
UpdateWeaponOrder();
cg_weaponsCount = cg_weaponOrder.modificationCount;
}
Dieser Code ruft UpdateWeaponOrder() sobald die cvar "cg_weaponOrder" verändert
wird.
Damit sind wir fertig mit cg_main.c. Öffne nun cg_weapons.c und suche die
Funktion CG_DrawWeaponSelect. Diese Funktion werden wir komplett ersetzen,
du kannst also die alte Version löschen oder auskommentieren. Hier die neue
Version (Z 1440):
void CG_DrawWeaponSelect( void ) {
int i;
int bits;
int count;
int weap;
int x, y, w;
char *name;
float *color;
// don't display if dead
if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) {
return;
}
color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME );
if ( !color ) {
return;
}
trap_R_SetColor( color );
// showing weapon select clears pickup item display, but not the blend blob
cg.itemPickupTime = 0;
// count the number of weapons owned
bits = cg.snap->ps.stats[ STAT_WEAPONS ];
count = 0;
for ( i = 1 ; i < NUM_WEAPS ; i++ ) { //WarZone
if ( bits & ( 1 << i ) ) {
count++;
}
}
x = 320 - count * 20;
y = 380;
weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- select last weapon
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
weap = NextWeapon( weap );
if ( !( bits & ( 1 << weap ) ) ) {
continue;
}
CG_RegisterWeapon( weap );
// draw weapon icon
CG_DrawPic( x, y, 32, 32, cg_weapons[weap].weaponIcon );
// draw selection marker
if ( weap == cg.weaponSelect ) {
CG_DrawPic( x-4, y-4, 40, 40, cgs.media.selectShader );
}
// no ammo cross on top
if ( !cg.snap->ps.ammo[ weap ] ) {
CG_DrawPic( x, y, 32, 32, cgs.media.noammoShader );
}
x += 40;
}
// draw the selected name
if ( cg_weapons[ cg.weaponSelect ].item ) {
name = cg_weapons[ cg.weaponSelect ].item->pickup_name;
if ( name ) {
w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH;
x = ( SCREEN_WIDTH - w ) / 2;
CG_DrawBigStringColor(x, y - 22, name, color);
}
}
trap_R_SetColor( NULL );
}
Mit Hilfe dieser Funktion werden die Icons der Waffen entsprechend der Prioritäten des Spielers angezeigt...
verdammt cool :)
6. Ersetze CG_NextWeapon_f und CG_PrevWeapon_f
Etwas weiter unter findest du CG_NextWeapon_f. Ersetze sie durch folgende Funktion (Z 1540):
void CG_NextWeapon_f( void ) {
int i;
int original;
if ( !cg.snap ) {
return;
}
if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
return;
}
cg.weaponSelectTime = cg.time;
original = cg.weaponSelect;
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
cg.weaponSelect = NextWeapon( cg.weaponSelect ); //WarZone
if ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
Diese Funktion ruft unsere NextWeapon() Funktion aus cg_main.c auf, um die nächste Waffe zu wählen.
Das gleiche Spiel noch einmal mit CG_PrevWeapon_f. Ersetze sie durch folgende Funktion (Z 1570):
void CG_PrevWeapon_f( void ) {
int i;
int original;
if ( !cg.snap ) {
return;
}
if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) {
return;
}
cg.weaponSelectTime = cg.time;
original = cg.weaponSelect;
for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone
cg.weaponSelect = PrevWeapon( cg.weaponSelect ); //WarZone
if ( cg.weaponSelect == WP_GAUNTLET ) {
continue; // never cycle to gauntlet
}
if ( CG_WeaponSelectable( cg.weaponSelect ) ) {
break;
}
}
}
Hier wird PrevWeapon() benutzt, um die nächstschlechtere Waffe entsprechend der Reihenfolge des
Spielers zu wählen.
7. Ersetze CG_OutOfAmmoChange()
Scrolle etwas weiter nach unten und ersetze CG_OutOfAmmoChange durch folgende Funktion (Z 1630):
void CG_OutOfAmmoChange( void ) {
int i;
int weap;
cg.weaponSelectTime = cg.time;
weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- pick the best weapon they have
for ( i = 0 ; i < NUM_WEAPS ; i++, weap = PrevWeapon( weap )) {
if ( CG_WeaponSelectable( weap ) ) {
if (weap != WP_GAUNTLET)
{
cg.weaponSelect = weap;
return;
}
}
}
cg.weaponSelect = WP_GAUNTLET;
}
Hier wird, sobald die aktuelle Waffe ohne Ammo ist, die nächstbeste Waffe ausgewählt.
Das war alles in cg_weapons.c
8. Ändere CG_ItemPickUp
Öffne nun cg_event.c und suche die Funktion CG_ItemPickUp, ersetze sie durch folgende (Z 380):
static void CG_ItemPickup( int itemNum, int isnewitem ) { //WarZone
cg.itemPickup = itemNum;
cg.itemPickupTime = cg.time;
cg.itemPickupBlendTime = cg.time;
// see if it should be the grabbed weapon
if ( bg_itemlist[itemNum].giType == IT_WEAPON && isnewitem ) //WarZone
{
// select it immediately
if ( cg_autoswitch.integer && bg_itemlist[itemNum].giTag != WP_MACHINEGUN ) {
if (RateWeapon( bg_itemlist[itemNum].giTag) > RateWeapon( cg.weaponSelect )) //WarZone
{
cg.weaponSelectTime = cg.time;
cg.weaponSelect = bg_itemlist[itemNum].giTag;
}
}
}
}
Wenn du dir den Code genau anschaust, wirst du feststellen, dass wir dem Funktionsheader von
CG_ItemPickUp den Parameter "isnewitem" hinzugefügt haben. An dieser Stelle mutiert die Modifikation
von "nur Client" zu "sowohl Client als auch Server". Das liegt vor allem daran, dass der Client keine
einfache Möglichkeit hat, festzustellen, ob ein Item gerade aufgenommen wurde, oder schon länger im
Inventar liegt ( man könnte z.B. eine "oldweapons" Variable anlegen, und dann immer die aktuelle
Waffenmenge mit der alten Vergleichen, aber das scheitert spätestens wenn der Spieler stirbt und somit
alle Waffen verliert).
9. Ein neues Event "Item Aufnahme"
Suche nun die Funktion CG_EntityEvent und füge darin folgendes um das EV_ITEM_PICKUP
Event (Z 655):
case EV_ITEM_PICKUP2:
DEBUGNAME("EV_ITEM_PICKUP2");
es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT!
case EV_ITEM_PICKUP2:
DEBUGNAME("EV_ITEM_PICKUP2");
es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT!
case EV_ITEM_PICKUP:
DEBUGNAME("EV_ITEM_PICKUP");
{
gitem_t *item;
int index;
int isnewitem; //WarZone
index = es->eventParm; // player predicted
isnewitem = es->otherEntityNum2; //WarZone
if ( index < 1 || index >= bg_numItems ) {
break;
}
item = &bg_itemlist[ index ];
// powerups and team items will have a separate global sound, this one
// will be played at prediction time
if ( item->giType == IT_POWERUP || item->giType == IT_TEAM) {
trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.n_healthSound );
} else if (item->giType == IT_PERSISTANT_POWERUP) {
#ifdef MISSIONPACK
switch (item->giTag ) {
case PW_SCOUT:
trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.scoutSound );
break;
case PW_GUARD:
trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.guardSound );
break;
case PW_DOUBLER:
trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.doublerSound );
break;
case PW_AMMOREGEN:
trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.ammoregenSound );
break;
}
#endif
} else {
trap_S_StartSound (NULL, es->number,
CHAN_AUTO,
trap_S_RegisterSound( item->pickup_sound,
qfalse ) );
}
// show icon and name on status bar
if ( es->number == cg.snap->ps.clientNum ) {
CG_ItemPickup( index, isnewitem ); //WarZone
}
}
break;
Auch hier beziehen wir uns wieder auf neuen Servercode. es->otherEntityNum2 muss Serverseitig gesetzt
werden, um dem Clientcode mitzuteilen, ob das aufgenommene Item ein neues ist oder nicht. Das neue
Event EV_ITEM_PICKUP2 werde ich weiter unten erklären.
10. Eine weitere Modifikation zur Item-aufnahme
Srolle nun bis EV_GLOBAL_ITEM_PICKUP nach unten und ändere folgendes:
case EV_GLOBAL_ITEM_PICKUP:
DEBUGNAME("EV_GLOBAL_ITEM_PICKUP");
{
gitem_t *item;
int index;
index = es->eventParm; // player predicted
if ( index < 1 || index >= bg_numItems ) {
break;
}
item = &bg_itemlist[ index ];
// powerup pickups are global
trap_S_StartSound (NULL, cg.snap->ps.clientNum,
CHAN_AUTO,
trap_S_RegisterSound( item->pickup_sound ) );
// show icon and name on status bar
if ( es->number == cg.snap->ps.clientNum ) {
CG_ItemPickup( index, 1 ); //WarZone
}
}
break;
Das war alles in cg_event.c
11. Ändere das Verhalten von Touch_Item()
Öffne nun g_items.c im game Projekt und suche die Funktion Touch_Item. Hier werden wir nur einige
kleine Änderungen vornehmen (Z 400):
void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) {
int respawn;
qboolean predict;
int had = 1; //WarZone
...
case IT_WEAPON:
//WarZone
if ( other->client->ps.stats[STAT_WEAPONS] & (1 << ent->item->giTag) )
had = 1;
else
had = 0;
respawn = Pickup_Weapon(ent, other);
// predict = qfalse;
break;
Entferne nun das Stück if (predict) so das es wie folgt aussieht (Z 460):
if ( 0 ) { //WarZone
//do nothing...
} else {
//WarZone
gentity_t *event;
event = G_TempEntity(ent->s.origin, EV_ITEM_PICKUP2); //WarZone
event->s.eventParm = ent->s.modelindex;
event->s.otherEntityNum = other->s.number;
event->s.otherEntityNum2 = !had; //WarZone -- used to tell cgame if its a new weapon
event->r.svFlags |= SVF_BROADCAST; //broadcast it to everyone
// G_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex ); //kill this line
}
...
}
Das "..." bedeutet "irgendwelcher Code dazwischen", also schreib das ja nicht in deinen Code.
Wenn du aufmerksam warst, hast du festgestellt, dass wir die Item-aufnahme Logik drastisch verändert haben.
Der (erste und wichtigste) Grund dafür ist, dass vorhergesagte Events nur ein Argument haben dürfen. Der
zweite Grund ist die Unzuverlässigkeit der Event-vorhersage Struktur. Bist du schon einmal in Quake3 über
ein Item gelaufen, aber der Waffenaufnahme-sound kam nicht ? Das liegt daran, das die Event-vorhersage
Struktur überflutet wird, und dein Event verloren geht.
Wir schicken die EV_ITEM_PICKUP Events über eine externe temporäre entity um sicherzustellen, dass diese
Nachrichten nicht in der Hitze des Gefechts untergehen.
g_items.c ist damit eredigt.
12. Den Server anzeigen lassen, dass er individuelle Waffenordnung unterstützt
Das wird unsere letzte Ergänzung. Öffne g_main.c und füge folgendes an das Ende der cvar Tabelle (Z 160):
{ &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse},
{ &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse},
{ &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse},
{ &g_rankings, "g_rankings", "0", 0, 0, qfalse},
{ NULL, "g_supportsWeaponOrder", "1", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse } //WarZone
achte auch hier wieder auf das Komma am Ende der g_rankings Zeile.
Diese cvar erleichtert es Spielern, diejenigen Server zu finden, welche die cg_weaponOrder variable
unterstützen. Das ist wichtig, da der neue Clientcode nicht mehr zu Standard Q3A Servern kompatibel ist.
Fertig mit g_main.c, und fast fertig mit der mod...
Wir haben vergessen, EV_ITEM_PICKUP2 zu definieren ! Upss.. also fix noch bg_public.h geöffnet und
folgendes angefügt:
EV_TAUNT_NO,
EV_TAUNT_FOLLOWME,
EV_TAUNT_GETFLAG,
EV_TAUNT_GUARDBASE,
EV_TAUNT_PATROL,
EV_ITEM_PICKUP2 // WarZone -- used to create a temp entity to send the pickup message
} entity_event_t;
das Komma, du weisst...
So, das wars endgültig !
13. Schlussbemerkung
Du musst sowohl den game als auch den cgame code kompilieren, damit alles läuft. Und wie funktioniert
die neue cg_weaponOrder variable ? Folgendermassen: der Anfangswert ist "1/2/3/4/5/6/7/8/8", dieser legt
die Reihenfolge fest, in der die Waffen des Spielers gewechselt werden sollen. Der Anfangswert wird ein
Wechselverhalten vergleichbar mit dem in Quake2 erzeugen (wenn neueWaffe > alteWaffe dann waffeWechseln):
wenn du eine Shotgun(3) hast und einen Rocketlauncher(5) aufnimmst, wird zum Rocketlauncher gewechselt.
Wenn du einen Rocketlauncher hast, und einen Granadelauncher aufnimmst, bleibt der Rocketlauncher
ausgewählt.
Jede Waffe hat einen spezifischen Index:
1 = Gauntlet
2 = Machine Gun
3 = Shot Gun
4 = Grenade Launcher
5 = Rocket Launcher
6 = Lightning Gun
7 = Rail Gun
8 = Plasma Gun
9 = BFG 10K
Ich persönlich bevorzuge "1/2/3/4/6/8/5/7/9", was in etwa der Reihenfolge von Quake2 entspricht.
Wenn dein Mod mehr als 9 Waffen enthält, musst du die #define NUM_WEAPS Zeile in cg_local.c entsprechend ändern. Der
cg_weaponOrder String muss wie folgt formattiert werden:
xx\xx\xx
xx/xx/xx
x/xx\xx
xx\x/xx
x/x\x
x/x/x
x\x\x
D.h. zwischen den Seperatoren ('/' oder '\') dürfen bis zu zwie Ziffern stehen. Doppelte oder ungültige
Einträge werden ignoriert und fehlende Einträge werden am Anfang der Liste eingefügt. Der String
"2/2/3/3/4/5/6/7/8/8/8/9" erzeugt das selbe resultat wie "1/2/3/4/5/6/7/8/9", da die fehlende 1 am Anfang
eingefügt wird und die doppelten Werte ignoriert werden.
Dieser Code unterliegt dem Copyright (c) 2000 von Mark "WarZone" Smeltzer. Du darfst den Code frei
verwenden, so lange du auf den Autor verweist.
<<
Tutorial 15 | | Themenübersicht >>